import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto'; import { z } from 'zod'; import type { GatewayPort, PaymentRequest, RefundRequest, } from '../../src/payments/gateway.contract'; import { commandHash } from '../../src/inventory/inventory.policy'; const captureSchema = z .object({ eventId: z.uuid(), paymentId: z.uuid(), orderId: z.uuid(), amountMinor: z.string().regex(/^[1-9]\d{0,15}$/), currency: z.enum(['INR', 'USD', 'EUR', 'GBP']), }) .strict(); // Deliberately confined to tests: this signature format is not a real provider protocol. export class FakeGateway implements GatewayPort { private readonly payments = new Map< string, { hash: string; paymentId: string } >(); private readonly refunds = new Map< string, { hash: string; refundId: string } >(); constructor(private readonly secret: string) {} async createPayment(input: PaymentRequest) { const hash = commandHash(input.orderId, input.amountMinor, input.currency); const prior = this.payments.get(input.idempotencyKey); if (prior && prior.hash !== hash) throw new Error('Gateway idempotency conflict'); const result = prior ?? { hash, paymentId: randomUUID() }; this.payments.set(input.idempotencyKey, result); return { paymentId: result.paymentId }; } sign(raw: Buffer) { return createHmac('sha256', this.secret).update(raw).digest('hex'); } verifyCapture(raw: Buffer, signature: string) { if ( raw.length > 32768 || !/^[a-f0-9]{64}$/.test(signature) || !timingSafeEqual( Buffer.from(signature, 'hex'), Buffer.from(this.sign(raw), 'hex'), ) ) throw new Error('Invalid gateway signature'); return captureSchema.parse(JSON.parse(raw.toString('utf8'))); } async requestRefund(input: RefundRequest) { const hash = commandHash(input.paymentId, input.amountMinor); const prior = this.refunds.get(input.idempotencyKey); if (prior && prior.hash !== hash) throw new Error('Refund idempotency conflict'); const result = prior ?? { hash, refundId: randomUUID() }; this.refunds.set(input.idempotencyKey, result); return { refundId: result.refundId, status: 'PENDING' as const }; } }