import { BadRequestException } from '@nestjs/common'; import { AuditLogService } from '../audit/audit-log.service'; import { PrismaService } from '../prisma/prisma.service'; import { lockRowForUpdate } from '../prisma/row-lock'; import { ComplianceSettingsService } from './compliance-settings.service'; jest.mock('../prisma/row-lock', () => ({ lockRowForUpdate: jest.fn().mockResolvedValue(true), })); const STORED = { id: 'singleton', veterinaryOfficeName: null, veterinaryOfficeAddress: null, registrationNumber: null, noticeOnColonyCountChange: true, noticeOnApiaryChange: true, tskReferenceDay: '01-01', tskReportDueDay: null, tskMemberNumber: null, tskExcludedColonyTypes: [], netQuantityDigitRules: [ { maxGrams: 50, minMm: 2 }, { maxGrams: null, minMm: 6 }, ], updatedAt: new Date(0), }; describe('ComplianceSettingsService', () => { let service: ComplianceSettingsService; let tx: { complianceSettings: { upsert: jest.Mock; findUniqueOrThrow: jest.Mock; update: jest.Mock; }; }; let prisma: { $transaction: jest.Mock; complianceSettings: { findUnique: jest.Mock; upsert: jest.Mock }; }; let auditLog: { record: jest.Mock }; beforeEach(() => { (lockRowForUpdate as jest.Mock).mockResolvedValue(true); tx = { complianceSettings: { upsert: jest.fn().mockResolvedValue(STORED), findUniqueOrThrow: jest.fn().mockResolvedValue(STORED), update: jest .fn() .mockImplementation(({ data }: { data: object }) => Promise.resolve({ ...STORED, ...data }), ), }, }; prisma = { $transaction: jest .fn() .mockImplementation((fn: (client: unknown) => unknown) => fn(tx)), complianceSettings: { findUnique: jest.fn().mockResolvedValue(null), upsert: jest.fn().mockResolvedValue(STORED), }, }; auditLog = { record: jest.fn() }; service = new ComplianceSettingsService( prisma as unknown as PrismaService, auditLog as unknown as AuditLogService, ); }); it('creates defaults on first read', async () => { const settings = await service.get(); expect(prisma.complianceSettings.upsert).toHaveBeenCalledWith({ where: { id: 'singleton' }, update: {}, create: { id: 'singleton' }, }); expect(settings).toBe(STORED); }); it('returns the stored row without creating one', async () => { prisma.complianceSettings.findUnique.mockResolvedValue(STORED); await service.get(); expect(prisma.complianceSettings.upsert).not.toHaveBeenCalled(); }); it('rejects 02-29 as reference day with 400', async () => { await expect( service.update({ tskReferenceDay: '02-29' }, 'u1'), ).rejects.toBeInstanceOf(BadRequestException); await expect( service.update({ tskReportDueDay: '13-01' }, 'u1'), ).rejects.toBeInstanceOf(BadRequestException); }); it('rejects digit rules that are not ascending or whose last rule has a limit', async () => { const attempts = [ [ { maxGrams: 200, minMm: 3 }, { maxGrams: 50, minMm: 2 }, { maxGrams: null, minMm: 6 }, ], [ { maxGrams: 50, minMm: 2 }, { maxGrams: 50, minMm: 3 }, { maxGrams: null, minMm: 6 }, ], [{ maxGrams: 50, minMm: 2 }], [ { maxGrams: null, minMm: 2 }, { maxGrams: null, minMm: 6 }, ], [], ]; for (const rules of attempts) { await expect( service.update({ netQuantityDigitRules: rules }, 'u1'), ).rejects.toBeInstanceOf(BadRequestException); } }); it('rejects a digit height outside 0.5 to 20 mm and more than 10 rules', async () => { await expect( service.update( { netQuantityDigitRules: [{ maxGrams: null, minMm: 0.4 }] }, 'u1', ), ).rejects.toBeInstanceOf(BadRequestException); await expect( service.update( { netQuantityDigitRules: [{ maxGrams: null, minMm: 20.5 }] }, 'u1', ), ).rejects.toBeInstanceOf(BadRequestException); const eleven = Array.from({ length: 10 }, (_, index) => ({ maxGrams: (index + 1) * 10, minMm: 2, })).concat([{ maxGrams: null as never, minMm: 2 }]); await expect( service.update({ netQuantityDigitRules: eleven }, 'u1'), ).rejects.toBeInstanceOf(BadRequestException); }); it('treats a missing maxGrams key like null', async () => { await service.update( { netQuantityDigitRules: [ { maxGrams: 50, minMm: 2 }, { minMm: 6 } as never, ], }, 'u1', ); const call = ( tx.complianceSettings.update.mock.calls as unknown[][] )[0][0] as { data: { netQuantityDigitRules: unknown }; }; expect(call.data.netQuantityDigitRules).toEqual([ { maxGrams: 50, minMm: 2 }, { maxGrams: null, minMm: 6 }, ]); await expect( service.update( { netQuantityDigitRules: [{ minMm: 2 } as never, { minMm: 6 } as never], }, 'u1', ), ).rejects.toBeInstanceOf(BadRequestException); }); it('rejects a colony type listed twice', async () => { await expect( service.update( { tskExcludedColonyTypes: ['NUCLEUS_COLONY', 'NUCLEUS_COLONY'] }, 'u1', ), ).rejects.toBeInstanceOf(BadRequestException); }); it('stores valid values under the row lock with an audit entry', async () => { await service.update( { veterinaryOfficeName: ' Veterinäramt ', tskReferenceDay: '04-15', tskReportDueDay: null, netQuantityDigitRules: [{ maxGrams: null, minMm: 3 }], }, 'u1', ); expect(lockRowForUpdate).toHaveBeenCalledWith( tx, 'ComplianceSettings', 'singleton', ); expect(tx.complianceSettings.update).toHaveBeenCalledWith({ where: { id: 'singleton' }, data: expect.objectContaining({ veterinaryOfficeName: 'Veterinäramt', tskReferenceDay: '04-15', tskReportDueDay: null, netQuantityDigitRules: [{ maxGrams: null, minMm: 3 }], }) as unknown, }); expect(auditLog.record).toHaveBeenCalledWith( expect.objectContaining({ entityType: 'ComplianceSettings', entityId: 'singleton', action: 'UPDATE', userId: 'u1', }), tx, ); }); it('an omitted field keeps the stored value', async () => { await service.update({ noticeOnApiaryChange: false }, 'u1'); const call = ( tx.complianceSettings.update.mock.calls as unknown[][] )[0][0] as { data: Record; }; expect(call.data.noticeOnApiaryChange).toBe(false); expect(call.data.tskReferenceDay).toBeUndefined(); expect(call.data.veterinaryOfficeName).toBeUndefined(); }); });