mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 16:35:42 +00:00
153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
import { BadRequestException } from '@nestjs/common';
|
|
|
|
import { ContractTransitionService } from './contract-transition.service';
|
|
|
|
/**
|
|
* Where the booking-contract view reads the global stamp live, the contracts
|
|
* path SNAPSHOTS it onto the signature row at signing time, so replacing the
|
|
* company stamp can never restamp an already-executed contract. 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 build = (globalStamp: string | null = GLOBAL_STAMP) => {
|
|
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: {
|
|
getStampImageUrl: jest.fn().mockResolvedValue(globalStamp),
|
|
},
|
|
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 with the global stamp', async () => {
|
|
const { service, uploads, saved } = build();
|
|
|
|
await apply(service, staffDto);
|
|
|
|
expect(uploads).toContainEqual({ code: 'stamp_staff', image: GLOBAL_STAMP });
|
|
expect(saved[0]).toEqual(
|
|
expect.objectContaining({ stampFileId: 'file-stamp_staff' }),
|
|
);
|
|
});
|
|
|
|
it('ignores a stamp a staff client tries to supply', async () => {
|
|
const { service, uploads } = build();
|
|
|
|
await apply(service, {
|
|
...staffDto,
|
|
stampImageBase64: 'data:image/png;base64,SEFDSw==',
|
|
});
|
|
|
|
expect(uploads).toContainEqual({ code: 'stamp_staff', image: GLOBAL_STAMP });
|
|
expect(uploads.map((u) => u.image)).not.toContain(
|
|
'data:image/png;base64,SEFDSw==',
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Failing loudly matters here: getStampImageUrl degrades to null when the
|
|
* stamp cannot be inlined, and 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 }));
|
|
});
|
|
});
|