import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { DbClient } from '../prisma/db-client'; import { lockRowForUpdate } from '../prisma/row-lock'; import { AuditLogService } from '../audit/audit-log.service'; import { DomainEventsService } from '../domain-events/domain-events.service'; import { TasksService } from '../tasks/tasks.service'; import { Inspection, Task } from '../../generated/prisma'; import { CreateInspectionDto } from './dto/create-inspection.dto'; import { CreateTaskFromInspectionDto } from './dto/create-task-from-inspection.dto'; import { UpdateInspectionDto } from './dto/update-inspection.dto'; import { optionalDate } from '../common/patch-values'; /** * Inspections are written together with their audit entry and domain event * in one transaction: InspectionRecorded, InspectionCorrected. Automation * rules (automation.inspection-rules, on InspectionRecorded only), calendar * entry (calendar-sync.inspection) and the inspection.created webhook * (webhooks.forward) follow from the event, never from a direct call - so * none of them can fail the request or run twice on a retried one. */ @Injectable() export class InspectionsService { constructor( private readonly prisma: PrismaService, private readonly auditLog: AuditLogService, private readonly events: DomainEventsService, private readonly tasks: TasksService, ) {} async findAll(): Promise { return this.prisma.inspection.findMany({ orderBy: { date: 'desc' } }); } async findById(id: string): Promise { return this.findIn(this.prisma, id); } async findByColony(colonyId: string): Promise { return this.prisma.inspection.findMany({ where: { colonyId }, orderBy: { date: 'desc' }, }); } async create( dto: CreateInspectionDto, actingUserId: string, ): Promise { return this.prisma.$transaction(async (tx) => { const inspection = await tx.inspection.create({ data: { ...dto, date: new Date(dto.date), userId: actingUserId }, }); await this.auditLog.record( { entityType: 'Inspection', entityId: inspection.id, action: 'CREATE', userId: actingUserId, afterData: inspection, }, tx, ); await this.events.record(tx, { type: 'InspectionRecorded', aggregateType: 'Inspection', aggregateId: inspection.id, payload: { colonyId: inspection.colonyId }, actorUserId: actingUserId, }); return inspection; }); } /** * A correction evaluates no automation rule, as before the event * backbone. InspectionCorrected moves the calendar entry when the date or * colony changed instead of leaving the old one behind. */ async update( id: string, dto: UpdateInspectionDto, actingUserId: string, ): Promise { return this.prisma.$transaction(async (tx) => { // "before" is read under the row lock: a concurrent correction can // neither slip in between nor be mislabelled in the audit entry. await lockRowForUpdate(tx, 'Inspection', id); const before = await this.findIn(tx, id); const inspection = await tx.inspection.update({ where: { id }, data: { ...dto, date: dto.date ? new Date(dto.date) : undefined }, }); await this.auditLog.record( { entityType: 'Inspection', entityId: id, action: 'UPDATE', userId: actingUserId, beforeData: before, afterData: inspection, }, tx, ); await this.events.record(tx, { type: 'InspectionCorrected', aggregateType: 'Inspection', aggregateId: id, payload: { colonyId: inspection.colonyId }, actorUserId: actingUserId, }); return inspection; }); } // Through TasksService like a task from the task page: audit entry, // TaskCreated and with it calendar entry and webhook do not depend on // where the task was started. async createTaskFromInspection( id: string, dto: CreateTaskFromInspectionDto, actingUserId: string, ): Promise { const inspection = await this.findById(id); return this.prisma.$transaction((tx) => this.tasks.createInTx( tx, { title: dto.title, description: dto.description, priority: dto.priority, dueDate: optionalDate(dto.dueDate), assignedUserId: dto.assignedUserId, relatedType: 'COLONY', relatedId: inspection.colonyId, sourceInspectionId: inspection.id, }, actingUserId, ), ); } private async findIn(db: DbClient, id: string): Promise { const inspection = await db.inspection.findUnique({ where: { id } }); if (!inspection) { throw new NotFoundException('Inspection not found'); } return inspection; } }