From ec285eabcd6de15b08600e40474c36d4fd90674f Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Ducasse Date: Sun, 11 Oct 2026 15:04:39 +0200 Subject: [PATCH] stripe: enable tax after card setup without the stale status gate Co-Authored-By: Claude Opus 5.5 --- .../src/router/stripe/trial-lifecycle.test.ts | 50 ++++++++++++++----- packages/api/src/router/stripe/webhook.ts | 21 ++++++-- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/packages/api/src/router/stripe/trial-lifecycle.test.ts b/packages/api/src/router/stripe/trial-lifecycle.test.ts index 9841f8c53..f5a9b82b5 100644 --- a/packages/api/src/router/stripe/trial-lifecycle.test.ts +++ b/packages/api/src/router/stripe/trial-lifecycle.test.ts @@ -454,11 +454,7 @@ describe("trial lifecycle", () => { ]); }); - test("setup checkout with a taxable address → automatic tax on", async () => { - customer = { - ...customer, - tax: { automatic_tax: "supported" }, - } as Stripe.Customer; + function stubSetup() { stubs.push( stub(stripe.setupIntents, "retrieve", () => Promise.resolve({ @@ -477,19 +473,47 @@ describe("trial lifecycle", () => { Promise.resolve({} as Stripe.Response), ), ); + } - await caller().sessionCompleted( - event("checkout.session.completed", { - mode: "setup", - customer: "cus_setup", - setup_intent: "seti_1", - } as Stripe.Checkout.Session), + const setupCompleted = () => + event("checkout.session.completed", { + mode: "setup", + customer: "cus_setup", + setup_intent: "seti_1", + } as Stripe.Checkout.Session); + + test("setup checkout completed → automatic tax on after the card", async () => { + stubSetup(); + + await caller().sessionCompleted(setupCompleted()); + + expect(updateSubscription.calls.map((c) => c.args)).toEqual([ + ["sub_setup", { default_payment_method: "pm_new" }], + ["sub_setup", { automatic_tax: { enabled: true } }], + ]); + }); + + test("Stripe rejecting the tax location still keeps the card", async () => { + stubSetup(); + stubs = stubs.filter((s) => s !== updateSubscription); + updateSubscription.restore(); + const update = stub( + stripe.subscriptions, + "update", + (_id: string, params?: Stripe.SubscriptionUpdateParams) => + params?.automatic_tax + ? Promise.reject(new Error("customer_tax_location_invalid")) + : Promise.resolve({} as Stripe.Response), ); + stubs.push(update); - expect(updateSubscription.calls[0]?.args).toEqual([ + await caller().sessionCompleted(setupCompleted()); + + expect(update.calls[0]?.args).toEqual([ "sub_setup", - { default_payment_method: "pm_new", automatic_tax: { enabled: true } }, + { default_payment_method: "pm_new" }, ]); + assertSpyCalls(update, 2); }); test("trialing → active clears trialEndsAt", async () => { diff --git a/packages/api/src/router/stripe/webhook.ts b/packages/api/src/router/stripe/webhook.ts index 2760177eb..486f39716 100644 --- a/packages/api/src/router/stripe/webhook.ts +++ b/packages/api/src/router/stripe/webhook.ts @@ -26,7 +26,6 @@ import { removeDomainFromVercelIfUnused } from "../../lib/vercel"; import { createTRPCRouter, publicProcedure } from "../../trpc"; import { buildFromSubscriptionOrThrow, - canAutomateTax, cancelSupersededSubscriptions, customerIdOf, getCurrentPeriodEnd, @@ -222,13 +221,25 @@ async function attachSetupPaymentMethod(session: Stripe.Checkout.Session) { const subscription = await stripe.subscriptions.retrieve(subscriptionId); if (customerIdOf(subscription) !== customerId) return; - // The setup session collected the billing address, so tax can start here. await stripe.subscriptions.update(subscriptionId, { default_payment_method: paymentMethod, - ...((await canAutomateTax(customerId)) && { - automatic_tax: { enabled: true }, - }), }); + + // The card now carries the billing address the setup session required, and + // Stripe Tax falls back to it. Just try: Stripe rejects a location it cannot + // place, and that must not undo the card we already attached. + if (!subscription.automatic_tax?.enabled) { + try { + await stripe.subscriptions.update(subscriptionId, { + automatic_tax: { enabled: true }, + }); + } catch (err) { + console.error( + `Failed to enable automatic tax on ${subscriptionId}:`, + err, + ); + } + } return subscription; } -- 2.51.2