diff --git a/src/pipeline/pipeline.controller.ts b/src/pipeline/pipeline.controller.ts new file mode 100644 index 0000000..e2c7f81 --- /dev/null +++ b/src/pipeline/pipeline.controller.ts @@ -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; + } +}