import { randomUUID } from 'node:crypto'; import { INestApplication } from '@nestjs/common'; import { NestExpressApplication } from '@nestjs/platform-express'; import { Test } from '@nestjs/testing'; import * as bcrypt from 'bcryptjs'; import request from 'supertest'; import { App } from 'supertest/types'; import { addDays, berlinToday, dayToDate } from '../src/common/dates'; import { buildTreatmentLog, treatmentLogGaps, } from '../src/export/treatment-log'; import type { ReadinessItem, ReadinessReport, ReadinessRule, } from '../src/compliance/readiness/readiness.types'; import { READINESS_RULES, READINESS_RULES_TOKEN, } from '../src/compliance/readiness/rules'; import { AppModule } from '../src/app.module'; import { configureApp } from '../src/app.config'; import { THROTTLER_NAMESPACE } from '../src/common/throttler/prisma-throttler.storage'; import { PrismaService } from '../src/prisma/prisma.service'; import { login } from './utils/api'; import { pdfText } from './utils/pdf-text'; const READINESS = '/api/v1/compliance/readiness'; const ACKNOWLEDGEMENTS = '/api/v1/compliance/acknowledgements'; const SOFT_KEY = 'e2e-soft-check'; interface SiteFields { holderName: string | null; holderAddress: string | null; foodBusinessName: string | null; foodBusinessAddress: string | null; } describe('Compliance readiness (e2e)', () => { let app: INestApplication; let prisma: PrismaService; const suffix = Date.now(); const password = 'SuperSecret123!'; const tokens: Record = {}; let apiaryOnlyRoleId: string; let apiaryId: string; let keyCounter = 0; let softValue = 1; let originalSite: SiteFields; let originalVetOffice: string | null | undefined; // Only rows this file created are ever deleted (the database is shared). const noticeIds: string[] = []; const restrictionIds: string[] = []; const acknowledgementIds: string[] = []; // A rule that can be acknowledged: the real ones of this task cannot, the // later ones will. It gives the acknowledgement flow a real database. const softRule: ReadinessRule = { key: SOFT_KEY, area: 'ETIKETTEN', severity: 'PRUEFEN', title: 'Testpunkt', legalBasis: null, acknowledgeable: true, evaluate: () => [ { subjectId: 'global', detail: 'Bitte prüfen.', fingerprintParts: { value: softValue }, subjects: [], }, ], }; const server = () => app.getHttpServer(); const as = (name: string) => ({ Authorization: `Bearer ${tokens[name]}` }); const nextKey = () => `readiness-e2e-${suffix}-${(keyCounter += 1).toString().padStart(4, '0')}`; async function report(user = 'beekeeper'): Promise { const response = await request(server()).get(READINESS).set(as(user)); expect(response.status).toBe(200); return response.body as ReadinessReport; } function itemOf( body: ReadinessReport, key: string, subjectId = 'global', ): ReadinessItem { const found = body.items.find( (item) => item.key === key && item.subjectId === subjectId, ); if (!found) { throw new Error(`No readiness item ${key}/${subjectId}`); } return found; } function acknowledge(body: Record, user = 'beekeeper') { return request(server()) .post(ACKNOWLEDGEMENTS) .set(as(user)) .set('Idempotency-Key', nextKey()) .send(body); } async function createUser( name: string, roleWhere: { key: string } | { id: string }, ) { const email = `readiness-e2e-${name}-${suffix}@example.com`; await prisma.user.create({ data: { email, passwordHash: await bcrypt.hash(password, 12), name, role: { connect: roleWhere }, }, }); tokens[name] = (await login(app, email, password)).accessToken; } // Settings rows are saved in beforeAll and put back in afterAll, never // deleted: other suites rely on the seeded rows. async function setSite(data: SiteFields) { await prisma.siteSettings.upsert({ where: { id: 'singleton' }, update: data, create: { id: 'singleton', ...data }, }); } async function setVetOffice(name: string | null) { await prisma.complianceSettings.upsert({ where: { id: 'singleton' }, update: { veterinaryOfficeName: name }, create: { id: 'singleton', veterinaryOfficeName: name }, }); } async function removeOwnRows() { await prisma.complianceAcknowledgement.deleteMany({ where: { id: { in: acknowledgementIds } }, }); await prisma.authorityNotice.deleteMany({ where: { id: { in: noticeIds } }, }); await prisma.apiaryRestriction.deleteMany({ where: { id: { in: restrictionIds } }, }); noticeIds.length = 0; restrictionIds.length = 0; acknowledgementIds.length = 0; } beforeAll(async () => { // The module gets the soft test rule through its token; the shared list // stays untouched. const moduleFixture = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(THROTTLER_NAMESPACE) .useValue(randomUUID()) .overrideProvider(READINESS_RULES_TOKEN) .useValue([...READINESS_RULES, softRule]) .compile(); const nest = moduleFixture.createNestApplication(); configureApp(nest); await nest.init(); app = nest; prisma = app.get(PrismaService); originalVetOffice = ( await prisma.complianceSettings.findUnique({ where: { id: 'singleton' } }) )?.veterinaryOfficeName; const site = await prisma.siteSettings.findUnique({ where: { id: 'singleton' }, }); originalSite = { holderName: site?.holderName ?? null, holderAddress: site?.holderAddress ?? null, foodBusinessName: site?.foodBusinessName ?? null, foodBusinessAddress: site?.foodBusinessAddress ?? null, }; await createUser('beekeeper', { key: 'BEEKEEPER' }); await createUser('staff', { key: 'STAFF' }); await createUser('readonly', { key: 'READONLY' }); await createUser('apiclient', { key: 'API_CLIENT' }); // compliance:read only, without apiaries:read. const role = await prisma.role.create({ data: { key: `READINESS_E2E_${suffix}`, name: 'Nur Prüfbereitschaft' }, }); apiaryOnlyRoleId = role.id; const permission = await prisma.permission.findFirstOrThrow({ where: { key: 'compliance:read' }, }); await prisma.rolePermission.create({ data: { roleId: role.id, permissionId: permission.id }, }); await createUser('complianceonly', { id: role.id }); const apiary = await prisma.apiary.create({ data: { name: `Readiness Stand ${suffix}` }, }); apiaryId = apiary.id; }); beforeEach(async () => { await removeOwnRows(); softValue = 1; }); afterAll(async () => { await removeOwnRows(); await setSite(originalSite); if (originalVetOffice !== undefined) { await setVetOffice(originalVetOffice); } await prisma.apiary.deleteMany({ where: { id: apiaryId } }); const mine = { email: { startsWith: 'readiness-e2e-', endsWith: `-${suffix}@example.com`, }, }; const userIds = ( await prisma.user.findMany({ where: mine, select: { id: true } }) ).map((user) => user.id); await prisma.refreshToken.deleteMany({ where: { userId: { in: userIds } }, }); await prisma.user.deleteMany({ where: { id: { in: userIds } } }); await prisma.rolePermission.deleteMany({ where: { roleId: apiaryOnlyRoleId }, }); await prisma.role.deleteMany({ where: { id: apiaryOnlyRoleId } }); await app.close(); }); it('without site data and without a SENT registration the Stammdaten and registration items are open and the status is OPEN', async () => { await setSite({ holderName: null, holderAddress: null, foodBusinessName: null, foodBusinessAddress: null, }); await setVetOffice(null); // Rows of other suites stay: the registration follows what exists. const sentRegistrations = await prisma.authorityNotice.count({ where: { kind: 'REGISTRATION', status: 'SENT' }, }); const body = await report(); for (const key of ['holder-data', 'food-business-data']) { expect(itemOf(body, key)).toMatchObject({ state: 'OPEN' }); } expect(itemOf(body, 'registration').state).toBe( sentRegistrations === 0 ? 'OPEN' : 'DONE', ); expect(itemOf(body, 'holder-data').subjects[0].missing).toEqual([ 'Name', 'Anschrift', ]); expect(itemOf(body, 'vet-office').state).toBe('OPEN'); expect(body.status).toBe('OPEN'); expect(body.openCount).toBeGreaterThanOrEqual(3); expect(Number.isNaN(Date.parse(body.generatedAt))).toBe(false); const summary = await request(server()) .get(`${READINESS}/summary`) .set(as('beekeeper')); expect(summary.status).toBe(200); expect(summary.body).toMatchObject({ status: 'OPEN' }); expect((summary.body as { firstItemKey: string }).firstItemKey).toEqual( expect.any(String), ); }); it('complete master data and a SENT registration make those items DONE', async () => { await setSite({ holderName: 'Imker E2E', holderAddress: 'Weg 1, 12345 Ort', foodBusinessName: 'Imkerei E2E', foodBusinessAddress: 'Weg 2, 12345 Ort', }); await setVetOffice('Amt E2E'); const sent = await prisma.authorityNotice.create({ data: { kind: 'REGISTRATION', status: 'SENT', referenceDate: dayToDate('2026-01-01'), countedAt: new Date('2026-01-01T10:00:00.000Z'), snapshot: { version: 1, takenAt: 'x', apiaries: [], totalLiving: 0 }, totalColonies: 0, sentOn: dayToDate('2026-01-02'), }, }); noticeIds.push(sent.id); const body = await report(); for (const key of [ 'holder-data', 'food-business-data', 'vet-office', 'registration', ]) { expect(itemOf(body, key)).toMatchObject({ state: 'DONE', subjects: [], more: 0, }); } }); it('READONLY may read the report but gets 403 acknowledging; STAFF gets 403 on acknowledgements; API_CLIENT gets 403 everywhere', async () => { const payload = { itemKey: SOFT_KEY, subjectId: 'global', fingerprint: 'a'.repeat(64), note: 'Geprüft und in Ordnung.', }; expect((await report('readonly')).items.length).toBeGreaterThan(0); expect((await report('staff')).items.length).toBeGreaterThan(0); expect((await acknowledge(payload, 'readonly')).status).toBe(403); expect((await acknowledge(payload, 'staff')).status).toBe(403); expect( ( await request(server()) .post(`${ACKNOWLEDGEMENTS}/someid/revoke`) .set(as('staff')) ).status, ).toBe(403); for (const path of [READINESS, `${READINESS}/summary`]) { expect( (await request(server()).get(path).set(as('apiclient'))).status, ).toBe(403); } expect((await acknowledge(payload, 'apiclient')).status).toBe(403); expect((await request(server()).get(READINESS)).status).toBe(401); }); it('shows the apiaries of an active restriction only with apiaries:read, but counts them for everyone', async () => { const created = await prisma.apiaryRestriction.create({ data: { apiaryId, disease: 'Faulbrut', authority: 'Amt Geheim', startsOn: dayToDate(berlinToday()), }, }); restrictionIds.push(created.id); const full = itemOf(await report('beekeeper'), 'restriction-active'); const masked = itemOf(await report('complianceonly'), 'restriction-active'); expect(full.state).toBe('OPEN'); expect(full.subjects.map((subject) => subject.id)).toContain(apiaryId); expect(masked.state).toBe('OPEN'); expect(masked.subjects).toEqual([]); expect(masked.more).toBeGreaterThanOrEqual(1); expect(JSON.stringify(masked)).not.toContain('Readiness Stand'); expect(JSON.stringify(masked)).not.toContain('Amt Geheim'); expect(JSON.stringify(full)).not.toContain('Amt Geheim'); }); it('an ended restriction does not keep the item open', async () => { const ended = await prisma.apiaryRestriction.create({ data: { apiaryId, disease: 'Faulbrut', startsOn: dayToDate('2020-01-01'), endsOn: dayToDate('2020-02-01'), }, }); restrictionIds.push(ended.id); const body = await report(); expect( body.items .find((item) => item.key === 'restriction-active') ?.subjects.map((subject) => subject.id) ?? [], ).not.toContain(apiaryId); }); describe('acknowledgements', () => { it('refuses a PFLICHT item, an unknown item and a short note with 400', async () => { const body = await report(); const holder = itemOf(body, 'holder-data'); const soft = itemOf(body, SOFT_KEY); const pflicht = await acknowledge({ itemKey: 'holder-data', subjectId: 'global', fingerprint: holder.fingerprint, note: 'Geprüft und in Ordnung.', }); const unknown = await acknowledge({ itemKey: 'no-such-item', subjectId: 'global', fingerprint: soft.fingerprint, note: 'Geprüft und in Ordnung.', }); const shortNote = await acknowledge({ itemKey: SOFT_KEY, subjectId: 'global', fingerprint: soft.fingerprint, note: 'kurz', }); expect(pflicht.status).toBe(400); expect(unknown.status).toBe(400); expect(shortNote.status).toBe(400); }); it('acknowledges with the current fingerprint, reopens on changed data, and a revoke reopens it', async () => { const before = itemOf(await report(), SOFT_KEY); const stale = await acknowledge({ itemKey: SOFT_KEY, subjectId: 'global', fingerprint: 'b'.repeat(64), note: 'Geprüft und in Ordnung.', }); const created = await acknowledge({ itemKey: SOFT_KEY, subjectId: 'global', fingerprint: before.fingerprint, note: 'Geprüft und in Ordnung.', }); expect(stale.status).toBe(409); expect(stale.body).toMatchObject({ code: 'READINESS_FINGERPRINT_STALE', message: 'Der Punkt hat sich geändert. Bitte neu laden.', }); expect(created.status).toBe(201); const createdItem = created.body as ReadinessItem; expect(createdItem.state).toBe('ACKNOWLEDGED'); const acknowledgementId = createdItem.acknowledgement!.id; acknowledgementIds.push(acknowledgementId); expect(itemOf(await report(), SOFT_KEY)).toMatchObject({ state: 'ACKNOWLEDGED', acknowledgement: { id: acknowledgementId }, }); expect( await prisma.auditLog.count({ where: { entityType: 'ComplianceAcknowledgement', entityId: acknowledgementId, action: 'CREATE', }, }), ).toBe(1); softValue = 2; expect(itemOf(await report(), SOFT_KEY).state).toBe('OPEN'); softValue = 1; expect(itemOf(await report(), SOFT_KEY).state).toBe('ACKNOWLEDGED'); const revoke = () => request(server()) .post(`${ACKNOWLEDGEMENTS}/${acknowledgementId}/revoke`) .set(as('beekeeper')); expect((await revoke()).status).toBe(201); expect(itemOf(await report(), SOFT_KEY).state).toBe('OPEN'); const again = await revoke(); expect(again.status).toBe(409); expect(again.body).toMatchObject({ code: 'READINESS_ACK_REVOKED', message: 'Die Quittung ist schon zurückgenommen.', }); const row = await prisma.complianceAcknowledgement.findUniqueOrThrow({ where: { id: acknowledgementId }, }); expect(row.revokedAt).not.toBeNull(); expect(row.revokedByUserId).not.toBeNull(); }); it('acknowledging a subject that is not open answers 409 READINESS_ITEM_NOT_OPEN', async () => { const response = await acknowledge({ itemKey: SOFT_KEY, subjectId: 'no-such-subject', fingerprint: 'a'.repeat(64), note: 'Geprüft und in Ordnung.', }); expect(response.status).toBe(409); expect(response.body).toMatchObject({ code: 'READINESS_ITEM_NOT_OPEN', message: 'Der Punkt hat sich geändert. Bitte neu laden.', }); }); it('revoking an unknown acknowledgement is a 404', async () => { const response = await request(server()) .post(`${ACKNOWLEDGEMENTS}/does-not-exist/revoke`) .set(as('beekeeper')); expect(response.status).toBe(404); }); }); describe('medicine, label and harvest areas', () => { const area = Date.now(); const today = berlinToday(); const created = { treatments: [] as string[], lots: [] as string[], harvests: [] as string[], }; let colonyId: string; let medicineId: string; let lotOk: string; let lotSmall: string; let lotOld: string; let treatmentOk: string; let treatmentSmall: string; let treatmentOld: string; let harvestInside: string; let harvestAfter: string; let productId: string; let templateId: string; let userId: string; let extractionId: string; let honeyBatchId: string; let bottlingId: string; let medicineName: string; const textElement = (id: string, y: number, key: string, size = 9) => ({ id, type: 'text', x: 2, y, width: 80, height: 8, text: '', placeholderKey: key, fontSize: size, fontFamily: 'sans', bold: false, italic: false, color: '#000000', align: 'left', }); const itemsOf = (body: ReadinessReport, key: string) => body.items.filter((item) => item.key === key); const openSubjectIds = (body: ReadinessReport, key: string) => itemsOf(body, key) .filter((item) => item.state === 'OPEN') .flatMap((item) => [ item.subjectId, ...item.subjects.map((subject) => subject.id), ]); async function postTreatment(body: Record) { const response = await request(server()) .post('/api/v1/treatments') .set(as('beekeeper')) .send({ colonyId, date: today, ...body }); expect(response.status).toBe(201); const id = (response.body as { id: string }).id; created.treatments.push(id); return id; } async function makeLot(patch: Record) { const lot = await prisma.veterinaryMedicineLot.create({ data: { medicineId, purchasedOn: dayToDate('2026-01-10'), quantity: 100, supplier: 'Apotheke E2E', purchaseProof: 'Beleg E2E', expiresOn: dayToDate('2099-01-01'), ...patch, }, }); created.lots.push(lot.id); return lot.id; } beforeAll(async () => { const owner = await prisma.user.findFirstOrThrow({ where: { email: `readiness-e2e-beekeeper-${suffix}@example.com` }, }); userId = owner.id; colonyId = ( await prisma.colony.create({ data: { code: `RA-${area}`, apiaryId, colonyType: 'PRODUCTION_COLONY', }, }) ).id; medicineName = `Readiness-Mittel ${area}`; medicineId = ( await prisma.veterinaryMedicine.create({ data: { name: medicineName, unit: 'ML', withdrawalPeriodDays: 0 }, }) ).id; lotOk = await makeLot({}); lotSmall = await makeLot({ quantity: 10 }); lotOld = await makeLot({ quantity: 5, expiresOn: dayToDate('2020-01-01'), }); treatmentOk = await postTreatment({ medicineLotId: lotOk, doseQuantity: 30, withdrawalPeriodDays: 0, }); treatmentSmall = await postTreatment({ medicineLotId: lotSmall, doseQuantity: 30, withdrawalPeriodDays: 0, }); treatmentOld = await postTreatment({ medicineLotId: lotOld, doseQuantity: 1, withdrawalPeriodDays: 0, }); await postTreatment({ product: `Wartezeit-Mittel ${area}`, amount: '10 ml', supplier: 'Apotheke E2E', purchaseProof: 'Beleg E2E', withdrawalPeriodDays: 10, }); harvestInside = ( await prisma.harvest.create({ data: { apiaryId, date: dayToDate(today), userId }, }) ).id; harvestAfter = ( await prisma.harvest.create({ data: { apiaryId, date: dayToDate(addDays(today, 20)), userId }, }) ).id; created.harvests.push(harvestInside, harvestAfter); for (const harvestId of created.harvests) { await prisma.harvestColony.create({ data: { harvestId, colonyId } }); } // A complete label: its own bottling (far in the future, so it is the // newest one with stock) and a product with a legal name. productId = ( await prisma.product.create({ data: { name: `Readiness-Glas ${area}`, legalName: 'Blütenhonig' }, }) ).id; extractionId = ( await prisma.extraction.create({ data: { date: new Date(), userId } }) ).id; honeyBatchId = ( await prisma.honeyBatch.create({ data: { batchNumber: `RA-${area}`, extractionId, amount: 25, initialAmount: 25, }, }) ).id; bottlingId = ( await prisma.bottlingBatch.create({ data: { honeyBatchId, productId, date: new Date('2099-06-01T00:00:00.000Z'), jarSize: 500, jarCount: 2, totalAmount: 1, bestBeforeDate: new Date('2101-06-01T00:00:00.000Z'), lotNumber: `RA-L-${area}`, userId, }, }) ).id; templateId = ( await prisma.labelTemplate.create({ data: { name: `Readiness-Vorlage ${area}`, elements: [ textElement('legal', 1, 'legalName'), textElement('net', 10, 'jarSize', 16), textElement('mhd', 20, 'bestBeforeLine'), textElement('lot', 29, 'lotLine'), textElement('origin', 38, 'originCountry'), textElement('firm', 47, 'foodBusinessName'), textElement('address', 56, 'foodBusinessAddress'), ], widthMm: 90, heightMm: 70, }, }) ).id; await setSite({ holderName: 'Imker E2E', holderAddress: 'Weg 1, 12345 Ort', foodBusinessName: 'Imkerei E2E', foodBusinessAddress: 'Weg 2, 12345 Ort', }); await prisma.complianceSettings.upsert({ where: { id: 'singleton' }, update: {}, create: { id: 'singleton' }, }); }); afterAll(async () => { await prisma.veterinaryMedicineMovement.deleteMany({ where: { lotId: { in: created.lots } }, }); await prisma.domainEvent.deleteMany({ where: { aggregateType: 'Treatment', aggregateId: { in: created.treatments }, }, }); await prisma.treatment.deleteMany({ where: { id: { in: created.treatments } }, }); await prisma.veterinaryMedicineLot.deleteMany({ where: { id: { in: created.lots } }, }); await prisma.veterinaryMedicine.deleteMany({ where: { id: medicineId }, }); await prisma.harvestColony.deleteMany({ where: { harvestId: { in: created.harvests } }, }); await prisma.harvest.deleteMany({ where: { id: { in: created.harvests } }, }); await prisma.bottlingBatch.deleteMany({ where: { id: bottlingId } }); await prisma.honeyBatch.deleteMany({ where: { id: honeyBatchId } }); await prisma.extraction.deleteMany({ where: { id: extractionId } }); await prisma.labelTemplate.deleteMany({ where: { id: templateId } }); await prisma.product.deleteMany({ where: { id: productId } }); await prisma.colony.deleteMany({ where: { id: colonyId } }); }); it('complete data in every area leaves no open item for the subjects of this test and no open item of the Stammdaten rules', async () => { await setVetOffice('Amt E2E'); const body = await report(); const keys = [ 'treatment-log-gaps', 'treatment-location-uncertain', 'lot-gaps', 'lot-prescription', 'lot-expired-stock', 'treatment-expired-lot', 'label-template-gaps', 'label-template-form', 'medicine-withdrawal-missing', ]; const ownIds = [ treatmentOk, treatmentSmall, lotOk, lotSmall, medicineId, templateId, harvestAfter, ]; for (const key of ['holder-data', 'food-business-data', 'vet-office']) { expect(itemOf(body, key).state).toBe('DONE'); } for (const key of keys) { const open = openSubjectIds(body, key); for (const id of ownIds) { expect({ key, id, open: open.includes(id) }).toEqual({ key, id, open: false, }); } } }); it('a treatment from a complete lot gives a Bestandsbuch row without FEHLT and no treatment-log-gaps item', async () => { const year = Number(today.slice(0, 4)); const log = await buildTreatmentLog(prisma, year, apiaryId); const rows = log.sheets .flatMap((sheet) => sheet.rows) .filter((row) => row.lotId === lotOk); expect(rows).toHaveLength(1); expect(treatmentLogGaps(rows[0])).toEqual([]); const open = openSubjectIds(await report(), 'treatment-log-gaps'); expect(open).not.toContain(treatmentOk); expect(open).not.toContain(lotOk); }); it('the gap names of an incomplete treatment equal the gaps the Bestandsbuch row reports', async () => { // Dated in the first year of the lookback: the item lists at most 50 // subjects and the shared database holds other treatments. const year = Number(today.slice(0, 4)) - 5; const id = await postTreatment({ date: `${year}-01-01`, product: `Luecken-Mittel ${area}`, withdrawalPeriodDays: 0, }); const item = itemOf(await report(), 'treatment-log-gaps', String(year)); const subject = item.subjects.find((entry) => entry.id === id); const log = await buildTreatmentLog(prisma, year, apiaryId); const row = log.sheets .flatMap((sheet) => sheet.rows) .find((entry) => entry.product === `Luecken-Mittel ${area}`)!; const names = { SUPPLIER: 'Lieferant', PROOF: 'Beleg', AMOUNT: 'Menge', WITHDRAWAL: 'Wartezeit', VET: 'Tierarzt', }; expect(item.state).toBe('OPEN'); expect(subject).toMatchObject({ type: 'TREATMENT', parentId: colonyId }); expect(subject!.missing).toEqual( treatmentLogGaps(row).map((code) => names[code]), ); expect(JSON.stringify(item)).not.toContain('Luecken-Mittel'); }); it('a negative rest after a treatment from a too small lot opens lot-negative', async () => { const body = await report(); expect(itemOf(body, 'lot-negative').state).toBe('OPEN'); expect(openSubjectIds(body, 'lot-negative')).toContain(lotSmall); expect(openSubjectIds(body, 'lot-negative')).not.toContain(lotOk); }); it('lot-expired-stock is acknowledgeable per lot, a booking that changes the rest reopens it', async () => { const before = itemOf(await report(), 'lot-expired-stock', lotOld); expect(before).toMatchObject({ state: 'OPEN', acknowledgeable: true }); expect(openSubjectIds(await report(), 'treatment-expired-lot')).toContain( treatmentOld, ); const response = await acknowledge({ itemKey: 'lot-expired-stock', subjectId: lotOld, fingerprint: before.fingerprint, note: 'Wird bei der nächsten Abgabe entsorgt.', }); expect(response.status).toBe(201); acknowledgementIds.push( (response.body as ReadinessItem).acknowledgement!.id, ); expect(itemOf(await report(), 'lot-expired-stock', lotOld).state).toBe( 'ACKNOWLEDGED', ); const disposal = await request(server()) .post(`/api/v1/veterinary-medicine-lots/${lotOld}/movements`) .set(as('beekeeper')) .send({ type: 'DISPOSAL', quantity: 1, reason: 'Teil entsorgt' }); expect(disposal.status).toBe(201); expect(itemOf(await report(), 'lot-expired-stock', lotOld).state).toBe( 'OPEN', ); }); it('hides the lot subjects without veterinary-medicines:read but keeps the count', async () => { const masked = itemOf(await report('complianceonly'), 'lot-negative'); const full = itemOf(await report('beekeeper'), 'lot-negative'); expect(masked.state).toBe('OPEN'); expect(masked.subjects).toEqual([]); expect(masked.more).toBeGreaterThanOrEqual(1); expect(JSON.stringify(masked)).not.toContain(medicineName); expect(full.subjects.map((subject) => subject.id)).toContain(lotSmall); }); it('opens harvest-withdrawal only for the harvest inside the window and acknowledges it', async () => { const body = await report(); const open = openSubjectIds(body, 'harvest-withdrawal'); expect(open).toContain(harvestInside); expect(open).not.toContain(harvestAfter); const item = itemOf(body, 'harvest-withdrawal', harvestInside); expect(JSON.stringify(item)).not.toContain('Wartezeit-Mittel'); const response = await acknowledge({ itemKey: 'harvest-withdrawal', subjectId: harvestInside, fingerprint: item.fingerprint, note: 'Honig bleibt bis zum Ende der Wartezeit zurück.', }); expect(response.status).toBe(201); acknowledgementIds.push( (response.body as ReadinessItem).acknowledgement!.id, ); }); it('the label rules judge a template against the newest bottling with stock', async () => { expect( openSubjectIds(await report(), 'label-template-gaps'), ).not.toContain(templateId); await prisma.labelTemplate.update({ where: { id: templateId }, data: { elements: [textElement('legal', 1, 'legalName')] }, }); expect(openSubjectIds(await report(), 'label-template-gaps')).toContain( templateId, ); }); const fetchPdf = (user: string) => request(server()) .get(`${READINESS}/report.pdf`) .set(as(user)) .buffer(true) .parse((res, callback) => { const chunks: Buffer[] = []; res.on('data', (chunk: Buffer) => chunks.push(chunk)); res.on('end', () => callback(null, Buffer.concat(chunks))); }); it('GET /compliance/readiness/report.pdf answers application/pdf and no-store', async () => { const response = await fetchPdf('beekeeper'); expect(response.status).toBe(200); expect(response.headers['content-type']).toContain('application/pdf'); expect(response.headers['cache-control']).toBe('no-store'); expect(response.headers['content-disposition']).toContain( 'pruefbericht.pdf', ); const text = pdfText(response.body as Buffer); expect(text).toContain('Prüfbericht'); expect(text).not.toContain(medicineName); expect((await fetchPdf('apiclient')).status).toBe(403); }); it('the report PDF of a user with compliance:read but without veterinary-medicines:read names no medicine', async () => { const response = await fetchPdf('complianceonly'); expect(response.status).toBe(200); const text = pdfText(response.body as Buffer); expect(text).toContain('Prüfbericht'); for (const name of [ medicineName, `Wartezeit-Mittel ${area}`, `Luecken-Mittel ${area}`, ]) { expect(text).not.toContain(name); } }); }); });