import { BadRequestException } from '@nestjs/common'; import { ContractTransitionService } from './contract-transition.service'; /** * The staff signature seals with the ONE global stamp by REFERENCE: the * signature row stores the current global stampFileId instead of re-uploading * a copy per contract. That id stays valid after the stamp is replaced * (StampSettingsService never deletes retired stamp files), so each contract * keeps the exact seal it was signed with. These specs pin the sourcing * split: EDR always seals with the global stamp and staff never supply one, * while the customer must upload their own. */ describe('applySignature stamp sourcing', () => { const contract = { id: 'c-1', reference: 'CTR-1', status: 'SIGNED_CUSTOMER' }; const GLOBAL_STAMP = 'data:image/png;base64,RURS'; const GLOBAL_STAMP_FILE_ID = 'file-global-stamp'; const build = (globalStampFileId: string | null = GLOBAL_STAMP_FILE_ID) => { const uploads: Array<{ code: string; image: string }> = []; const saved: unknown[] = []; const service = Object.create( ContractTransitionService.prototype, ) as ContractTransitionService; Object.assign(service, { logger: { warn: jest.fn(), log: jest.fn() }, stampSettings: { get: jest.fn().mockResolvedValue({ id: 's-1', stampFileId: globalStampFileId, }), }, contractsRepository: { saveSignature: jest.fn((row: unknown) => { saved.push(row); return Promise.resolve(undefined); }), }, signaturesService: { getForUser: jest.fn().mockResolvedValue(null), upsertForUser: jest.fn().mockResolvedValue(undefined), }, uploadSignatureAsset: jest.fn((_c: unknown, code: string, image: string) => { uploads.push({ code, image }); return Promise.resolve({ id: `file-${code}` }); }), }); return { service, uploads, saved }; }; const apply = ( service: ContractTransitionService, dto: Record, ) => ( service as unknown as { applySignature( c: unknown, d: unknown, o: { signerUserId?: string }, ): Promise; } ).applySignature(contract, dto, { signerUserId: 'u-1' }); const staffDto = { role: 'STAFF' as const, signerDisplayName: 'E. Staff', signatureImageBase64: 'data:image/png;base64,U0lH', }; it('seals the EDR side by referencing the global stamp file, without re-uploading it', async () => { const { service, uploads, saved } = build(); await apply(service, staffDto); expect(uploads.map((u) => u.code)).toEqual(['signature_staff']); expect(saved[0]).toEqual( expect.objectContaining({ stampFileId: GLOBAL_STAMP_FILE_ID }), ); }); it('ignores a stamp a staff client tries to supply', async () => { const { service, uploads, saved } = build(); await apply(service, { ...staffDto, stampImageBase64: 'data:image/png;base64,SEFDSw==', }); expect(uploads.map((u) => u.image)).not.toContain( 'data:image/png;base64,SEFDSw==', ); expect(saved[0]).toEqual( expect.objectContaining({ stampFileId: GLOBAL_STAMP_FILE_ID }), ); }); /** * Failing loudly matters here: silently executing an unsealed contract * would be worse than refusing to counter-sign. */ it('refuses to counter-sign when no global stamp is configured', async () => { const { service, saved } = build(null); await expect(apply(service, staffDto)).rejects.toBeInstanceOf( BadRequestException, ); await expect(apply(service, staffDto)).rejects.toThrow(/company stamp is configured/i); expect(saved).toHaveLength(0); }); it('requires the customer to upload their own stamp', async () => { const { service, saved } = build(); await expect( apply(service, { role: 'CUSTOMER', signerDisplayName: 'C. Customer', signatureImageBase64: 'data:image/png;base64,U0lH', }), ).rejects.toThrow(/company stamp is required/i); expect(saved).toHaveLength(0); }); it('snapshots the customer stamp and never substitutes the global one', async () => { const { service, uploads } = build(); const customerStamp = 'data:image/png;base64,Q1VTVA=='; await apply(service, { role: 'CUSTOMER', signerDisplayName: 'C. Customer', signatureImageBase64: 'data:image/png;base64,U0lH', stampImageBase64: customerStamp, }); expect(uploads).toContainEqual({ code: 'stamp_customer', image: customerStamp, }); expect(uploads.map((u) => u.image)).not.toContain(GLOBAL_STAMP); }); /** * DIRECTOR/CEO rows are internal approval signatures, not party seals, so * they are deliberately exempt from the stamp requirement. */ it('lets internal approval signatures through without any stamp', async () => { const { service, uploads, saved } = build(); await apply(service, { role: 'DIRECTOR', signerDisplayName: 'D. Director', signatureImageBase64: 'data:image/png;base64,U0lH', }); expect(uploads.map((u) => u.code)).toEqual(['signature_director']); expect(saved[0]).toEqual(expect.objectContaining({ stampFileId: null })); }); });