diff --git a/src/risks/risks.controller.ts b/src/risks/risks.controller.ts new file mode 100644 index 0000000..7288790 --- /dev/null +++ b/src/risks/risks.controller.ts @@ -0,0 +1,68 @@ +import { Body, Controller, Get, HttpCode, IsOptional, IsString, NotFoundException, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { and, desc, eq } from 'drizzle-orm'; +import { CurrentSession } from '../auth/session.decorator'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import { db } from '../db/client'; +import { risks } from '../db/schema'; + +class CreateRiskDto { + @IsString() title!: string; + @IsOptional() @IsString() description?: string; + @IsOptional() @IsString() category?: string; + @IsOptional() @IsString() probability?: string; + @IsOptional() @IsString() impact?: string; + @IsOptional() @IsString() mitigation?: string; + @IsOptional() @IsString() owner?: string; + @IsOptional() @IsString() decisionId?: string; + @IsOptional() @IsString() reviewDueAt?: string; +} + +class UpdateRiskDto { + @IsOptional() @IsString() title?: string; + @IsOptional() @IsString() description?: string; + @IsOptional() @IsString() category?: string; + @IsOptional() @IsString() probability?: string; + @IsOptional() @IsString() impact?: string; + @IsOptional() @IsString() status?: string; + @IsOptional() @IsString() mitigation?: string; + @IsOptional() @IsString() owner?: string; + @IsOptional() @IsString() reviewDueAt?: string; +} + +@Controller('risks') +export class RisksController { + @Get() + async list(@CurrentSession() session: AuthenticatedSession, @Query('status') status?: string) { + const where = status + ? and(eq(risks.tenantId, session.tenantId), eq(risks.status, status)) + : eq(risks.tenantId, session.tenantId); + return db.query.risks.findMany({ where, orderBy: desc(risks.createdAt), limit: 200 }); + } + + @Post() + async create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateRiskDto) { + const [risk] = await db.insert(risks).values({ + ...dto, + tenantId: session.tenantId, + reviewDueAt: dto.reviewDueAt ? new Date(dto.reviewDueAt) : undefined, + }).returning(); + return risk; + } + + @Patch(':id') + async update(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateRiskDto) { + const [updated] = await db.update(risks).set({ + ...dto, + updatedAt: new Date(), + reviewDueAt: dto.reviewDueAt !== undefined ? (dto.reviewDueAt ? new Date(dto.reviewDueAt) : null) : undefined, + }).where(and(eq(risks.id, id), eq(risks.tenantId, session.tenantId))).returning(); + if (!updated) throw new NotFoundException('Risk not found'); + return updated; + } + + @HttpCode(204) @Post(':id/close') + async close(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + await db.update(risks).set({ status: 'closed', updatedAt: new Date() }) + .where(and(eq(risks.id, id), eq(risks.tenantId, session.tenantId))); + } +}