> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myfundingmachine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0014 retention billing transitions

# Stripe decides Retention billing transitions; staff arrangements hold them

Status: accepted

Date: 2026-10-19

## Decision

1. Every billing reconciliation plans one row of the transitions table below for
   the Retention card. It saves the plan on `retentionBillingState.planned` on
   every run, and writes to the CRM only when `RETENTION_BILLING_WRITES_ENABLED`
   is `true`. The flag covers billing only. `RETENTION_CRM_WRITES_ENABLED` still
   gates the staff stage picker, tag sync and review writes.
2. The plan is a pure function, `planRetentionBilling` in
   `lib/retentionBilling.ts`. The card move it needs comes from
   `billingStageTarget`. The Convex action only reads, plans, guards and writes.

   | Row | Stripe facts | Billing status / next bill / paid-through | Stage |
   | - | - | - | - |
   | 1 | Trialing, no paid service on this plan yet | Trial — new customer / trial end / last paid invoice | Unchanged |
   | 2 | Active, renewing, nothing owed; or trialing after this plan paid for service (a trial extension) | Active — paying / next bill / last paid invoice, or the trial end for an extension | Unchanged, or restored from the app's Critical |
   | 3 | `past_due`, `unpaid` or an open invoice | Past due / next bill / last paid invoice | Critical, saving the prior stage |
   | 4 | Collection paused | Paused / next bill if it resumes first, else blank / last paid invoice, or kept when open-ended | Unchanged |
   | 5 | Monthly plan, cancellation ahead | Canceled / blank / service end | Unchanged |
   | 6 | Ended, the Denver day after service end | Canceled / blank / service end | Churned + lost, saving the prior stage |
   | 7 | Canceled with money owed | Same as rows 5 and 6; open invoices on a canceled subscription are void | As rows 5 and 6 |
   | 8 | An arrangement label | Not written while in force | Unchanged |
   | 9 | Yearly plan ending | Review: "Annual plan: set Paid through." | Unchanged |
   | 10 | New subscription after a churn | Review: a fresh onboarding | Unchanged until staff reopen the card |

   An unlinked card (row 11) is never planned.
3. **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.
4. **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.
5. **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.

   Before each billing dispatch, the app saves the stage it moves from and the
   stage it moves to (`crmBeforeStageId`, `crmBeforeStatus`, `crmMoved`). It
   restores only a card still at `crmMoved`, 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.