feat(CC-064): add PipelineController (sales pipeline deals with stage/probability)

This commit is contained in:
admin-valentin 2026-08-01 21:24:23 +00:00
parent d48bc25b46
commit bc4ee37359

View file

@ -0,0 +1,56 @@
import { Body, Controller, Get, IsArray, IsNumber, IsOptional, IsString, NotFoundException, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common';
import { and, desc, eq, isNull } from 'drizzle-orm';
import { Type } from 'class-transformer';
import { CurrentSession } from '../auth/session.decorator';
import type { AuthenticatedSession } from '../auth/tenant.guard';
import { db } from '../db/client';
import { pipelineDeals } from '../db/schema';
class CreateDealDto {
@IsString() title!: string;
@IsOptional() @IsString() organizationId?: string;
@IsOptional() @IsString() contactId?: string;
@IsOptional() @IsString() stage?: string;
@IsOptional() @IsNumber() @Type(() => Number) valueMinorUnits?: number;
@IsOptional() @IsString() currency?: string;
@IsOptional() @IsNumber() @Type(() => Number) probability?: number;
@IsOptional() @IsString() expectedCloseDate?: string;
@IsOptional() @IsString() notes?: string;
@IsOptional() @IsArray() tags?: string[];
}
class UpdateDealDto {
@IsOptional() @IsString() title?: string;
@IsOptional() @IsString() stage?: string;
@IsOptional() @IsNumber() @Type(() => Number) valueMinorUnits?: number;
@IsOptional() @IsNumber() @Type(() => Number) probability?: number;
@IsOptional() @IsString() expectedCloseDate?: string;
@IsOptional() @IsString() notes?: string;
}
@Controller('pipeline')
export class PipelineController {
@Get()
async list(@CurrentSession() session: AuthenticatedSession, @Query('stage') stage?: string) {
const where = stage
? and(eq(pipelineDeals.tenantId, session.tenantId), isNull(pipelineDeals.deletedAt), eq(pipelineDeals.stage, stage))
: and(eq(pipelineDeals.tenantId, session.tenantId), isNull(pipelineDeals.deletedAt));
return db.query.pipelineDeals.findMany({ where, orderBy: desc(pipelineDeals.createdAt), limit: 200 });
}
@Post()
async create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateDealDto) {
const [deal] = await db.insert(pipelineDeals).values({
...dto, tenantId: session.tenantId, tags: dto.tags ?? [],
}).returning();
return deal;
}
@Patch(':id')
async update(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateDealDto) {
const [updated] = await db.update(pipelineDeals).set({ ...dto, updatedAt: new Date() })
.where(and(eq(pipelineDeals.id, id), eq(pipelineDeals.tenantId, session.tenantId))).returning();
if (!updated) throw new NotFoundException('Deal not found');
return updated;
}
}