import { ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import type { JwtUser } from '../common/auth.types.js'; import { PrismaService } from '../prisma/prisma.service.js'; import type { CreateKeyResultDto } from './dto/create-key-result.dto.js'; import type { UpdateProgressDto } from './dto/update-progress.dto.js'; const keyResultInclude = { objective: { include: { owner: { select: { id: true, name: true, username: true, email: true, role: true } }, }, }, } satisfies Prisma.KeyResultInclude; export type KeyResultWithObjective = Prisma.KeyResultGetPayload<{ include: typeof keyResultInclude }>; @Injectable() export class KeyResultsService { constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {} async getById(id: number, user: JwtUser): Promise { const keyResult = await this.prisma.keyResult.findUnique({ where: { id }, include: keyResultInclude }); if (keyResult === null) { throw new NotFoundException('Key result not found'); } this.assertCanRead(keyResult.objective.ownerId, user); return keyResult; } async create(dto: CreateKeyResultDto, user: JwtUser): Promise { const objective = await this.prisma.objective.findUnique({ where: { id: dto.objectiveId } }); if (objective === null) { throw new NotFoundException('Objective not found'); } this.assertCanWrite(objective.ownerId, user); const keyResult = await this.prisma.keyResult.create({ data: { objectiveId: dto.objectiveId, title: dto.title, progress: dto.progress, startValue: dto.startValue, targetValue: dto.targetValue, deadline: new Date(dto.deadline), }, include: keyResultInclude, }); await this.recalculateObjectiveStatus(dto.objectiveId); return keyResult; } async updateProgress(id: number, dto: UpdateProgressDto, user: JwtUser): Promise { const existing = await this.prisma.keyResult.findUnique({ where: { id }, include: keyResultInclude }); if (existing === null) { throw new NotFoundException('Key result not found'); } this.assertCanWrite(existing.objective.ownerId, user); const updated = await this.prisma.$transaction(async (tx) => { const keyResult = await tx.keyResult.update({ where: { id }, data: { progress: dto.progress }, include: keyResultInclude, }); await tx.progressUpdate.create({ data: { keyResultId: id, progress: dto.progress, comment: dto.comment, createdById: user.sub, }, }); await this.recalculateObjectiveStatus(existing.objectiveId, tx); return keyResult; }); return updated; } private assertCanRead(ownerId: number, user: JwtUser): void { if (user.role === 'EMPLOYEE' && ownerId !== user.sub) { throw new ForbiddenException('Key result belongs to another owner'); } } private assertCanWrite(ownerId: number, user: JwtUser): void { if (user.role === 'EMPLOYEE' && ownerId !== user.sub) { throw new ForbiddenException('Only the owner can update this key result'); } } private async recalculateObjectiveStatus( objectiveId: number, tx: Prisma.TransactionClient = this.prisma, ): Promise { const keyResults = await tx.keyResult.findMany({ where: { objectiveId } }); const average = keyResults.length === 0 ? 0 : Math.round(keyResults.reduce((total, keyResult) => total + keyResult.progress, 0) / keyResults.length); const status = average === 0 ? 'NOT_STARTED' : average >= 100 ? 'COMPLETED' : 'IN_PROGRESS'; await tx.objective.update({ where: { id: objectiveId }, data: { status } }); } }