Stripe decides Retention billing transitions; staff arrangements hold them
Status: accepted Date: 2026-10-19Decision
-
Every billing reconciliation plans one row of the transitions table below for
the Retention card. It saves the plan on
retentionBillingState.plannedon every run, and writes to the CRM only whenRETENTION_BILLING_WRITES_ENABLEDistrue. The flag covers billing only.RETENTION_CRM_WRITES_ENABLEDstill gates the staff stage picker, tag sync and review writes. -
The plan is a pure function,
planRetentionBillinginlib/retentionBilling.ts. The card move it needs comes frombillingStageTarget. The Convex action only reads, plans, guards and writes.An unlinked card (row 11) is never planned. -
Service end is the latest of Stripe’s end date, the latest paid invoice
period and any workspace paid-through arrangement. It is compared as a
calendar day in
America/Denver. The account churns the day after. -
Arrangement labels are staff-owned billing status values:
- Complimentary holds indefinitely. Review when Stripe shows a paid, renewing plan or a missed payment.
- Billing deferred holds until Stripe bills on or after the card’s next billing date. A missed payment ends the hold as row 3; a payment on or after that date ends it as row 2. Review when the date is blank, or when it has passed with no bill.
- Paid through holds until the card’s paid-through date passes, then goes to review to renew or churn.
-
One writer per transition, and every write is guarded.
- Onboarding runner: creates the card and moves it to Ramp.
- Engine (usage): Ramp → Engaged or At Risk, At Risk ↔ Engaged, At Risk → Critical.
- App (billing): → Critical, the restore from it, and → Churned + lost.
- Staff: any stage through the picker, the arrangement labels and the Hold fields.
- CRM workflows only react, through notifications or tasks.
crmBeforeStageId,crmBeforeStatus,crmMoved). It restores only a card still atcrmMoved, and from Critical only on row 2: any other row or review keeps the card and its baseline. A card staff moved since then is theirs, and the restore waits for review. The final read before dispatch must still show the card’s stage and the three billing fields the plan was made from.
Context
Staff were setting the billing fields and Churned stage by hand from Stripe. The reconciliation already read Stripe every fifteen minutes for every linked card. It wrote only a partial projection and treated a cancellation with an open invoice as past due. A churn was keyed to an exact timestamp rather than the day staff and customers use. Two live trials failed with no recorded reason. FM-2888 recorded these decisions between 2026-10-17 and 10-19:- Q1: Critical with restore.
- Q2: Churn on the Denver day after service end.
- Q3: Canceled with money owed is not Past due.
- Q4: An annual term end goes to review.
- Q5: A restart goes to review.
- Q6: A billing-only flag.
- Q7: Hold Status and Hold Until stay staff fields.
Considered options
- Keep billing behind
RETENTION_CRM_WRITES_ENABLED. Rejected: that flag also turns on the stage picker, tag retry and an hourly Slack tag sync for every owner. Billing needs its own shadow period and off switch. - Treat a past-due owner as churn risk only, with no stage move. Rejected (Q1): staff work from the Critical column. The restore baseline makes the move reversible.
- Churn at Stripe’s exact end time. Rejected (Q2): it would churn an account in the evening of the last paid Mountain day.
- Churn an annual plan automatically. Rejected (Q4): annual terms are negotiated, and staff set Paid through instead.
- Restore a churned account when any subscription starts. Rejected (Q5): a new subscription is a fresh onboarding, not the old relationship resuming.
- Write arrangement labels from Stripe. Rejected: they record agreements Stripe cannot see. The app reads them and goes to review when Stripe contradicts or outlasts them.
Consequences
- Rollout is shadow first. Merging changes nothing in the CRM until the flag is set. Every run stores its plan, so a read-only plan-versus-CRM diff can gate the flag. Unsetting the flag stops writes on the next run.
- Billing review lists every planned transition that needs staff, and every card whose fields or stage differ from the plan. In shadow, this includes every planned move.
- A refresh failure keeps the earlier safeguards and records a scrubbed, 200-character reason.
- A restore baseline from before this change (Churned only,
crmChurnStageId) is still honoured. - Known gaps, accepted for phase 1:
- A card staff moved out of the app’s Critical stays in review until it reaches the saved stage.
- A refunded paid invoice still extends service to its period end.
- A restarted account whose card staff already reopened skips the Critical move for one run.

