Turning Stripe's Managed Payments off per request, rather than letting it pick the payment methods
Affects: backend/src/integrations/stripe/checkout.ts, backend/test/adapters.test.ts, components/integrations/billing-runner.tsx.
What was wrong
POST /v1/checkout/sessions had been failing since the billing runner shipped, and D-075 was written so that the reason could be read from outside rather than only from a log viewer. It worked on the first attempt after deploy. The endpoint answered 502 provider_error with { provider: "stripe", provider_status: 400, type: "invalid_request_error" } — no code, no param, which is itself a clue: Stripe attaches param when a named field is malformed and omits it when the field should not have been sent at all.
Stripe's own message, read from the account's request log:
Unsupported parameter:payment_method_types. Managed Payments, which is enabled by default on your account, handles this parameter for you. Removepayment_method_types, or passmanaged_payments[enabled]=falseto disable it for this request.
Managed Payments is on by default on accounts created recently, and it owns the method list. The request named payment_method_types[0]=card. Stripe refused the whole thing.
The decision
Send managed_payments[enabled]=false alongside payment_method_types[0]=card.
Stripe offered both ways out and they are not equivalent. Dropping payment_method_types is less code and it surrenders the guarantee that field exists to make: card is named explicitly so that an account with acss_debit enabled cannot offer a pre-authorised debit on a page this code describes as settling on approval. Under that failure a quote sits unpaid for days and nothing anywhere reports an error. Letting Managed Payments choose would move that decision into a dashboard toggle no reader of this repository can see.
Per request rather than account-wide, and deliberately: the setting travels with the code that depends on it, so a deployment against a different Stripe account behaves identically without anyone remembering to configure it.
The test, and what it can honestly claim
No unit test can know that a particular Stripe account has Managed Payments switched on, so no unit test could have predicted this 400. What a test can hold is the coupling, and adapters.test.ts now does: if the form names payment_method_types, it must also carry managed_payments[enabled]=false. Written as an implication rather than two flat equalities, so a future change that decides to let Managed Payments choose passes by deleting the parameter — only the half-change, which is the one that breaks production, fails.
The idempotency key needed no attention, and that is worth recording. Changing the body changed the key automatically, because D-074 made the key a hash of the bytes being posted. Under the previous scheme this fix would have re-broken every bucket already registered under the old shape, in production, on deploy, with the same opaque 400.
The second defect this surfaced
With the session finally rendering, the panel printed $4,120.79 CAD CAD. The house formatter formatCad appends the literal " CAD" and the runner appended the currency off the response as well. The fix is not to delete one of them: the totals everywhere else on this site are numbers this site computed, where hardcoding CAD is correct, and this one is a number a third party sent back, where the only honest label is the code that came with it. The runner now formats in the currency Stripe reported and falls back to a plain decimal rather than guessing when Intl does not recognise the code.
What this does not do
It does not make the account's Managed Payments setting irrelevant to anything else. The invoice path in adapter.ts does not name payment methods and was never affected — its /v1/invoiceitems calls were returning 200 throughout, and the 4xx seen there in an earlier session did not recur.
What would make this wrong
If Stripe deprecates managed_payments[enabled], or if a future requirement genuinely wants more than card on the hosted page. The second is the likelier one, and the answer then is to name the additional methods explicitly rather than to hand the list back — the point is that the page's behaviour is stated in the repository, not that it is decided elsewhere.