import { beforeEach, describe, expect, test } from "bun:test"; import type Stripe from "stripe"; const { state, accountSql, disabled, adminNotices, processStripeEvent, event, post, plan, subscription, resetWebhookHarness, } = await import("./webhook.test-helper"); beforeEach(() => resetWebhookHarness()); describe("release hardening", () => { const invoice = (subscriptionId: string) => ({ customer: "cus_1", parent: { subscription_details: { subscription: subscriptionId } }, }); const status = () => ( accountSql("o1") .query("SELECT subscription_status AS s FROM billing_customers WHERE account_id = 'o1'") .get() as { s: string; } ).s; test("events from the other Stripe mode are ignored", async () => { const live = { ...event("customer.subscription.created", subscription("active")), livemode: true }; expect(await processStripeEvent(live as Stripe.Event)).toBe("ignored"); expect(plan("o1")).toBeNull(); expect((await post(live as Stripe.Event)).status).toBe(200); expect(plan("o1")).toBeNull(); }); test("invoice.paid recovers a past_due subscription; an older one cannot", async () => { await processStripeEvent(event("customer.subscription.created", subscription("active"), 100)); await processStripeEvent(event("invoice.payment_failed", invoice("sub_1"), 200)); expect(status()).toBe("past_due"); await processStripeEvent(event("invoice.paid", invoice("sub_other"), 300)); expect(status()).toBe("past_due"); await processStripeEvent(event("invoice.paid", invoice("sub_1"), 50)); expect(status()).toBe("past_due"); expect(await processStripeEvent(event("invoice.paid", invoice("sub_1"), 300))).toBe("processed"); expect(status()).toBe("active"); expect(plan("o1")).toBe("pro"); }); test("past_due_since starts once and is not reset by later past_due updates", async () => { const since = () => ( accountSql("o1").query("SELECT past_due_since AS v FROM billing_customers WHERE account_id = 'o1'").get() as { v: number | null; } ).v; await processStripeEvent(event("customer.subscription.created", subscription("active"), 100)); expect(since()).toBeNull(); await processStripeEvent(event("invoice.payment_failed", invoice("sub_1"), 200)); expect(since()).toBe(200_000); await processStripeEvent(event("customer.subscription.updated", subscription("past_due"), 300)); await processStripeEvent(event("customer.subscription.updated", subscription("past_due"), 400)); expect(since()).toBe(200_000); await processStripeEvent(event("customer.subscription.updated", subscription("active"), 500)); expect(since()).toBeNull(); }); test("an older out-of-order subscription event cannot undo a completed checkout", async () => { const checkout = { id: "cs_order", mode: "subscription", status: "complete", payment_status: "paid", customer: "cus_1", subscription: "sub_1", metadata: { account_id: "o1", kind: "pro" }, }; await processStripeEvent(event("checkout.session.completed", checkout, 500)); expect(plan("o1")).toBe("pro"); await processStripeEvent(event("customer.subscription.created", subscription("incomplete"), 400)); expect(plan("o1")).toBe("pro"); expect(status()).toBe("active"); await processStripeEvent(event("customer.subscription.updated", subscription("active"), 600)); const row = accountSql("o1") .query("SELECT current_period_end AS e FROM billing_customers WHERE account_id = 'o1'") .get() as { e: number; }; expect(row.e).toBe(1_800_000_000_000); }); test("same-second checkout and incomplete subscription events end active in either order", async () => { const checkout = { id: "cs_tie", mode: "subscription", status: "complete", payment_status: "paid", customer: "cus_1", subscription: "sub_1", metadata: { account_id: "o1", kind: "pro" }, }; const incomplete = () => event("customer.subscription.created", subscription("incomplete"), 700); const completed = () => event("checkout.session.completed", { ...checkout, id: `cs_tie_${state.sequence}` }, 700); // Checkout first, then the stale incomplete from the same second. await processStripeEvent(completed()); await processStripeEvent(incomplete()); expect(status()).toBe("active"); expect(plan("o1")).toBe("pro"); // Reset, then the incomplete first and the checkout after it. accountSql("o1").exec("DELETE FROM billing_customers; DELETE FROM account_billing;"); await processStripeEvent(incomplete()); expect(status()).toBe("incomplete"); await processStripeEvent(completed()); expect(status()).toBe("active"); expect(plan("o1")).toBe("pro"); // A same-second active update still applies; neither incomplete nor incomplete_expired can undo active. await processStripeEvent(event("customer.subscription.updated", subscription("active"), 700)); await processStripeEvent(event("customer.subscription.updated", subscription("incomplete"), 700)); expect(status()).toBe("active"); await processStripeEvent(event("customer.subscription.updated", subscription("incomplete_expired"), 700)); expect(status()).toBe("active"); }); test("same-second recovery and resume reach active; same-second failures still move forward", async () => { await processStripeEvent(event("customer.subscription.created", subscription("active"), 800)); await processStripeEvent(event("customer.subscription.updated", subscription("past_due"), 800)); expect(status()).toBe("past_due"); await processStripeEvent(event("customer.subscription.updated", subscription("active"), 800)); expect(status()).toBe("active"); await processStripeEvent(event("customer.subscription.updated", subscription("paused"), 900)); await processStripeEvent(event("customer.subscription.updated", subscription("active"), 900)); expect(status()).toBe("active"); await processStripeEvent(event("customer.subscription.updated", subscription("unpaid"), 1000)); await processStripeEvent(event("customer.subscription.updated", subscription("active"), 1000)); expect(status()).toBe("active"); await processStripeEvent(event("customer.subscription.deleted", subscription("canceled"), 1100)); await processStripeEvent(event("customer.subscription.updated", subscription("active"), 1100)); expect(status()).toBe("canceled"); }); test("incomplete_expired follows incomplete at the same second", async () => { await processStripeEvent(event("customer.subscription.created", subscription("incomplete"), 1200)); await processStripeEvent(event("customer.subscription.updated", subscription("incomplete_expired"), 1200)); expect(status()).toBe("incomplete_expired"); expect(plan("o1")).toBe("free"); }); test("same-second invoice.paid and payment_failed: the payment wins in either order", async () => { await processStripeEvent(event("customer.subscription.created", subscription("active"), 100)); await processStripeEvent(event("invoice.payment_failed", invoice("sub_1"), 200)); await processStripeEvent(event("invoice.paid", invoice("sub_1"), 200)); expect(status()).toBe("active"); await processStripeEvent(event("invoice.paid", invoice("sub_1"), 300)); await processStripeEvent(event("invoice.payment_failed", invoice("sub_1"), 300)); expect(status()).toBe("active"); // A newer failure still marks it past_due. await processStripeEvent(event("invoice.payment_failed", invoice("sub_1"), 400)); expect(status()).toBe("past_due"); }); test("a dispute matching no account tells admins instead of passing silently", async () => { await processStripeEvent( event("charge.dispute.created", { id: "dp_lost", payment_intent: "pi_none", amount: 900 }), ); expect(disabled).toEqual([]); expect(adminNotices.some((title) => title.includes("dp_lost"))).toBe(true); }); });