import { Injectable, Logger } from '@nestjs/common'; import { loggableError } from '../common/loggable-error'; import { DomainEventsService } from '../domain-events/domain-events.service'; import { PrismaService } from '../prisma/prisma.service'; import { catchUpEvents, DAILY_EVALUATION_RULES, DailyRule, } from './daily-evaluation.rules'; import { Job, JobResult } from './job'; import { JobName } from './job-names'; /** * Records the time-based domain events (CronJob 04:30): one rule per type, * see DAILY_EVALUATION_RULES. An occurrence is recorded on every run within * CATCH_UP_DAYS after its day under the same daily dedupe key, so a missed * run is caught up and a repeated one adds nothing. The result counts the * events that were new, per type. */ @Injectable() export class DailyEvaluationJob implements Job { readonly name: JobName = 'daily-evaluation'; private readonly logger = new Logger(DailyEvaluationJob.name); constructor( private readonly prisma: PrismaService, private readonly events: DomainEventsService, ) {} /** Evaluates the current day of the database clock. */ async run(): Promise { const [{ now }] = await this.prisma.$queryRaw< { now: Date }[] >`SELECT now() AS "now"`; return this.evaluate(now); } /** * Runs every rule for `today`. A failing rule is logged and does not stop * the others - their events are committed - but fails the run, so it * shows on the System page; the next run catches up. */ async evaluate(today: Date): Promise { const counts: [string, number][] = []; const failed: string[] = []; for (const rule of DAILY_EVALUATION_RULES) { try { counts.push([rule.type, await this.evaluateRule(rule, today)]); } catch (error: unknown) { const { message, stack } = loggableError(error); this.logger.error(`Rule "${rule.type}" failed: ${message}`, stack); failed.push(rule.type); } } if (failed.length > 0) { throw new Error(`Daily evaluation failed for: ${failed.join(', ')}`); } return Object.fromEntries(counts); } /** One transaction per type; returns how many events were new. */ private async evaluateRule(rule: DailyRule, today: Date): Promise { const events = catchUpEvents( await rule.occurrences(this.prisma, today), today, ); if (events.length === 0) { return 0; } return this.prisma.$transaction((tx) => this.events.recordAll(tx, events)); } }