From 988a7db958565441dc38a93be421053188c2ef6a Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:09:05 +0000 Subject: [PATCH] feat(CC-066): add ScenariosController (CRUD + decision grouping) --- src/scenarios/scenarios.controller.ts | 90 +++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/scenarios/scenarios.controller.ts diff --git a/src/scenarios/scenarios.controller.ts b/src/scenarios/scenarios.controller.ts new file mode 100644 index 0000000..102ca88 --- /dev/null +++ b/src/scenarios/scenarios.controller.ts @@ -0,0 +1,90 @@ +import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { IsIn, IsNumberString, IsOptional, IsString, IsUUID } from 'class-validator'; +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 { scenarios } from '../db/schema'; + +class CreateScenarioDto { + @IsString() title!: string; + @IsOptional() @IsString() description?: string; + @IsOptional() @IsUUID() decisionId?: string; + @IsOptional() @IsIn(['low','medium','high']) probability?: string; + @IsOptional() @IsString() outcome?: string; + @IsOptional() @IsNumberString() financialImpactMinorUnits?: string; + @IsOptional() @IsString() financialImpactCurrency?: string; + @IsOptional() @IsIn(['positive','negative','neutral']) impactDirection?: string; + @IsOptional() @IsIn(['hypothetical','likely','confirmed','ruled_out']) status?: string; +} + +class UpdateScenarioDto { + @IsOptional() @IsString() title?: string; + @IsOptional() @IsString() description?: string; + @IsOptional() @IsIn(['low','medium','high']) probability?: string; + @IsOptional() @IsString() outcome?: string; + @IsOptional() @IsNumberString() financialImpactMinorUnits?: string; + @IsOptional() @IsIn(['positive','negative','neutral']) impactDirection?: string; + @IsOptional() @IsIn(['hypothetical','likely','confirmed','ruled_out']) status?: string; +} + +@Controller('scenarios') +export class ScenariosController { + @Get() + async list( + @CurrentSession() session: AuthenticatedSession, + @Query('decisionId') decisionId?: string, + @Query('status') status?: string, + ) { + const tid = session.tenantId; + let where = eq(scenarios.tenantId, tid) as ReturnType; + if (decisionId) where = and(where, eq(scenarios.decisionId, decisionId)) as typeof where; + if (status) where = and(where, eq(scenarios.status, status)) as typeof where; + return db.query.scenarios.findMany({ where, orderBy: [desc(scenarios.createdAt)], limit: 500 }); + } + + @Post() + async create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateScenarioDto) { + const [row] = await db.insert(scenarios).values({ + tenantId: session.tenantId, + title: dto.title, + description: dto.description, + decisionId: dto.decisionId ?? null, + probability: dto.probability ?? 'medium', + outcome: dto.outcome, + financialImpactMinorUnits: dto.financialImpactMinorUnits ?? null, + financialImpactCurrency: dto.financialImpactCurrency ?? 'RON', + impactDirection: dto.impactDirection ?? 'neutral', + status: dto.status ?? 'hypothetical', + }).returning(); + return row; + } + + @Patch(':id') + async update( + @CurrentSession() session: AuthenticatedSession, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateScenarioDto, + ) { + const updates: Record = { updatedAt: new Date() }; + if (dto.title !== undefined) updates.title = dto.title; + if (dto.description !== undefined) updates.description = dto.description; + if (dto.probability !== undefined) updates.probability = dto.probability; + if (dto.outcome !== undefined) updates.outcome = dto.outcome; + if (dto.financialImpactMinorUnits !== undefined) updates.financialImpactMinorUnits = dto.financialImpactMinorUnits; + if (dto.impactDirection !== undefined) updates.impactDirection = dto.impactDirection; + if (dto.status !== undefined) updates.status = dto.status; + const [row] = await db.update(scenarios) + .set(updates) + .where(and(eq(scenarios.id, id), eq(scenarios.tenantId, session.tenantId))) + .returning(); + return row; + } + + @Delete(':id') + async remove(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + await db.delete(scenarios) + .where(and(eq(scenarios.id, id), eq(scenarios.tenantId, session.tenantId))); + return { deleted: true }; + } +}