From 76cf59d82ea825f96df9a676fb077ccbd8edea5d Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sat, 1 Aug 2026 21:24:20 +0000 Subject: [PATCH] feat(CC-064): add ContractsController (CRUD contracts with parties/status) --- src/contracts/contracts.controller.ts | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/contracts/contracts.controller.ts diff --git a/src/contracts/contracts.controller.ts b/src/contracts/contracts.controller.ts new file mode 100644 index 0000000..845f692 --- /dev/null +++ b/src/contracts/contracts.controller.ts @@ -0,0 +1,70 @@ +import { Body, Controller, Get, HttpCode, IsArray, IsOptional, IsString, NotFoundException, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { and, desc, eq, isNull } from 'drizzle-orm'; +import { CurrentSession } from '../auth/session.decorator'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import { db } from '../db/client'; +import { contracts } from '../db/schema'; + +class CreateContractDto { + @IsString() title!: string; + @IsOptional() @IsString() contractType?: string; + @IsOptional() @IsString() organizationId?: string; + @IsOptional() @IsArray() parties?: string[]; + @IsOptional() @IsString() startDate?: string; + @IsOptional() @IsString() endDate?: string; + @IsOptional() @IsString() notes?: string; + @IsOptional() @IsArray() tags?: string[]; +} + +class UpdateContractDto { + @IsOptional() @IsString() title?: string; + @IsOptional() @IsString() status?: string; + @IsOptional() @IsString() contractType?: string; + @IsOptional() @IsArray() parties?: string[]; + @IsOptional() @IsString() startDate?: string; + @IsOptional() @IsString() endDate?: string; + @IsOptional() @IsString() notes?: string; + @IsOptional() @IsArray() tags?: string[]; +} + +@Controller('contracts') +export class ContractsController { + @Get() + async list(@CurrentSession() session: AuthenticatedSession, @Query('status') status?: string) { + const where = status + ? and(eq(contracts.tenantId, session.tenantId), isNull(contracts.deletedAt), eq(contracts.status, status)) + : and(eq(contracts.tenantId, session.tenantId), isNull(contracts.deletedAt)); + return db.query.contracts.findMany({ where, orderBy: desc(contracts.createdAt), limit: 200 }); + } + + @Post() + async create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateContractDto) { + const [contract] = await db.insert(contracts).values({ + ...dto, tenantId: session.tenantId, parties: dto.parties ?? [], tags: dto.tags ?? [], + }).returning(); + return contract; + } + + @Get(':id') + async getOne(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + const contract = await db.query.contracts.findFirst({ + where: and(eq(contracts.id, id), eq(contracts.tenantId, session.tenantId), isNull(contracts.deletedAt)), + }); + if (!contract) throw new NotFoundException('Contract not found'); + return contract; + } + + @Patch(':id') + async update(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateContractDto) { + const [updated] = await db.update(contracts).set({ ...dto, updatedAt: new Date() }) + .where(and(eq(contracts.id, id), eq(contracts.tenantId, session.tenantId))).returning(); + if (!updated) throw new NotFoundException('Contract not found'); + return updated; + } + + @HttpCode(204) @Post(':id/archive') + async archive(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + await db.update(contracts).set({ deletedAt: new Date() }) + .where(and(eq(contracts.id, id), eq(contracts.tenantId, session.tenantId))); + } +}