import { randomUUID } from 'node:crypto'; import { FakeGateway } from './helpers/fake-gateway'; import { captureDecision, refundAmount } from '../src/payments/payment.policy'; describe('provider-neutral payment blueprint', () => { const gateway = new FakeGateway('test-only-secret'); it('replays payment creation and rejects changed retry payloads', async () => { const input = { orderId: randomUUID(), amountMinor: '12500', currency: 'INR', idempotencyKey: randomUUID(), }; const first = await gateway.createPayment(input); expect(await gateway.createPayment(input)).toEqual(first); await expect( gateway.createPayment({ ...input, amountMinor: '1' }), ).rejects.toThrow('idempotency'); }); it('verifies exact raw bytes and rejects malformed, oversized and forged callbacks', () => { const event = { eventId: randomUUID(), paymentId: randomUUID(), orderId: randomUUID(), amountMinor: '12500', currency: 'INR', }; const raw = Buffer.from(JSON.stringify(event)); expect(gateway.verifyCapture(raw, gateway.sign(raw))).toEqual(event); for (const signature of ['', 'x'.repeat(64), '0'.repeat(64)]) expect(() => gateway.verifyCapture(raw, signature)).toThrow('signature'); expect(() => gateway.verifyCapture( Buffer.concat([raw, Buffer.from(' ')]), gateway.sign(raw), ), ).toThrow('signature'); const oversized = Buffer.alloc(32769); expect(() => gateway.verifyCapture(oversized, gateway.sign(oversized)), ).toThrow('signature'); const malformed = Buffer.from('{}'); expect(() => gateway.verifyCapture(malformed, gateway.sign(malformed)), ).toThrow(); }); it('requires matching capture references, amount and currency before fulfillment', () => { const now = new Date(); const event = { eventId: randomUUID(), paymentId: randomUUID(), orderId: randomUUID(), amountMinor: '12500', currency: 'INR', }; const expected = { ...event, expiresAt: new Date(now.getTime() + 1000), cancelled: false, alreadyCaptured: false, }; expect(captureDecision(expected, event, now)).toBe('COMMIT_RESERVED_STOCK'); expect( captureDecision({ ...expected, alreadyCaptured: true }, event, now), ).toBe('DUPLICATE'); expect(captureDecision({ ...expected, cancelled: true }, event, now)).toBe( 'REVIEW_AND_REFUND', ); expect(captureDecision({ ...expected, expiresAt: now }, event, now)).toBe( 'REVIEW_AND_REFUND', ); for (const change of [ { paymentId: randomUUID() }, { orderId: randomUUID() }, { amountMinor: '1' }, { currency: 'USD' }, ]) expect(() => captureDecision(expected, { ...event, ...change }, now), ).toThrow(); }); it('counts pending refunds against captured money and makes refund retries idempotent', async () => { expect(refundAmount(1000n, 200n, 300n, 500n)).toBe('500'); for (const request of [0n, -1n, 501n]) expect(() => refundAmount(1000n, 200n, 300n, request)).toThrow(); expect(() => refundAmount(1000n, -1n, 0n, 1n)).toThrow(); const request = { paymentId: randomUUID(), amountMinor: '500', idempotencyKey: randomUUID(), }; expect(await gateway.requestRefund(request)).toEqual( await gateway.requestRefund(request), ); await expect( gateway.requestRefund({ ...request, amountMinor: '501' }), ).rejects.toThrow('idempotency'); }); });