Files
edr-platform/apps/edr-freight-api/src/modules/contracts/contract-staff-stamp.spec.ts

160 lines
5.2 KiB
TypeScript

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<string, unknown>,
) =>
(
service as unknown as {
applySignature(
c: unknown,
d: unknown,
o: { signerUserId?: string },
): Promise<void>;
}
).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 }));
});
});