feat: platform backbone and field capture complete (checkpoint)

Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed:
- webhooks with delivery table, retries and stable delivery id
- calendar handlers; a finished task removes its Outlook event
- tasks, automations, inspections, varroa, treatments, feedings,
  harvests, colonies, wintering, stock and bottling record domain events
  in the same transaction; legacy post-commit calls removed
- row locks through one helper (lockRowForUpdate, lockRowsForShare,
  lockRowsForUpdate)
- job framework with JobRun, cli exit codes, cleanup, task templates,
  daily evaluation, hourly app-backup and the rekey job
- system API and page "Verwaltung > System" with retry
- @nestjs/schedule removed, guard tests against in-process schedulers

Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed:
- inspection photo offline without metadata
- bulk capture for treatments, feedings and varroa (backend + frontend)
- offline stand mode with service worker, offline pages and packs
- offline session, cold start and logout on shared devices

Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md.
Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema
drift; frontend lint, typecheck, 913 tests, build. Not rolled out.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 11:36:38 +02:00
1 parent c01a38835a
commit cdd30cf3ee
288 files changed
+39165 -2853

No files matched your search

@@ -0,0 +1,115 @@
import { CalendarSyncService } from './calendar-sync.service';
import { CalendarSyncEventHandlers } from './calendar-sync.handlers';
import type {
AnyDomainEventEnvelope,
DomainEventType,
} from '../domain-events/domain-event-types';
import { domainEventHandlerMetadata } from '../domain-events/on-domain-event.decorator';
import { Prisma } from '../../generated/prisma';
describe('CalendarSyncEventHandlers', () => {
let handlers: CalendarSyncEventHandlers;
let calendarSync: { enqueueIn: jest.Mock };
const tx = {} as Prisma.TransactionClient;
const envelope = <T extends DomainEventType>(
type: T,
aggregateId: string,
payload: Record<string, unknown>,
) => ({ type, aggregateId, payload }) as unknown as AnyDomainEventEnvelope<T>;
beforeEach(() => {
calendarSync = { enqueueIn: jest.fn().mockResolvedValue(undefined) };
handlers = new CalendarSyncEventHandlers(
calendarSync as unknown as CalendarSyncService,
);
});
it.each([
['inspection', 'INSPECTION', 'InspectionRecorded'],
['inspection', 'INSPECTION', 'InspectionCorrected'],
['treatment', 'TREATMENT', 'TreatmentRecorded'],
['treatment', 'TREATMENT', 'TreatmentCorrected'],
['feeding', 'FEEDING', 'FeedingRecorded'],
['feeding', 'FEEDING', 'FeedingCorrected'],
['harvest', 'HARVEST', 'HarvestRecorded'],
['harvest', 'HARVEST', 'HarvestCorrected'],
] as const)(
'%s handler enqueues %s on %s through tx',
async (method, sourceType, type) => {
await handlers[method](envelope(type, 'src-1', {}) as never, tx);
expect(calendarSync.enqueueIn).toHaveBeenCalledWith(
tx,
sourceType,
'src-1',
);
},
);
it.each(['TaskCreated', 'TaskUpdated', 'TaskCompleted'] as const)(
'task handler enqueues the task on %s',
async (type) => {
await handlers.task(
envelope(type, 'task-1', { dueDate: '2027-05-01' }),
tx,
);
expect(calendarSync.enqueueIn).toHaveBeenCalledWith(tx, 'TASK', 'task-1');
},
);
it('TaskCreated without a due date enqueues nothing', async () => {
await handlers.task(
envelope('TaskCreated', 'task-1', { dueDate: null }),
tx,
);
expect(calendarSync.enqueueIn).not.toHaveBeenCalled();
});
it('TaskUpdated without a due date still enqueues, so the worker removes the event', async () => {
await handlers.task(
envelope('TaskUpdated', 'task-1', { dueDate: null, status: 'OPEN' }),
tx,
);
expect(calendarSync.enqueueIn).toHaveBeenCalledWith(tx, 'TASK', 'task-1');
});
it('lets an enqueue failure propagate so the delivery is retried', async () => {
calendarSync.enqueueIn.mockRejectedValue(new Error('db down'));
await expect(
handlers.inspection(envelope('InspectionRecorded', 'i-1', {}), tx),
).rejects.toThrow('db down');
});
it('registers the stable handler ids and event types', () => {
const registered = (method: keyof CalendarSyncEventHandlers) =>
domainEventHandlerMetadata(
Object.getOwnPropertyDescriptor(
CalendarSyncEventHandlers.prototype,
method,
)?.value as object,
);
expect(registered('inspection')).toEqual([
{
types: ['InspectionRecorded', 'InspectionCorrected'],
handlerId: 'calendar-sync.inspection',
},
]);
expect(registered('treatment')[0].handlerId).toBe(
'calendar-sync.treatment',
);
expect(registered('feeding')[0].handlerId).toBe('calendar-sync.feeding');
expect(registered('harvest')[0].handlerId).toBe('calendar-sync.harvest');
expect(registered('task')).toEqual([
{
types: ['TaskCreated', 'TaskUpdated', 'TaskCompleted'],
handlerId: 'calendar-sync.task',
},
]);
});
});