@@ -46,27 +46,32 @@ Two problems motivated this record (raised in
4646The post-retirement routing matrix, confirmed as the intended final behavior in
4747the #14615 review, is:
4848
49- | Condition | Billing type | Cancellation UI |
50- | ---------------------------------------------------- | ------------ | --------------- |
51- | OSS distribution | ` legacy ` | fallback dialog |
52- | Cloud, workspace not yet loaded (no ` type ` ) | ` legacy ` | fallback dialog |
53- | Cloud team workspace (any rail) | ` workspace ` | rail-dependent |
54- | Cloud personal, rail ` legacy_stripe ` | ` legacy ` | fallback dialog |
55- | Cloud personal, rail ` stripe ` | ` workspace ` | Churnkey |
56- | Cloud personal, rail ` metronome ` | ` workspace ` | fallback dialog |
57- | Cloud personal, rail omitted / fetch failed / ` null ` | ` workspace ` | fallback dialog |
49+ | Condition | Billing type | Cancellation UI |
50+ | ------------------------------------------- | ------------ | --------------- |
51+ | OSS distribution | ` legacy ` | fallback dialog |
52+ | Cloud, workspace not yet loaded (no ` type ` ) | ` legacy ` | fallback dialog |
53+ | Cloud team workspace (any rail) | ` workspace ` | rail-dependent |
54+ | Cloud personal, rail ` legacy_stripe ` | ` legacy ` | fallback dialog |
55+ | Cloud personal, rail ` stripe ` | ` workspace ` | Churnkey |
56+ | Cloud personal, rail ` metronome ` | ` workspace ` | fallback dialog |
57+ | Cloud personal, no cached rail | ` workspace ` | fallback dialog |
5858
5959Churnkey is available only when the rail is known to be ` stripe ` ; every other
60- rail (including unknown) uses the fallback cancellation dialog.
60+ rail (including unknown) uses the fallback cancellation dialog. "No cached
61+ rail" means the session never received one: an omitted rail or a failed
62+ refresh does not clear an already-cached value, so a session that once saw
63+ ` legacy_stripe ` keeps routing to legacy until a response delivers a different
64+ rail.
6165
6266All consumers must obtain rail classification from a single shared decision
6367site (` getBillingRailPolicy ` in ` src/composables/billing/billingRailPolicy.ts ` )
6468rather than comparing ` billing_rail ` inline. That site handles the
6569` BillingRail ` union exhaustively (` satisfies never ` on the default branch), so
6670a widened union from the backend fails ` pnpm typecheck ` instead of silently
67- routing to workspace billing. Runtime absence (` null ` — rail not yet fetched, omitted, or fetch
68- failed) is handled outside the exhaustive switch, because it is an expected
69- state, not a type error.
71+ routing to workspace billing. The generated union is compile-time only, so the
72+ same site also handles the runtime states explicitly: an absent rail (` null ` —
73+ not yet fetched, omitted, or fetch failed) and a rail value this build does
74+ not recognize both return the fail-open policy.
7075
7176### 2. Unknown rail fails open to workspace billing, deliberately
7277
0 commit comments