import { Injectable } from '@nestjs/common'; import type { AnyDomainEventEnvelope } from '../domain-events/domain-event-types'; import { OnDomainEvent } from '../domain-events/on-domain-event.decorator'; import { isLivingColonyStatus } from '../colonies/colony-status'; import { Prisma } from '../../generated/prisma'; import { StockService } from '../stock/stock.service'; import { AutomationService } from './automation.service'; type Tx = Prisma.TransactionClient; /** * Evaluates the automation rules on domain events. Pure DB writer: it reads * the record the event is about through the delivery transaction and lets * AutomationService write suggestion, notification and audit entries in it. * A record deleted before the event was dispatched, or of a colony that * ended, is skipped. Errors propagate, so the delivery is rolled back as a * whole and retried. */ @Injectable() export class AutomationEventHandlers { constructor( private readonly automation: AutomationService, private readonly stock: StockService, ) {} /** * Fast path only: proposeForRuleIn re-checks the colony under its lock. * Whether the colony still lives (a colony that ended takes no proposals). */ private async isLivingColonyIn(tx: Tx, colonyId: string): Promise { const colony = await tx.colony.findUnique({ where: { id: colonyId }, select: { status: true }, }); return colony !== null && isLivingColonyStatus(colony.status); } @OnDomainEvent('InspectionRecorded', 'automation.inspection-rules') async inspectionRules( event: AnyDomainEventEnvelope<'InspectionRecorded'>, tx: Tx, ): Promise { const inspection = await tx.inspection.findUnique({ where: { id: event.aggregateId }, }); if ( inspection === null || !(await this.isLivingColonyIn(tx, inspection.colonyId)) ) { return; } await this.automation.evaluateInspectionIn(tx, inspection); } // A correction can lift a count over the threshold, so it is evaluated // like a new measurement; the open-suggestion check keeps it to one. @OnDomainEvent( ['VarroaMeasured', 'VarroaCorrected'], 'automation.varroa-threshold', ) async varroaThreshold( event: AnyDomainEventEnvelope<'VarroaMeasured' | 'VarroaCorrected'>, tx: Tx, ): Promise { const measurement = await tx.varroaMeasurement.findUnique({ where: { id: event.aggregateId }, }); if ( measurement === null || !(await this.isLivingColonyIn(tx, measurement.colonyId)) ) { return; } await this.automation.evaluateVarroaMeasurementIn(tx, measurement); } @OnDomainEvent('TreatmentRecorded', 'automation.treatment-follow-up') async treatmentFollowUp( event: AnyDomainEventEnvelope<'TreatmentRecorded'>, tx: Tx, ): Promise { const treatment = await tx.treatment.findUnique({ where: { id: event.aggregateId }, }); if ( treatment === null || !(await this.isLivingColonyIn(tx, treatment.colonyId)) ) { return; } await this.automation.evaluateTreatmentIn(tx, treatment); } @OnDomainEvent('FeedingRecorded', 'automation.feeding-follow-up') async feedingFollowUp( event: AnyDomainEventEnvelope<'FeedingRecorded'>, tx: Tx, ): Promise { const feeding = await tx.feeding.findUnique({ where: { id: event.aggregateId }, }); if ( feeding === null || !(await this.isLivingColonyIn(tx, feeding.colonyId)) ) { return; } await this.automation.evaluateFeedingIn(tx, feeding); } // Decides on the snapshots the event carries, not on the colony as it is // now: a later edit must not hide the transition into CRITICAL. @OnDomainEvent('ColonyStatusChanged', 'automation.colony-health') async colonyHealth( event: AnyDomainEventEnvelope<'ColonyStatusChanged'>, tx: Tx, ): Promise { if (!(await this.isLivingColonyIn(tx, event.aggregateId))) { return; } await this.automation.evaluateColonyHealthIn( tx, event.aggregateId, event.payload.from, event.payload.to, { id: event.id, occurredAt: event.occurredAt }, ); } // Per kind the change touched, what is left of it across all its lots. @OnDomainEvent('StockChanged', 'automation.raw-material-low') async rawMaterialLow( event: AnyDomainEventEnvelope<'StockChanged'>, tx: Tx, ): Promise { for (const kind of new Set(event.payload.rawMaterialKinds)) { const remaining = await this.stock.remainingByRawMaterialKind(kind); await this.automation.evaluateRawMaterialLowIn(tx, kind, remaining); } } }