From c2a070b33f9db84895162683c11ba35db5a67321 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:12:59 +0000 Subject: [PATCH] feat(CC-067): add OutcomeReviewsController (list, create, patch with rating/lessons) --- .../outcome-reviews.controller.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/outcome-reviews/outcome-reviews.controller.ts diff --git a/src/outcome-reviews/outcome-reviews.controller.ts b/src/outcome-reviews/outcome-reviews.controller.ts new file mode 100644 index 0000000..a13e775 --- /dev/null +++ b/src/outcome-reviews/outcome-reviews.controller.ts @@ -0,0 +1,80 @@ +import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { IsBoolean, IsIn, IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator'; +import { Type } from 'class-transformer'; +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 { outcomeReviews } from '../db/schema'; + +class CreateOutcomeReviewDto { + @IsString() title!: string; + @IsOptional() @IsUUID() decisionId?: string; + @IsOptional() @IsString() periodStart?: string; + @IsOptional() @IsString() periodEnd?: string; + @IsOptional() @IsString() actualOutcome?: string; + @IsOptional() @IsBoolean() @Type(() => Boolean) wasSuccessful?: boolean; + @IsOptional() @IsString() lessonsLearned?: string; + @IsOptional() @IsString() nextSteps?: string; + @IsOptional() @IsInt() @Min(1) @Max(10) @Type(() => Number) rating?: number; +} + +class UpdateOutcomeReviewDto { + @IsOptional() @IsString() title?: string; + @IsOptional() @IsString() actualOutcome?: string; + @IsOptional() @IsBoolean() @Type(() => Boolean) wasSuccessful?: boolean; + @IsOptional() @IsString() lessonsLearned?: string; + @IsOptional() @IsString() nextSteps?: string; + @IsOptional() @IsInt() @Min(1) @Max(10) @Type(() => Number) rating?: number; +} + +@Controller('outcome-reviews') +export class OutcomeReviewsController { + @Get() + async list( + @CurrentSession() session: AuthenticatedSession, + @Query('decisionId') decisionId?: string, + ) { + const tid = session.tenantId; + let where = eq(outcomeReviews.tenantId, tid) as ReturnType; + if (decisionId) where = and(where, eq(outcomeReviews.decisionId, decisionId)) as typeof where; + return db.query.outcomeReviews.findMany({ where, orderBy: [desc(outcomeReviews.createdAt)], limit: 200 }); + } + + @Post() + async create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateOutcomeReviewDto) { + const [row] = await db.insert(outcomeReviews).values({ + tenantId: session.tenantId, + title: dto.title, + decisionId: dto.decisionId ?? null, + periodStart: dto.periodStart ?? null, + periodEnd: dto.periodEnd ?? null, + actualOutcome: dto.actualOutcome, + wasSuccessful: dto.wasSuccessful ?? null, + lessonsLearned: dto.lessonsLearned, + nextSteps: dto.nextSteps, + rating: dto.rating ?? null, + }).returning(); + return row; + } + + @Patch(':id') + async update( + @CurrentSession() session: AuthenticatedSession, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateOutcomeReviewDto, + ) { + const upd: Record = { updatedAt: new Date() }; + if (dto.title !== undefined) upd.title = dto.title; + if (dto.actualOutcome !== undefined) upd.actualOutcome = dto.actualOutcome; + if (dto.wasSuccessful !== undefined) upd.wasSuccessful = dto.wasSuccessful; + if (dto.lessonsLearned !== undefined) upd.lessonsLearned = dto.lessonsLearned; + if (dto.nextSteps !== undefined) upd.nextSteps = dto.nextSteps; + if (dto.rating !== undefined) upd.rating = dto.rating; + const [row] = await db.update(outcomeReviews) + .set(upd) + .where(and(eq(outcomeReviews.id, id), eq(outcomeReviews.tenantId, session.tenantId))) + .returning(); + return row; + } +}