import { expect, test } from "bun:test"; import { defineGateway, definePayment, memoryPaymentStore, paypalGateway, razorpayGateway, sandboxGateway, signSandboxWebhook, stripeGateway, derivePaymentStatus, verifyGatewayContract, } from "../src/index.ts"; test("gateway occurrence order and sticky terminal states prevent webhook regression", () => { const events = [ { id: "refund", intentId: "p", type: "refunded", status: "refunded" as const, at: 100, occurredAt: 300, }, { id: "late-capture-retry", intentId: "p", type: "captured", status: "captured" as const, at: 400, occurredAt: 200, }, ]; expect(derivePaymentStatus(events)).toBe("refunded"); expect( derivePaymentStatus([...events, { ...events[1], id: "new-id", at: 500, occurredAt: 500 }]), ).toBe("refunded"); }); test("sandbox drives webhook-authoritative capture and bounded partial refunds", async () => { const store = memoryPaymentStore(), gateway = sandboxGateway({ secret: "test-secret" }); const payment = definePayment({ store, adapters: [gateway], default: "sandbox", webhookSecrets: { sandbox: "test-secret" }, environment: "test", }); const created = await payment.initializePayment({ subject: "user-1", amount: 1000, currency: "USD", idempotencyKey: "checkout-1", }); expect(created.ok).toBe(true); if (!created.ok) return; expect(await payment.checkPayment(created.value.id)).toBe("pending"); // A browser redirect cannot advance local state. expect(await payment.checkPayment(created.value.id)).toBe("pending"); const signed = await signSandboxWebhook( { id: "evt-1", intentRef: created.value.gatewayRef, type: "payment.captured", status: "captured", }, "test-secret", ); const handler = payment.paymentWebhookHandler("sandbox"); expect( ( await handler({ req: new Request("https://app.test/webhook", { method: "POST", headers: { "x-wrnexus-signature": signed.signature }, body: signed.body, }), }) ).status, ).toBe(200); expect(await payment.checkPayment(created.value.id)).toBe("captured"); const first = await payment.refundPayment({ id: created.value.id, amount: 400, reason: "partial return", idempotencyKey: "refund-1", }); expect(first.ok).toBe(true); expect((await payment.refundableAmount(created.value.id)).amount).toBe(600); const excessive = await payment.refundPayment({ id: created.value.id, amount: 601, reason: "too much", idempotencyKey: "refund-2", }); expect(excessive).toMatchObject({ ok: false, fault: "refused" }); expect( ( await payment.refundPayment({ id: created.value.id, amount: 600, reason: "remaining return", idempotencyKey: "refund-remaining", }) ).ok, ).toBe(true); const stale = await signSandboxWebhook( { id: "evt-delayed-capture", intentRef: created.value.gatewayRef, type: "payment.captured", status: "captured", occurredAt: 1, }, "test-secret", ); await handler({ req: new Request("https://app.test/webhook", { method: "POST", headers: { "x-wrnexus-signature": stale.signature }, body: stale.body, }), }); expect(await payment.checkPayment(created.value.id)).toBe("refunded"); }); test("initialize, webhook, and refund replay change records once", async () => { const store = memoryPaymentStore(), gateway = sandboxGateway(); const payment = definePayment({ store, adapters: [gateway], default: "sandbox", webhookSecrets: { sandbox: "sandbox-secret" }, }); const input = { subject: "user-2", amount: 500, currency: "EUR", idempotencyKey: "same" }; const one = await payment.initializePayment(input), two = await payment.initializePayment(input); expect(one.ok && two.ok && one.value.id).toBe(two.ok ? two.value.id : ""); if (!one.ok) return; const signed = await signSandboxWebhook({ id: "evt-replay", intentRef: one.value.gatewayRef, type: "payment.captured", status: "captured", }); const request = () => new Request("https://app.test/webhook", { method: "POST", headers: { "x-wrnexus-signature": signed.signature }, body: signed.body, }); const handler = payment.paymentWebhookHandler("sandbox"); await handler({ req: request() }); const replay = await handler({ req: request() }); expect(await replay.json()).toEqual({ received: true, duplicate: true }); const refund = await payment.refundPayment({ id: one.value.id, amount: 100, reason: "return", idempotencyKey: "same-refund", }); const duplicate = await payment.refundPayment({ id: one.value.id, amount: 100, reason: "return", idempotencyKey: "same-refund", }); expect(refund.ok && duplicate.ok && refund.value.id).toBe(duplicate.ok ? duplicate.value.id : ""); expect(await payment.listRefunds(one.value.id)).toHaveLength(1); }); test("selection and capability differences are refused before money moves", async () => { const sandbox = sandboxGateway(); const noPartial = defineGateway({ ...sandbox, id: "full-only", capabilities: { ...sandbox.capabilities, partialRefund: false }, }); const payment = definePayment({ store: memoryPaymentStore(), adapters: [sandbox, noPartial], default: "sandbox", route: (input) => (input.currency === "INR" ? "full-only" : "sandbox"), }); const manual = await payment.initializePayment({ subject: "u", amount: 100, currency: "INR", country: "IN", capture: "manual", idempotencyKey: "route", }); expect(manual.ok).toBe(true); expect(verifyGatewayContract(sandbox)).toEqual({ ok: true, issues: [] }); const broken = defineGateway({ ...sandbox, id: "broken", capabilities: { ...sandbox.capabilities, authorizeThenCapture: false }, }); expect(verifyGatewayContract(broken).ok).toBe(false); }); test("invalid webhook signatures never update payment status", async () => { const gateway = sandboxGateway(), payment = definePayment({ store: memoryPaymentStore(), adapters: [gateway], default: "sandbox", webhookSecrets: { sandbox: "right" }, }); const created = await payment.initializePayment({ subject: "u", amount: 100, currency: "USD", idempotencyKey: "bad-hook", }); if (!created.ok) return; const response = await payment.paymentWebhookHandler("sandbox")({ req: new Request("https://app.test", { method: "POST", headers: { "x-wrnexus-signature": "wrong" }, body: JSON.stringify({ id: "bad", intentRef: created.value.gatewayRef, status: "captured" }), }), }); expect(response.status).toBe(400); expect(await payment.checkPayment(created.value.id)).toBe("pending"); }); test("every Tier 1 adapter passes the shared capability contract", () => { const adapters = [ sandboxGateway(), stripeGateway({ secretKey: "sk_test", publishableKey: "pk_test", webhookSecret: "whsec" }), razorpayGateway({ keyId: "rzp_test", keySecret: "secret", webhookSecret: "hook" }), paypalGateway({ clientId: "client", clientSecret: "secret", webhookId: "hook" }), ]; for (const adapter of adapters) expect(verifyGatewayContract(adapter)).toEqual({ ok: true, issues: [] }); });