Q1 2026 TECH SPEC
Smart Checkout: Confirm Before Commit
Set the Stripe parameters that enforce payment-before-state on every subscription path. Then flip entitlement provisioning to the payment success callback. Rollbacks become structurally impossible.
| Status | IN PROGRESS — Act 1 Phase 2 active, Continuation planned |
|---|---|
| Driver | Jared Goguen |
| Repos | subscriptions-api |
| Staffing | 1 engineer, partial quarter |
The old model committed state before payment confirmed — provision entitlements, publish events, attempt payment, then try to rollback on failure. This commit-then-pay ordering is the root cause of state drift, duplicate charges, and entitlement inconsistency.
Commit state → attempt payment → rollback on failure. Each rollback is a window for drift. ~200 rollbacks/day at the peak.
The system blocks users without a valid payment method from any subscription change. Returns HTTP 402 / error 1210. Legitimate users can't subscribe.
7 components involved in undoing state on failure: rollback calculator, Stripe rollback, OPE rollback, fallback calculator, schedule re-creation. Each is a window for partial state.
Three checkout systems: Multisku, Unified Checkout, Billing Profile. Different payment flows, different error handling, different PM management. Consolidation blocked on the PM gate.
Q1 delivers the foundation: Stripe parameter migration across all payment paths, then the continuation workflow that flips entitlement provisioning to payment success. Together they unlock PM Gate removal and UC expansion in Q2.
Four Stripe API call sites in subscriptions-api create or modify subscriptions. Each one needs a specific set of Stripe parameters to enforce confirm-before-commit ordering. For each call site: set the right parameters, handle the new error types, and verify payment confirmation happens before state changes.
| # | Path | What It Does | Phase | Params |
|---|---|---|---|---|
| 1 | New subscription creation | Stripe.Subscription.Create with payment_behavior | 1 ✓ | default_incomplete |
| 2 | Subscription update (change) | Stripe.Subscription.Update with pending_if_incomplete | 2 | pending_if_incomplete |
| 3 | Schedule elimination | Existing schedules converted to immediate updates | 2 | pending_if_incomplete |
| 4 | Payment collection (on + off session) | PaymentIntent — on/off session collapsed | 1 ✓ | (already done) |
payment_behavior: default_incomplete. New subscriptions create an incomplete invoice with a PaymentIntent. Frontend collects payment via Stripe.js. Subscription activates only on payment success.
pending_if_incomplete on updates. Schedule elimination: convert future-dated changes to immediate updates with pending payment. Conflict handling: what happens when two updates race?
Two states. Today we're in State A. The continuation workflow moves us to State B. The difference is when entitlements are provisioned — before payment or after.
| Stage | What Changes |
|---|---|
| Commit | NEW — gains provision. Entitlements, events, billing anchor set HERE instead of at Apply start. |
| Rollback | DELETED — nothing was provisioned, so rollback is a no-op. Code deleted. |
Save Phases, Cancel Phases, Sync Stripe, Signal Loop, and Decision Engine are unchanged.
Flag gates the continuation path. Ramp by account segment. Kill switch: revert to eager entitlement if metrics degrade. No code deleted until flag is 100% for 2+ weeks.
0 rollbacks triggered. 0 entitlements granted without payment confirmation. Payment success rate ≥ baseline. Measured for 2 weeks at 100% before deletion manifest executes.
What dies when the flip happens. Each deletion is gated on the continuation workflow working correctly for all payment paths.
| Component | What It Does Today | Why It Dies |
|---|---|---|
| rollback_calculator.go | Computes what to undo on payment failure | Nothing was provisioned |
| RollbackChangesAsync | Async rollback of subscription state changes | Nothing to undo |
| RollBackMultiSubscriptionUpdate | Multi-sub rollback coordination | Nothing to undo |
| Fallback calculator | Fallback when rollback partially fails | No partial state |
| Schedule re-creation | Recreates schedules after failed payment | Schedules eliminated (Act 1) |
| OPE rollback | Rolls back entitlement changes | Entitlements never changed |
| Stripe rollback | Reverts Stripe subscription to prior state | Stripe sub in pending state, auto-expires |
- 7
- components deleted simultaneously
- 0
- rollbacks needed after the flip
- 0
- partial state windows
| # | Invariant | Property |
|---|---|---|
| I1 | No entitlement without payment | Every active subscription has a corresponding successful PaymentIntent. No entitlement granted before payment confirmation. |
Inline: commit() verifies PaymentIntent succeeded before granting entitlements. Batch: BQ query for active subs without successful PaymentIntent. Alert: entitlements_granted_without_payment counter — PagerDuty if > 0.
| # | Decision | Owner | By When | Status | Recommendation | Risk if Wrong |
|---|---|---|---|---|---|---|
| 1 | Phase 2 schedule conflicts — racing updates? | Engineering | Q1 (PDE P2) | OPEN | Second update fails or queues | Medium — race conditions |
| 2 | Event ordering — consumers assume Apply-time events? | Engineering | Q1 (flip) | OPEN | Audit all downstream consumers | High — silent downstream break |
| Risk | Impact | Owner | Mitigation |
|---|---|---|---|
| Workflow abandonment | Medium | Engine lead | Lost struct = pending Stripe sub. Need cleanup job. |
Q1 proves payment-before-entitlement on every subscription path. With that foundation in place, the PM Gate can be removed — the gate exists because the old model couldn't guarantee payment, but the continuation workflow makes payment confirmation structural. Unified Checkout can expand because checkout is unified behind a single Stripe.js component, no longer blocked by the gate.
PM Gate Removal, UC Expansion, and the full Smart Checkout orchestration.
- −141
- tickets/month eliminated by Continuation
Confirm payment before state commits — with the customer present. If payment fails, nothing was committed. Nothing to undo.