import { PrismaService } from '../src/prisma/prisma.service'; /** * Schema-level guarantees of the T6 migrations that the Prisma schema alone * cannot express (CHECK constraints, the JSON default, the nullable unique * key, RESTRICT) against a real PostgreSQL. The test database is shared and * not empty: every row here is created by this file and removed again, and * the singleton tables are exercised through rows with their own id. */ describe('T6 schema (e2e)', () => { let prisma: PrismaService; const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`; const CHECK_VIOLATION = /check constraint|violates/i; let userId: string; let apiaryId: string; let colonyId: string; let treatmentId: string; const medicineIds: string[] = []; const lotIds: string[] = []; const noticeIds: string[] = []; const restrictionIds: string[] = []; const templateIds: string[] = []; const settingsIds: string[] = []; async function createMedicine(label: string): Promise { const medicine = await prisma.veterinaryMedicine.create({ data: { name: `T6 ${label} ${suffix}`, unit: 'ML' }, }); medicineIds.push(medicine.id); return medicine.id; } async function createLot( medicineId: string, quantity = 100, ): Promise { const lot = await prisma.veterinaryMedicineLot.create({ data: { medicineId, purchasedOn: new Date('2026-08-01'), quantity }, }); lotIds.push(lot.id); return lot.id; } beforeAll(async () => { prisma = new PrismaService(); const role = await prisma.role.findUniqueOrThrow({ where: { key: 'READONLY' }, }); const user = await prisma.user.create({ data: { email: `t6-schema-${suffix}@example.com`, passwordHash: 'x', name: 'T6 schema', roleId: role.id, }, }); userId = user.id; const apiary = await prisma.apiary.create({ data: { name: `T6 Stand ${suffix}` }, }); apiaryId = apiary.id; const colony = await prisma.colony.create({ data: { code: `T6-${suffix}`, apiaryId, colonyType: 'PRODUCTION_COLONY' }, }); colonyId = colony.id; const treatment = await prisma.treatment.create({ data: { colonyId, userId, date: new Date('2026-08-02'), product: 'T6' }, }); treatmentId = treatment.id; }); afterAll(async () => { await prisma.veterinaryMedicineMovement.deleteMany({ where: { lotId: { in: lotIds } }, }); await prisma.treatment.deleteMany({ where: { id: treatmentId } }); await prisma.veterinaryMedicineLot.deleteMany({ where: { id: { in: lotIds } }, }); await prisma.veterinaryMedicine.deleteMany({ where: { id: { in: medicineIds } }, }); await prisma.apiaryRestriction.deleteMany({ where: { id: { in: restrictionIds } }, }); await prisma.authorityNotice.deleteMany({ where: { id: { in: noticeIds } }, }); await prisma.labelTemplate.deleteMany({ where: { id: { in: templateIds } }, }); await prisma.complianceSettings.deleteMany({ where: { id: { in: settingsIds } }, }); await prisma.colony.deleteMany({ where: { id: colonyId } }); await prisma.apiary.deleteMany({ where: { id: apiaryId } }); await prisma.user.deleteMany({ where: { id: userId } }); await prisma.$disconnect(); }); describe('veterinary medicines', () => { it('the CHECK rejects a lot with quantity 0', async () => { const medicineId = await createMedicine('lot0'); await expect(createLot(medicineId, 0)).rejects.toThrow(CHECK_VIOLATION); await expect(createLot(medicineId, -1)).rejects.toThrow(CHECK_VIOLATION); }); it('the CHECK rejects a movement with quantity 0', async () => { const lotId = await createLot(await createMedicine('mov0')); await expect( prisma.veterinaryMedicineMovement.create({ data: { lotId, type: 'CORRECTION', quantity: 0 }, }), ).rejects.toThrow(CHECK_VIOLATION); }); it('the CHECK rejects a TREATMENT movement without treatmentId and a CORRECTION movement with one', async () => { const lotId = await createLot(await createMedicine('type')); await expect( prisma.veterinaryMedicineMovement.create({ data: { lotId, type: 'TREATMENT', quantity: -5 }, }), ).rejects.toThrow(CHECK_VIOLATION); await expect( prisma.veterinaryMedicineMovement.create({ data: { lotId, type: 'CORRECTION', quantity: -5, treatmentId }, }), ).rejects.toThrow(CHECK_VIOLATION); await expect( prisma.veterinaryMedicineMovement.create({ data: { lotId, type: 'DISPOSAL', quantity: -5, treatmentId }, }), ).rejects.toThrow(CHECK_VIOLATION); }); it('accepts the valid combinations', async () => { const lotId = await createLot(await createMedicine('valid')); const booked = await prisma.veterinaryMedicineMovement.create({ data: { lotId, type: 'TREATMENT', quantity: -5, treatmentId }, }); const corrected = await prisma.veterinaryMedicineMovement.create({ data: { lotId, type: 'CORRECTION', quantity: 2.5, reason: 'Inventur' }, }); expect(booked.treatmentId).toBe(treatmentId); expect(corrected.treatmentId).toBeNull(); }); it('a medicine with lots cannot be deleted (RESTRICT)', async () => { const medicineId = await createMedicine('restrict'); await createLot(medicineId); await expect( prisma.veterinaryMedicine.delete({ where: { id: medicineId } }), ).rejects.toThrow(); expect( await prisma.veterinaryMedicine.count({ where: { id: medicineId } }), ).toBe(1); }); it('a lot with movements cannot be deleted (RESTRICT)', async () => { const lotId = await createLot(await createMedicine('restrict2')); await prisma.veterinaryMedicineMovement.create({ data: { lotId, type: 'CORRECTION', quantity: 1 }, }); await expect( prisma.veterinaryMedicineLot.delete({ where: { id: lotId } }), ).rejects.toThrow(); }); }); describe('label particulars', () => { it('rejects minXHeightMm below 0.5 and above 5', async () => { const create = async (minXHeightMm: number) => { const template = await prisma.labelTemplate.create({ data: { name: `T6 ${suffix}`, minXHeightMm }, }); templateIds.push(template.id); return template; }; await expect(create(0.49)).rejects.toThrow(CHECK_VIOLATION); await expect(create(5.01)).rejects.toThrow(CHECK_VIOLATION); await expect(create(0.5)).resolves.toMatchObject({ minXHeightMm: 0.5 }); await expect(create(5)).resolves.toMatchObject({ minXHeightMm: 5 }); }); it('defaults minXHeightMm to 1.2', async () => { const template = await prisma.labelTemplate.create({ data: { name: `T6 default ${suffix}` }, }); templateIds.push(template.id); expect(template.minXHeightMm).toBe(1.2); }); }); describe('authority compliance', () => { it('rejects an ApiaryRestriction that ends before it starts', async () => { const create = async (endsOn: Date | null) => { const restriction = await prisma.apiaryRestriction.create({ data: { apiaryId, disease: 'T6', startsOn: new Date('2026-09-10'), endsOn, }, }); restrictionIds.push(restriction.id); return restriction; }; await expect(create(new Date('2026-09-09'))).rejects.toThrow( CHECK_VIOLATION, ); await expect(create(new Date('2026-09-10'))).resolves.toBeDefined(); await expect(create(null)).resolves.toBeDefined(); }); it('an apiary with restrictions cannot be deleted (RESTRICT)', async () => { const lonely = await prisma.apiary.create({ data: { name: `T6 Sperrstand ${suffix}` }, }); const restriction = await prisma.apiaryRestriction.create({ data: { apiaryId: lonely.id, disease: 'T6', startsOn: new Date('2026-09-10'), }, }); restrictionIds.push(restriction.id); await expect( prisma.apiary.delete({ where: { id: lonely.id } }), ).rejects.toThrow(); await prisma.apiaryRestriction.delete({ where: { id: restriction.id } }); await prisma.apiary.delete({ where: { id: lonely.id } }); }); it('ComplianceSettings default netQuantityDigitRules equals the four rules of the spec', async () => { const settings = await prisma.complianceSettings.create({ data: { id: `t6-${suffix}` }, }); settingsIds.push(settings.id); expect(settings.netQuantityDigitRules).toEqual([ { maxGrams: 50, minMm: 2 }, { maxGrams: 200, minMm: 3 }, { maxGrams: 1000, minMm: 4 }, { maxGrams: null, minMm: 6 }, ]); expect(settings).toMatchObject({ noticeOnColonyCountChange: true, noticeOnApiaryChange: true, tskReferenceDay: '01-01', tskReportDueDay: null, tskExcludedColonyTypes: [], }); }); it('openKey is unique, several rows with openKey NULL are allowed', async () => { const notice = (openKey: string | null) => prisma.authorityNotice.create({ data: { kind: 'TSK_REPORT', year: 2099, referenceDate: new Date('2099-01-01'), countedAt: new Date('2099-01-01'), snapshot: { version: 1 }, totalColonies: 3, openKey, }, }); const key = `TSK_REPORT:2099:${suffix}`; const first = await notice(key); noticeIds.push(first.id); await expect(notice(key)).rejects.toThrow(); const sentA = await notice(null); const sentB = await notice(null); noticeIds.push(sentA.id, sentB.id); expect( await prisma.authorityNotice.count({ where: { id: { in: [first.id, sentA.id, sentB.id] } }, }), ).toBe(3); }); }); });