feat: T6 wave 4 – notice PDFs, compliance notifications, readiness core
- Authority notice PDFs: pure model plus renderer, TSK count in A4 landscape, FEHLT for missing particulars, stored totals with correction note; GET /authority-notices/:id/pdf with compliance:read. - Compliance notifications in category COMPLIANCE: herd change and TSK reference day, at most one unread per topic and user (advisory lock); daily rule TskReferenceDayReached with Berlin day and catch-up; handlers in API and worker. - Readiness core: live evaluation of seven pure rules, fingerprinted acknowledgements that reopen when revoked or stale, subject masking, read-only GET paths; READINESS_RULES injectable; kept out of the worker. - e2e hygiene: no unscoped deletes, seeded settings rows saved/restored. - ARCHITECTURE.md §12.8–12.10, §10 handler rows, Annahmen 78–85. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
edc193f128
commit
b912d29166
54 files changed
+5300
-14
No files matched your search
@@ -43,6 +43,11 @@ interface FakeRows {
|
||||
varroa?: { id: string; colonyId: string; date: Date }[];
|
||||
/** The latest inspection per colony. */
|
||||
inspections?: { colonyId: string; date: Date }[];
|
||||
/**
|
||||
* The ComplianceSettings reference day (MM-TT); null: no settings row.
|
||||
* Without a value a day far from the other tests' days is used.
|
||||
*/
|
||||
tskReferenceDay?: string | null;
|
||||
/** Stored rule configs by trigger type; others read as the built-in setting. */
|
||||
rules?: Record<string, object>;
|
||||
}
|
||||
@@ -87,6 +92,20 @@ function fakeDb(rows: FakeRows): FakeDb {
|
||||
})),
|
||||
),
|
||||
},
|
||||
complianceSettings: {
|
||||
findUnique: jest.fn().mockResolvedValue(
|
||||
rows.tskReferenceDay === null
|
||||
? null
|
||||
: {
|
||||
id: 'singleton',
|
||||
tskReferenceDay: rows.tskReferenceDay ?? '10-10',
|
||||
},
|
||||
),
|
||||
// A missing row is created with the schema default.
|
||||
upsert: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ id: 'singleton', tskReferenceDay: '01-01' }),
|
||||
},
|
||||
// No stored phenology: every apiary gets the calendar state.
|
||||
apiaryPhenology: { findMany: jest.fn().mockResolvedValue([]) },
|
||||
automationRule: {
|
||||
@@ -154,9 +173,60 @@ describe('daily evaluation rules', () => {
|
||||
'QueenAged',
|
||||
'VarroaMeasurementDue',
|
||||
'InspectionOverdue',
|
||||
'TskReferenceDayReached',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('TskReferenceDayReached', () => {
|
||||
const only = (events: DailyEvent[]) =>
|
||||
events.filter((event) => event.type === 'TskReferenceDayReached');
|
||||
|
||||
it('emits TskReferenceDayReached on the configured day', async () => {
|
||||
const { db } = fakeDb({ tskReferenceDay: '03-15' });
|
||||
|
||||
expect(only(await eventsOn(db, '2027-03-15'))).toEqual([
|
||||
{
|
||||
type: 'TskReferenceDayReached',
|
||||
aggregateType: 'Compliance',
|
||||
aggregateId: 'tsk:2027',
|
||||
payload: { year: 2027, referenceDay: '2027-03-15' },
|
||||
dedupeKey: 'TskReferenceDayReached:tsk:2027:2027-03-15',
|
||||
},
|
||||
]);
|
||||
expect(only(await eventsOn(db, '2027-03-14'))).toEqual([]);
|
||||
});
|
||||
|
||||
it('catches up within 7 days exactly once', async () => {
|
||||
const { db } = fakeDb({ tskReferenceDay: '03-15' });
|
||||
|
||||
const store = await runDaily(db, daysBetween('2027-03-15', '2027-03-22'));
|
||||
expect(keysOf(store, 'TskReferenceDayReached')).toEqual([
|
||||
'TskReferenceDayReached:tsk:2027:2027-03-15',
|
||||
]);
|
||||
expect(only(await eventsOn(db, '2027-03-23'))).toEqual([]);
|
||||
});
|
||||
|
||||
it("covers the previous year's day in early January", async () => {
|
||||
const { db } = fakeDb({ tskReferenceDay: '12-30' });
|
||||
|
||||
const events = only(await eventsOn(db, '2027-01-03'));
|
||||
|
||||
expect(events.map((event) => event.payload)).toEqual([
|
||||
{ year: 2026, referenceDay: '2026-12-30' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates the settings row with its default 01-01 when it is missing', async () => {
|
||||
const { db } = fakeDb({ tskReferenceDay: null });
|
||||
|
||||
const events = only(await eventsOn(db, '2027-01-04'));
|
||||
|
||||
expect(events.map((event) => event.payload)).toEqual([
|
||||
{ year: 2027, referenceDay: '2027-01-01' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('withdrawalEndDay', () => {
|
||||
it('a 14-day withdrawal of a treatment ending 2026-10-01 ends on 2026-10-15', () =>
|
||||
expect(
|
||||
@@ -387,7 +457,8 @@ describe('daily evaluation rules', () => {
|
||||
});
|
||||
|
||||
it('QueenAged fires on the first job day a queen assigned in June is aged, and not a second time in the same year', async () => {
|
||||
const { db, colonyFindMany } = fakeDb({});
|
||||
// The TSK day lies outside every window this test runs.
|
||||
const { db, colonyFindMany } = fakeDb({ tskReferenceDay: '03-03' });
|
||||
// The queen (hatched 2024) is assigned to the colony on 2026-06-15.
|
||||
let assigned = false;
|
||||
colonyFindMany.mockImplementation(
|
||||
|
||||
Reference in new issue
Block a user