A customer upgrades their plan. The system commits the subscription change, provisions entitlements, publishes events to ~50 downstream services, then attempts payment. On failure, it tries to roll everything back. The reversal logic is a parallel inverse of the forward path — every step that committed state needs an undo step.

Before: Commit → Pay → Rollback?

Subscription written, entitlements provisioned, events published. Then payment attempted. On failure: undo subscription, revoke entitlements, compensating events. 196 rollbacks/day.

After: Pay → Commit

PaymentIntent created and confirmed before any subscription exists. If payment fails or requires 3DS, there is nothing to roll back. Zero rollbacks by construction.

Commit Pay Rollback?
Pay Confirm Commit

Steps 1–4 have zero committed state. If anything fails, just stop. Only the Commit step creates hard-to-reverse state, and it only runs after payment is confirmed. The green zone is not a safety net — it is the absence of anything that needs one.

StepWhatStatusTimelineDepends On
1Unify payment collection — one path regardless of originIn progress~Feb
2adefault_incomplete for subscription createsShipped (gated)Q1Step 1
2bRefactor discounts out of update paramsShippedQ1
2cRefactor subscription schedules out of update paramsPending~MarStep 2b
2dpending_if_incomplete for subscription updatesPending~MarStep 2c
3Grant features after payment — planned-state struct, split MakeObjectProductsChanges into plan+commitPending~AprStep 2d
4Remove reversal logic — delete rollback machinery, remove WorkflowID/Phase1Pending~AprStep 3

Step 3 is the structural pivot. Today, MakeObjectProductsChanges does everything in one call — compute the change, write to Stripe, write to DB, provision entitlements. After Step 3, it splits into plan (compute what would change) and commit (write it all after payment confirms). The planned-state struct bridges the gap.

Plan Phase

Compute the subscription diff, proration, entitlement changes. Write nothing. Return a planned-state struct that captures the intended outcome.

Commit Phase

After payment confirms, apply the planned state: Stripe subscription, DB records, entitlement provisioning, PGQ events. One atomic sequence, no undo needed.

Payment ResultSessionActionState Committed?
SucceedsAnyCommit inlineYes — after payment
Requires action (3DS)On-sessionWorkflow → authenticate → commitYes — after auth
Requires actionOff-sessionVoid invoiceNo
Requires new PMOn-sessionWorkflow → new PM → commitYes — after new PM
Requires new PMOff-sessionVoid invoiceNo
Hard failureAnyVoid invoiceNo
ComponentPurposeWhy Unreachable
Rollback ProcessorEntire rollback machinery — undo Stripe writes, revert DB, deprovision entitlementsPayment confirms before commit; nothing to undo
ApplyImmediateSubscriptionChangesTemporal workflow for immediate (non-scheduled) subscription mutationsAlready removed (BILLACCT-946)
probation/ packageInterim account state while payment outcome was pendingConfirm-before-commit eliminates the pending window
WorkflowID / Phase1 OPE fieldsTracked rollback state so the system knew what to undo on failureNever written in the new flow — no rollback to track
Pre-computed rollback in PLANchanges_calculator PLAN stage pre-computes rollback data for each mutationNo rollback means no rollback data to compute