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:
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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user