feat: T6 wave 1 – compliance schema, permissions, lock order, backup
Backend Image / build-and-push (push) Successful in 6m39s
Frontend Image / build-and-push (push) Successful in 11m25s

- Migrations (additive): veterinary medicines with lots and movement
  ledger, treatment medicine/lot/dose links, label particulars, authority
  notices, apiary restrictions, compliance settings and acknowledgements;
  notification category COMPLIANCE, added to non-empty Telegram lists.
- Permissions veterinary-medicines:* and compliance:* with role grants.
- LOCKABLE_TABLES: Apiary locks before Colony; T6 tables appended.
- Backup exports and restores the new tables in FK order; unsealed files
  skip them and report dropped medicine links; an open TSK draft in the
  target wins.
- describeNullContracts shared helper; Telegram backlog e2e no longer
  depends on the time of day.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-15 13:04:13 +02:00
1 parent 7a1480138d
commit 8a31c42578
23 files changed
+2028 -84

No files matched your search

+214
View File
@@ -26,6 +26,15 @@ const SECTIONS_CHILD_FIRST = [
'storageContainer',
'extraction',
'harvest',
'veterinaryMedicineMovement',
'treatment',
'veterinaryMedicineLot',
'veterinaryMedicine',
'complianceAcknowledgement',
'authorityNotice',
'complianceSettings',
'colony',
'apiaryRestriction',
'apiary',
'product',
'automationSequence',
@@ -228,6 +237,118 @@ describe('Backup round trip (e2e)', () => {
]) {
remember('batchMovement', await prisma.batchMovement.create({ data }));
}
// T6: catalogue, pack, a treatment booked from it, the ledger rows, the
// authority notice, the restriction and the acknowledgement.
const colony = remember(
'colony',
await prisma.colony.create({
data: {
code: `B-${suffix}`,
apiaryId: apiary.id,
colonyType: 'PRODUCTION_COLONY',
},
}),
);
const medicine = remember(
'veterinaryMedicine',
await prisma.veterinaryMedicine.create({
data: { name: `Mittel ${suffix}`, unit: 'ML', withdrawalPeriodDays: 0 },
}),
);
const medicineLot = remember(
'veterinaryMedicineLot',
await prisma.veterinaryMedicineLot.create({
data: {
medicineId: medicine.id,
purchasedOn: day,
quantity: 100,
supplier: 'Lieferant',
userId,
},
}),
);
const treatment = remember(
'treatment',
await prisma.treatment.create({
data: {
colonyId: colony.id,
userId,
date: day,
product: `Mittel ${suffix}`,
medicineId: medicine.id,
medicineLotId: medicineLot.id,
doseQuantity: 5,
},
}),
);
remember(
'veterinaryMedicineMovement',
await prisma.veterinaryMedicineMovement.create({
data: {
lotId: medicineLot.id,
type: 'TREATMENT',
quantity: -5,
treatmentId: treatment.id,
userId,
},
}),
);
remember(
'veterinaryMedicineMovement',
await prisma.veterinaryMedicineMovement.create({
data: {
lotId: medicineLot.id,
type: 'DISPOSAL',
quantity: -2,
reason: 'Abgelaufen',
userId,
},
}),
);
remember(
'apiaryRestriction',
await prisma.apiaryRestriction.create({
data: {
apiaryId: apiary.id,
disease: 'Test',
startsOn: day,
createdByUserId: userId,
},
}),
);
remember(
'authorityNotice',
await prisma.authorityNotice.create({
data: {
kind: 'TSK_REPORT',
year: 2098,
referenceDate: day,
countedAt: day,
snapshot: { version: 1, totalLiving: 1 },
totalColonies: 1,
openKey: `TSK_REPORT:2098:${suffix}`,
createdByUserId: userId,
},
}),
);
remember(
'complianceAcknowledgement',
await prisma.complianceAcknowledgement.create({
data: {
itemKey: 'backup-test',
subjectId: suffix,
fingerprint: 'abc',
note: 'Geprüft und in Ordnung',
userId,
},
}),
);
remember(
'complianceSettings',
await prisma.complianceSettings.create({
data: { id: `backup-${suffix}`, registrationNumber: 'R-1' },
}),
);
const sequence = await prisma.automationSequence.create({
data: {
triggerType: 'RAW_MATERIAL_LOT_NUMBER_FORMAT',
@@ -250,6 +371,23 @@ describe('Backup round trip (e2e)', () => {
}, ROUND_TRIP_TIMEOUT_MS);
afterAll(async () => {
// The T6 rows reference users and colonies of this spec: remove them,
// children first, so the shared database does not collect compliance data.
for (const section of [
'veterinaryMedicineMovement',
'treatment',
'veterinaryMedicineLot',
'veterinaryMedicine',
'complianceAcknowledgement',
'authorityNotice',
'complianceSettings',
'apiaryRestriction',
] as const) {
const delegate = prisma[section] as unknown as {
deleteMany(args: { where: { id: { in: string[] } } }): Promise<unknown>;
};
await delegate.deleteMany({ where: { id: { in: [...own[section]] } } });
}
await app.close();
});
@@ -328,6 +466,35 @@ describe('Backup round trip (e2e)', () => {
calendarSyncOutboxEntry: await prisma.calendarSyncOutboxEntry.count(
where('calendarSyncOutboxEntry'),
),
veterinaryMedicine: await prisma.veterinaryMedicine.count(
where('veterinaryMedicine'),
),
veterinaryMedicineLot: await prisma.veterinaryMedicineLot.count(
where('veterinaryMedicineLot'),
),
treatmentWithPack: await prisma.treatment.count({
...where('treatment'),
where: {
id: { in: [...own.treatment] },
medicineLotId: { not: null },
},
}),
veterinaryMedicineMovement:
await prisma.veterinaryMedicineMovement.count(
where('veterinaryMedicineMovement'),
),
apiaryRestriction: await prisma.apiaryRestriction.count(
where('apiaryRestriction'),
),
authorityNotice: await prisma.authorityNotice.count(
where('authorityNotice'),
),
complianceAcknowledgement: await prisma.complianceAcknowledgement.count(
where('complianceAcknowledgement'),
),
complianceSettings: await prisma.complianceSettings.count(
where('complianceSettings'),
),
}).toEqual({
productionBatch: 1,
rawMaterialLot: 1,
@@ -337,6 +504,14 @@ describe('Backup round trip (e2e)', () => {
honeyBatchContainerAllocation: 1,
automationSequence: 1,
calendarSyncOutboxEntry: 1,
veterinaryMedicine: 1,
veterinaryMedicineLot: 1,
treatmentWithPack: 1,
veterinaryMedicineMovement: 2,
apiaryRestriction: 1,
authorityNotice: 1,
complianceAcknowledgement: 1,
complianceSettings: 1,
});
expect(ownRows(await backup.exportAll())).toEqual(exported);
},
@@ -387,6 +562,45 @@ describe('Backup round trip (e2e)', () => {
).toBeNull();
});
it('takes no T6 compliance table from an unsealed file and reports them', async () => {
const medicineId = `medicine-forged-${suffix}`;
const noticeId = `notice-forged-${suffix}`;
const summary = await backup.importFile(
{
version: 2,
exportedAt: '2026-01-01T00:00:00.000Z',
data: {
veterinaryMedicine: [
{ id: medicineId, name: 'Gefälscht', unit: 'ML' },
],
authorityNotice: [
{
id: noticeId,
kind: 'REGISTRATION',
referenceDate: '2026-01-01T00:00:00.000Z',
countedAt: '2026-01-01T00:00:00.000Z',
snapshot: {},
totalColonies: 1,
},
],
},
},
userId,
);
expect([...(summary.skipped ?? [])].sort()).toEqual([
'authorityNotice',
'veterinaryMedicine',
]);
expect(
await prisma.veterinaryMedicine.findUnique({ where: { id: medicineId } }),
).toBeNull();
expect(
await prisma.authorityNotice.findUnique({ where: { id: noticeId } }),
).toBeNull();
});
it('removes the file a restored document row replaced', async () => {
const directory = path.join(
path.resolve(process.env.DOCUMENTS_STORAGE_PATH ?? './uploads'),