feat: T6 wave 1 – compliance schema, permissions, lock order, backup
- 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:
1 parent
7a1480138d
commit
8a31c42578
23 files changed
+2028
-84
No files matched your search
@@ -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'),
|
||||
|
||||
Reference in new issue
Block a user