feat(billing): plans sheet schema_version 2
What changed and why
Merged origin/main (including #21367 as 61e1100) so this draft's diff is only schema_version 2. The plans sheet now follows the latest copy deck.
- Cancel scheduled: header
Your {Plan} plan ends {date}, current lineYou're on {Plan} · {$price}/mo(yearly{$price}/yr). Only the current tier ischeckoutwithResubscribeon its current price. Every other tier isaction: none,cta_label: null, andblocked_line: "Resubscribe first to change plans". No recommended badge. The current tier is first. No switch-to-yearly while cancel-scheduled. - Same tier monthly to yearly: the current tier's yearly price carries
cta_labelSwitch to yearly · save {saving}(12× monthly − annual, in the price currency) so the client renders it verbatim. - Current cards set
compact: truewhen they have no tier button and no price CTA, so the client drops bullets. - Hours lines always say "of recording". One free month is
1 month free, not1 months free.
STOREFRONT_LADDER_V1_ENABLED is unchanged. No Stripe network, production data, flag flips, or deploys. Mobile client #21363 consumes backend/tests/fixtures/storefront_v2/ byte-for-byte.
Schema 1 on this route never shipped. The app-client compatibility gate ignores only that unpublished 1→2 response replacement; request changes and any later response break on the route still fail.
Product invariants affected
none
How it was verified
backend/test.sh with BACKEND_UNIT_TEST_FILE_LIST for the storefront unit tests, the v1 off-path byte check, and the app-client OpenAPI/Dart/Swift/TypeScript generator checks. Golden JSON is rendered from fake Stripe prices with no network. scripts/pr-preflight --lane local.
Tests
backend/tests/unit/test_storefront_presentation.py— hours 15/100/280/1,200, singular month-free, the plan × interval × cancel × legacy × tier matrix (including resubscribe-only and the yearly switch CTA), and twelve golden scenarios.backend/tests/unit/test_storefront.py— route, flag, cache, usage seam, and OpenAPI fields (blocked_line,compact, pricecta_label).backend/tests/unit/test_storefront_off_path.py— v1/v1/payments/available-plansbytes stay independent of the flag.- App-client generator checks cover the regenerated Dart, Swift, and TypeScript clients.
UI evidence
UI-Evidence: none -- flag-off API contract and generated clients; no rendered plans UI in this PR.
Failure class (fixes)
Failure-Class: none