Files
ApiCore/ApiCore-Backend/src/calendar-sync/calendar-sync.handlers.ts
T
Nick SzaunigandClaude Opus 5.5 ef33e5549b feat: T4 wave 5 - colony end cleanup, withdrawal chain, next steps, inspection takeover UI
- Colony end releases the queen in the same transaction; open
  suggestions become obsolete and a CLEANUP suggestion lists open
  tasks and the queen; apply-cleanup validates evidence and blocks
  reactivated colonies
- Withdrawal chain: plausibility warnings on harvest and bottling,
  conflict notifications also when a treatment is entered or corrected
  after the harvest (naming bottled lots), withdrawal spans in the
  calendar and M365 sync, Berlin-day end events
- Next 3 steps per colony and the "Heute" endpoint with ranking and an
  explicit merge table by intent
- Inspection-triggered rule suggestions follow the colony's latest
  inspection (resolve when the finding is gone, refresh when it stays)
- Frontend: colony card takeover in the inspection form, feed status as
  condition, occupied frames 0-40; legacy offline entries keep syncing
- Notifications and preferences are deleted with their user

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-14 16:28:07 +02:00

105 lines
3.4 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import type { AnyDomainEventEnvelope } from '../domain-events/domain-event-types';
import { OnDomainEvent } from '../domain-events/on-domain-event.decorator';
import { Prisma } from '../../generated/prisma';
import { CalendarSyncService } from './calendar-sync.service';
/**
* Puts calendar-relevant records into the M365 outbox. Pure DB writer: the
* Graph call itself happens in CalendarSyncService.processOutbox (worker
* loop). Errors propagate, so a failed delivery is retried.
*/
@Injectable()
export class CalendarSyncEventHandlers {
constructor(private readonly calendarSync: CalendarSyncService) {}
@OnDomainEvent(
['InspectionRecorded', 'InspectionCorrected'],
'calendar-sync.inspection',
)
async inspection(
event: AnyDomainEventEnvelope<'InspectionRecorded' | 'InspectionCorrected'>,
tx: Prisma.TransactionClient,
): Promise<void> {
await this.calendarSync.enqueueIn(tx, 'INSPECTION', event.aggregateId);
}
@OnDomainEvent(
['TreatmentRecorded', 'TreatmentCorrected'],
'calendar-sync.treatment',
)
async treatment(
event: AnyDomainEventEnvelope<'TreatmentRecorded' | 'TreatmentCorrected'>,
tx: Prisma.TransactionClient,
): Promise<void> {
await this.calendarSync.enqueueIn(tx, 'TREATMENT', event.aggregateId);
}
// The same events, a second entry: the withdrawal span of the treatment.
// Enqueued also when the treatment has no withdrawal days (any more): the
// worker then removes an event created earlier.
@OnDomainEvent(
['TreatmentRecorded', 'TreatmentCorrected'],
'calendar-sync.withdrawal',
)
async withdrawal(
event: AnyDomainEventEnvelope<'TreatmentRecorded' | 'TreatmentCorrected'>,
tx: Prisma.TransactionClient,
): Promise<void> {
await this.calendarSync.enqueueIn(tx, 'WITHDRAWAL', event.aggregateId);
}
@OnDomainEvent(
['FeedingRecorded', 'FeedingCorrected'],
'calendar-sync.feeding',
)
async feeding(
event: AnyDomainEventEnvelope<'FeedingRecorded' | 'FeedingCorrected'>,
tx: Prisma.TransactionClient,
): Promise<void> {
await this.calendarSync.enqueueIn(tx, 'FEEDING', event.aggregateId);
}
@OnDomainEvent(
['HarvestRecorded', 'HarvestCorrected'],
'calendar-sync.harvest',
)
async harvest(
event: AnyDomainEventEnvelope<'HarvestRecorded' | 'HarvestCorrected'>,
tx: Prisma.TransactionClient,
): Promise<void> {
await this.calendarSync.enqueueIn(tx, 'HARVEST', event.aggregateId);
}
// Updated and completed tasks are enqueued even without a due date or
// when done: the worker then removes an event created earlier.
@OnDomainEvent(
['TaskCreated', 'TaskUpdated', 'TaskCompleted'],
'calendar-sync.task',
)
async task(
event: AnyDomainEventEnvelope<
'TaskCreated' | 'TaskUpdated' | 'TaskCompleted'
>,
tx: Prisma.TransactionClient,
): Promise<void> {
if (event.type === 'TaskCreated' && event.payload.dueDate === null) {
return;
}
await this.calendarSync.enqueueIn(tx, 'TASK', event.aggregateId);
}
// The aggregate is the queen; the calendar entry belongs to the history row.
@OnDomainEvent(['QueenAssigned'], 'calendar-sync.queen-assignment')
async queenAssignment(
event: AnyDomainEventEnvelope<'QueenAssigned'>,
tx: Prisma.TransactionClient,
): Promise<void> {
await this.calendarSync.enqueueIn(
tx,
'QUEEN_ASSIGNMENT',
event.payload.historyId,
);
}
}