fix: make payment event reduction monotonic
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-23 21:34:59 +05:30
parent 98e0813061
commit fe44bc2091
11 changed files with 458 additions and 16 deletions
@@ -0,0 +1,237 @@
import { afterEach, expect, test } from "bun:test";
import {
exerciseGatewayContract,
paypalGateway,
razorpayGateway,
sandboxGateway,
signSandboxWebhook,
stripeGateway,
type GatewayIntent,
} from "../src/index.ts";
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
const json = (value: unknown, status = 200) =>
new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
const encoder = new TextEncoder();
async function signature(body: string, secret: string) {
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
return [...new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(body)))]
.map((x) => x.toString(16).padStart(2, "0"))
.join("");
}
const input = {
id: "contract-pay",
subject: "contract-user",
amount: 1000,
currency: "USD",
idempotencyKey: "contract-init",
};
test("shared executable contract drives sandbox", async () => {
const adapter = sandboxGateway();
const report = await exerciseGatewayContract(adapter, {
input,
async webhook(intent) {
const signed = await signSandboxWebhook(
{
id: "sb-event",
intentRef: intent.ref,
type: "payment.captured",
status: "captured",
occurredAt: 100,
},
"contract-webhook-secret",
);
return new Request("https://test", {
method: "POST",
headers: { "x-wrnexus-signature": signed.signature },
body: signed.body,
});
},
});
expect(report).toEqual({ ok: true, issues: [] });
});
test("shared executable contract drives Stripe through its HTTP adapter", async () => {
globalThis.fetch = (async (value, init) => {
const url = String(value),
method = init?.method ?? "GET";
if (
url.includes("/payment_intents") &&
method === "POST" &&
!url.endsWith("/capture") &&
!url.endsWith("/cancel")
) {
const body = String(init?.body);
const id = body.includes("cancel") ? "pi_cancel" : "pi_contract";
return json({
id,
status: "requires_capture",
amount: 1000,
amount_received: 1000,
currency: "usd",
client_secret: "pi_client_safe",
});
}
if (url.endsWith("/capture"))
return json({
id: "pi_contract",
status: "succeeded",
amount_received: 1000,
currency: "usd",
});
if (url.endsWith("/cancel"))
return json({ id: "pi_cancel", status: "canceled", amount: 1000, currency: "usd" });
if (url.includes("/payment_intents/"))
return json({ id: "pi_contract", status: "requires_capture", amount: 1000, currency: "usd" });
if (url.endsWith("/refunds")) return json({ id: "re_1", status: "succeeded", amount: 500 });
if (url.endsWith("/customers")) return json({ id: "cus_1" });
if (url.includes("/payment_methods?")) return json({ data: [] });
if (url.includes("/payment_methods/") && url.endsWith("/attach"))
return json({
id: "pm_1",
card: { brand: "visa", last4: "4242", exp_month: 1, exp_year: 2030 },
});
if (url.includes("/payment_methods/") && url.endsWith("/detach")) return json({ id: "pm_1" });
return json({}, 404);
}) as typeof fetch;
const adapter = stripeGateway({
secretKey: "sk_test_secret",
publishableKey: "pk_test",
webhookSecret: "contract-webhook-secret",
});
const report = await exerciseGatewayContract(adapter, {
input,
async webhook(intent) {
const body = JSON.stringify({
id: "evt_1",
created: 100,
data: { object: { id: intent.ref, status: "succeeded" } },
type: "payment_intent.succeeded",
});
return new Request("https://test", {
method: "POST",
headers: {
"stripe-signature": `t=100,v1=${await signature(`100.${body}`, "contract-webhook-secret")}`,
},
body,
});
},
});
expect(report).toEqual({ ok: true, issues: [] });
});
test("shared executable contract drives Razorpay through its HTTP adapter", async () => {
globalThis.fetch = (async (value, init) => {
const url = String(value),
method = init?.method ?? "GET";
if (url.endsWith("/orders") && method === "POST") {
const body = String(init?.body);
return json({
id: body.includes("cancel") ? "order_cancel" : "order_1",
status: "created",
amount: 1000,
currency: "USD",
});
}
if (url.includes("/orders/"))
return json({
id: url.endsWith("order_cancel") ? "order_cancel" : "order_1",
status: "created",
amount: 1000,
currency: "USD",
});
if (url.includes("/refund")) return json({ id: "rfnd_1", status: "processed", amount: 500 });
return json({}, 404);
}) as typeof fetch;
const adapter = razorpayGateway({
keyId: "rzp_test",
keySecret: "key_secret",
webhookSecret: "contract-webhook-secret",
});
const report = await exerciseGatewayContract(adapter, {
input,
async webhook(intent) {
const body = JSON.stringify({
id: "rz_evt",
event: "payment.captured",
payload: {
payment: {
entity: { id: "pay_1", order_id: intent.ref, status: "captured", created_at: 100 },
},
},
});
return new Request("https://test", {
method: "POST",
headers: { "x-razorpay-signature": await signature(body, "contract-webhook-secret") },
body,
});
},
});
expect(report).toEqual({ ok: true, issues: [] });
});
test("shared executable contract drives PayPal through its HTTP adapter", async () => {
globalThis.fetch = (async (value, init) => {
const url = String(value),
method = init?.method ?? "GET";
if (url.endsWith("/oauth2/token")) return json({ access_token: "access" });
if (url.endsWith("/checkout/orders") && method === "POST") {
const body = String(init?.body);
return json({
id: body.includes("cancel") ? "PP-CANCEL" : "PP-ORDER",
status: "CREATED",
links: [{ rel: "approve", href: "https://paypal.test/approve" }],
});
}
if (url.includes("/checkout/orders/"))
return json({
id: url.endsWith("PP-CANCEL") ? "PP-CANCEL" : "PP-ORDER",
status: "CREATED",
purchase_units: [{ amount: { value: "10.00", currency_code: "USD" } }],
});
if (url.includes("/refund")) return json({ id: "PP-REFUND", status: "COMPLETED" });
if (url.endsWith("verify-webhook-signature")) return json({ verification_status: "SUCCESS" });
return json({}, 404);
}) as typeof fetch;
const adapter = paypalGateway({
clientId: "client",
clientSecret: "paypal_secret",
webhookId: "hook",
});
const report = await exerciseGatewayContract(adapter, {
input,
webhook(intent: GatewayIntent) {
return new Request("https://test", {
method: "POST",
headers: {
"paypal-auth-algo": "SHA256withRSA",
"paypal-cert-url": "https://paypal.test/cert",
"paypal-transmission-id": "tx",
"paypal-transmission-sig": "sig",
"paypal-transmission-time": "time",
},
body: JSON.stringify({
id: "WH-1",
create_time: "2026-08-23T00:00:00Z",
event_type: "PAYMENT.CAPTURE.COMPLETED",
resource: {
id: "CAPTURE",
status: "COMPLETED",
supplementary_data: { related_ids: { order_id: intent.ref } },
},
}),
});
},
});
expect(report).toEqual({ ok: true, issues: [] });
});
+12 -8
View File
@@ -20,7 +20,10 @@ test("payment plugin contributes its schema only when configured", async () => {
const runner = createPluginRunner(paymentPlugin(), context);
await runner.configure({ payment: { default: "sandbox", sandbox: true } } as any);
const contributions = await runner.contributions();
expect(contributions.migrations.map((row) => row.id)).toEqual(["wrnexus-payment-001"]);
expect(contributions.migrations.map((row) => row.id)).toEqual([
"wrnexus-payment-001",
"wrnexus-payment-002-event-order",
]);
expect(contributions.componentDirs).toHaveLength(1);
expect(contributions.routes.map((row) => row.path)).toEqual(["/api/payments/webhook/:gateway"]);
const off = createPluginRunner(paymentPlugin(), { ...context, metadata: new Map() });
@@ -41,13 +44,14 @@ test("database store persists append-only events and atomically caps refunds", a
warn() {},
});
await runner.configure({ payment: { default: "sandbox" } } as any);
const source = (await runner.contributions()).migrations[0].source!;
const up = source.split("-- +down")[0].replace("-- +up", "");
for (const statement of up
.split(";")
.map((x) => x.trim())
.filter(Boolean))
await db.exec(statement);
for (const migration of (await runner.contributions()).migrations) {
const up = migration.source!.split("-- +down")[0].replace("-- +up", "");
for (const statement of up
.split(";")
.map((x) => x.trim())
.filter(Boolean))
await db.exec(statement);
}
const store = databasePaymentStore();
const intent = {
id: "pay-1",
+54
View File
@@ -8,9 +8,35 @@ import {
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" });
@@ -69,6 +95,34 @@ test("sandbox drives webhook-authoritative capture and bounded partial refunds",
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 () => {