feat(billing): dark unified storefront presentation contract
What changed and why
Adds a default-off, versioned GET /v2/payments/plans contract for the unified Free/Plus/Pro/Architect sheet. Clients render backend-authored names, features, prices, actions and usage. Optional source and typed trigger carry opening context. Generated OpenAPI and mobile/macOS/Windows/web DTOs expose the contract; W6 owns rendered acceptance. Installed-client payment/user APIs remain unchanged, with byte-comparison coverage.
Review fixes prevent lower-tier offers for Neo (unlimited) and Unlimited (unlimited_v2): Free and any visible Plus row are downgrade_not_offered; only Pro/Architect are explicit higher contact offers. A non-ladder plan stays legacy even if its historical price is mistakenly activated. Every current plan id has exactly one card, with existing-benefits copy for legacy subscribers. Missing ladder metadata cannot raise StopIteration or imply new entitlements. Legacy migration checkout remains unauthorized.
Contextual recommendation badges apply only to higher upgrade/contact offers. Free receives Plus at budget/MCP gates and Pro at Pro gates; Plus and Neo/Unlimited may receive Pro. Pro/Architect never receive a current/lower Pro badge. Badges never select a plan or authorize checkout.
Price reads share the existing user-subscription stripe_price:{id} Redis cache and 24-hour TTL, including archived current prices. The route uses a boost-exempt 12/minute per-UID policy before provider/data reads, with the shared fail-open Redis behavior. Optional shared-budget usage is accepted only for Free/ladder-v1 subscribers; legacy usage is speech-labeled or unavailable.
W3 registrations and the existing checkout guard must both admit a ladder price. Missing bindings hide Plus/Pro; Architect remains contact-only without an invented amount. Prices use retrieved USD month/year recurring amounts and computed annual savings. App Review hiding runs before subscription/price/usage reads; subscription lookup uses provision=False. The feature flag accepts only exact true and remains default-off. Current main was integrated with a merge commit; both default-off registry declarations were preserved.
Product invariants affected
none
How it was verified
All backend tests were run via backend/test.sh: 85 storefront policy/FastAPI endpoint tests and 60 installed-client byte comparisons pass on the merged head. The shared rate-limit suite passes 89 tests with one existing fakeredis/Lua execution test skipped. Generator suites pass 43 tests. Database, Stripe and usage-provider boundaries are mocked; no live provider, production data or deployed endpoint was exercised.
Regression coverage includes empty/populated ladder bindings, mistakenly activated Neo/Unlimited prices, exactly one current card, no lower/current recommendation, cache hit/miss/24-hour TTL/provider failure, actual dependency 429 before data reads, and legacy/shared-budget separation. Normal push hooks validate full backend Pyright (zero errors), source closure, OpenAPI compatibility/generated outputs, web typechecking plus 658 tests, and the macOS debug build. Final make preflight and PR metadata validation are recorded on this head. GitHub CI remains a separate layer and is reported at handoff.
UI evidence
UI-Evidence: none -- dark backend contract and generated wire DTOs; W6 owns rendered acceptance
Failure-Class: none
Reviewer risks
- W3's optional usage facade is absent on main; coordinate its final signature before activation. Legacy chat-only usage explicitly remains unavailable in this schema.
- Price freshness follows the existing 24-hour cache TTL. USD monthly/yearly presentation only; Architect live billed amount was not verified.
- Registration alone cannot reopen checkout. Price-version-aware checkout admission and approved migration policy remain separate requirements.
- Do not activate until W3/W4/W6 and the W2 measurement baseline are ready. No Stripe writes, production data access, flag flips or deployments occurred.