StatusDRAFT — for frontend review
DriverJared Goguen (investigation), Frontend Engineering (implementation)
ReviewersFinTech Engineering Review — Mar 30
Repostratus (src/apps/dash/react/pages/billing/, src/apps/dash/react/pages/checkout/)
Staffing1 frontend engineer, 1 quarter
Cross-refsSmart Checkout (PM gate, UC expansion), Honest Invoicing (bad debt, invoices)

The billing dashboard has accumulated two checkout systems, twelve notification banner variants, two on-session payment hooks, and zero explicit handling for the most common billing error code. This section maps the surface as it exists today.

Multisku Checkout Redux CardElement 31-SKUs billing/checkout/ LEGACY
Unified Checkout React-Query PaymentElement per-product-gates checkout/ MODERN
Bad Debt Surfaces OutstandingBalanceCard PayOutstandingBalance 12-banners CONFUSING
Legacy use On Session Payment effect-based setClientSecret 4-consumers billing/checkout/hooks.ts
UC use On Session Payment callback-based confirmOnSessionPayment 2-consumers checkout/hooks/useOnSessionPayment.ts
Error 1210 no-explicit-handler generic-message 122-tickets/mo INVISIBLE
4 call sites 2 call sites PayOutstandingBalance
2
checkout systems
12+
banner variants
6
on-session-payment consumers
0
error 1210 handlers
Two Checkout Systems

Multisku (Redux, CardElement, 31 SKUs) and UC (React Query, PaymentElement, per-product gates). Different HTTP clients, state management, error handling. Same backend.

Banner Explosion

12+ notification variants. Three versioned iterations × two scope levels × two debt states. Plus enterprise dunning — a separate KV-targeting system entirely.

Two Identical Hooks

Two useOnSessionPayment hooks — effect-based (4 consumers) and callback-based (2 consumers). Same purpose, different API. Neither contains gate logic.

Silent Error 1210

Error 1210 (PM required) has zero frontend handlers. Multisku shows raw messages. UC shows generic "unexpected error." 122 can't-pay-now tickets/month.

Four changes that need only stratus code. No backend deploys, no gate flips, no API changes. Ship independently, in any order.

The notification system in notifications.tsx and NewFeatureNotifications.tsx has accumulated twelve-plus banner variants across three versioned iterations and two scope levels. Each variant is gated separately. The user experience: which of twelve banners will I see, and do they say the same thing?

Current banner variants (notifications.tsx)
Notification IDScopeCondition
bad_debt_on_account_notice_stripeAccountbad debt, no stripe pay
bad_debt_on_account_notice_stripe_v2Billingbad debt, stripe invoice pay, no stripe bad debt pay
bad_debt_on_account_notice_stripe_v3Billingbad debt, both stripe pays enabled — includes Pay now link
bad_debt_zone_stripeZonebad debt, no stripe pay (zone level)
bad_debt_zone_stripe_v2Zonebad debt, stripe invoice pay, no stripe bad debt pay
bad_debt_zone_stripe_v3Zonebad debt, both stripe pays enabled
outstanding_balance_on_account_notice_stripeBillingoutstanding balance, no bad debt
outstanding_balance_on_account_notice_stripe_v2Billingoutstanding balance + stripe pay enabled
unpaid_invoices_zone_stripeZoneunpaid invoices, no bad debt
unpaid_invoices_zone_stripe_v2Zoneunpaid invoices + stripe pay enabled
DunningBanner (enterprise)Accountdx-dunning-banner gate + KV targeting
Proposed: 3 States

(1) Outstanding balance + Pay Now CTA. (2) Bad debt + Pay Now CTA. (3) Enterprise dunning (keep as-is). Same message at account and zone level.

What Dies

The v1/v2/v3 versioned variants. The gate-combination filter chain. The distinction between "stripe invoice pay" and "stripe bad debt pay" — both enabled now.

Error code 1210 means "payment method required." The PM gate in subscriptions-api returns HTTP 402 with this code. The frontend has zero explicit handlers. This is the single highest-impact small change — 122 can't-pay-now tickets per month trace directly to users hitting this error with no guidance.

Today: Multisku

fetchWithoutErrorNotifications suppresses the global error bar. Raw backend message surfaces directly. Error code used only as a React key.

Today: UC

ApiError caught in useSubmitPurchase. Backend error discarded. Generic i18n string shown instead. Error code sent to analytics on wrong field.

CheckoutFileChange
Multiskubilling/checkout/components/view/PaymentCheckoutView.tsxCheck error code 1210 → show targeted PM recovery message
UCcheckout/hooks/useSubmitPurchase.tsCheck ApiError.status === 402 → show PM guidance with link
Bothcommon/components/billing/ (new)Shared PaymentMethodRequiredError component

Two hooks, same purpose, different API. Plus a code duplication within UC where PurchaseForm.tsx and useSubmitPurchase.ts contain near-identical executeOnSessionPayment logic.

Current state: two hooks
HookLocationAPI StyleConsumers
Legacybilling/checkout/hooks.tsEffect-basedPaymentCheckoutView, PayOutstandingBalance, SelectPlan, useChangePlanHandler
UCcheckout/components/payment/hooks/useOnSessionPayment.tsCallback-baseduseSubmitPurchase, PurchaseForm
Proposed: One Hook

Extract to common/components/billing/. Callback-based API (UC pattern). Both checkouts consume it. Gate checks stay at call sites.

Why Now

PM gate removal touches all 6 consumers. Dedup first → gate removal becomes mechanical. Also fixes PurchaseForm/useSubmitPurchase duplication.

useSubmitPurchase.ts only handles on-session payment for subs.length === 1. Multi-subscription checkouts silently skip 3DS confirmation. When the billing-on-session-payment gate becomes unconditional, this is a real bug — subscriptions created without payment confirmation.

The Fix

Extend executeOnSessionPayment to iterate over all client_secrets when multiple subscriptions are returned. Confirm each sequentially (Stripe requires serial confirmation). Can be done as part of the useOnSessionPayment dedup (1.3).

Two changes that need gate flips in LaunchDarkly or minor config changes, but no backend code. Can ship as soon as gates are confirmed.

Nine products moving from Multisku to Unified Checkout. Each product has a per-product feature gate. Independent of the Deferred Collection chain — runs in parallel. Full details in the Smart Checkout spec.

#ProductGateComplexityNotes
1Workers Paiddeseng-workers-unified-checkoutMediumUsage-based pricing
2Pages(needs gate)LowFixed pricing
3R2billing-unified-checkout-r2MediumUsage-based pricing
4Streambilling-unified-checkout-streamMediumUsage-based pricing
5Imagesbilling-unified-checkout-imagesLowFixed pricing
6Durable Objects(needs gate)MediumUsage + fixed
7Queues(needs gate)LowFixed pricing
8D1(needs gate)LowFixed pricing
9Vectorize(needs gate)LowFixed pricing

Full UC expansion details, per-product flow, and the invariant enforcement plan live in the parent spec. This section covers only the frontend implementation perspective.

The two-button problem: users see two invoices (original + synthetic consolidated) and don't know which to pay. The PayOutstandingBalancePage has a gate split — billing-use-pay-bad-debt-endpoint routes between consolidated and per-invoice payment. The UX doesn't explain the difference.

Today

Gate ON → /pay-bad-debt (consolidated). Gate OFF → /pay-invoice (per-invoice). Bad debt skips invoice selection. Nothing explains WHY or WHAT the amount represents.

Proposed

Add clear copy explaining the consolidated amount. Suppress confusing individual Pay buttons. Show breakdown on the outstanding balance card.

Two changes that need backend work or API shape confirmation before the frontend can implement. Prep work can start now.

Uncollectible Invoice Status

"Uncollectible" appears nowhere in stratus. Dashboard shows no special state. Need: badge, explanation, CTA. Requires API shape confirmation.

PM Gate Removal Prep

6 consumer locations become unconditional. Code change is trivial. Test matrix is not: 6 paths × 3 states × 2 checkouts. Write stubs now.

Key files in stratus that these changes touch. All paths relative to src/apps/dash/react/.

AreaFileChanges
Banners../../notifications.tsxConsolidate variants
Bannerscommon/components/NewFeatureNotifications.tsxSimplify filter chain
Error 1210pages/billing/checkout/components/view/PaymentCheckoutView.tsxAdd 1210 handler
Error 1210pages/checkout/hooks/useSubmitPurchase.tsAdd 402/1210 handler
Hook deduppages/billing/checkout/hooks.tsExtract → shared hook
Hook deduppages/checkout/components/payment/hooks/useOnSessionPayment.tsExtract → shared hook
Hook deduppages/checkout/components/purchase/PurchaseForm.tsxRemove duplicated logic
Multi-subpages/checkout/hooks/useSubmitPurchase.tsIterate client_secrets
Bad debtpages/billing/pay-outstanding-balance/components/PayOutstandingBalancePage.tsxAdd explanatory copy
Bad debtpages/billing/invoices/components/OutstandingBalanceCard.tsxAdd breakdown display
Bad debtpages/billing/invoices/components/InvoicesCard.tsxSuppress confusing Pay
UC expansionpages/checkout/ (various)Per-product adapters
Selectorscommon/selectors/billingSelectors.tsSimplify gate combos

Recommended execution order based on impact, effort, and dependency. The Tier 1 items can start immediately. UC expansion runs in parallel from start.

Week 1
Error 1210 Handler
Weeks 1–2
Banner Consolidation
Weeks 2–3
useOnSessionPayment Dedup + Multi-Sub Fix
Weeks 1–10
UC Expansion
Weeks 3–4
Bad Debt Pay Clarity
Weeks 4–5
PM Gate Removal Prep
Impact × Effort Matrix
ChangeEffortImpactBackend Dep?Tickets ΔPrep Value
Error 1210 handler2–3 daysHighNone−122/moPM gate removal
Banner consolidation~1 weekMediumNoneindirectClarity
Hook dedup + multi-sub~1 weekLow directNonePM gate removal
UC expansion (9 products)~9 weeksHighGate flipsMultisku sunset
Bad debt pay clarity~1 weekHighGate confirmindirectHonest invoicing
PM gate removal prep~1 weekFuture-highTest onlyDC readiness
#DecisionOwnerContext
1Are billing-stripe-bad-debt-payment-enabled and billing-pay-my-bill-enabled fully rolled out?FrontendDetermines if v1/v2 banner variants are dead code
2Which products need new UC gates vs. already have them?FrontendR2, Stream, Images, Workers have gates. Pages, DO, Queues, D1, Vectorize may not.
3Can the useOnSessionPayment dedup use the UC API (callback-based) as the canonical pattern?FrontendLegacy consumers (4 call sites) need migration to callback API
4Is billing-use-pay-bad-debt-endpoint fully rolled out?FrontendIf yes, the per-invoice path in PayOutstandingBalance is dead code
5Multisku sunset timeline — products remaining after UC Q2 cohort?FrontendDetermines whether Multisku can be decommissioned in Q2 or requires a Q3 tail
6Error 1210 analytics — is the Sparrow event firing today?FrontendNeed baseline measurement before building the handler