Skip to content

feat(billing): plans sheet schema_version 2 - #21397

Merged
Git-on-my-level merged 24 commits into
mainfrom
lane/sub-w5-presentation-v2
Oct 13, 2026
Merged

Git-on-my-level merged 24 commits into
mainfrom
lane/sub-w5-presentation-v2

Conversation

@Git-on-my-level

@Git-on-my-level Git-on-my-level commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

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 line You're on {Plan} · {$price}/mo (yearly {$price}/yr). Only the current tier is checkout with Resubscribe on its current price. Every other tier is action: none, cta_label: null, and blocked_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_label Switch to yearly · save {saving} (12× monthly − annual, in the price currency) so the client renders it verbatim.
  • Current cards set compact: true when 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, not 1 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, price cta_label).
  • backend/tests/unit/test_storefront_off_path.py — v1 /v1/payments/available-plans bytes 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

Git-on-my-level and others added 11 commits October 13, 2026 12:52
# Conflicts:
#	config/feature-flags.yaml
The dark storefront now returns the copy-deck contract — hours, headers, and checkout policy — from one English copy module, still behind the same flag.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Pyright rejected an always-true dict check, an unnarrowed currency, and checkout actions typed as plain strings.

Co-authored-by: Cursor <cursoragent@cursor.com>
@mintlify

mintlify Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
omi 🟢 Ready View Preview Oct 13, 2026, 8:27 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

Git-on-my-level and others added 3 commits October 13, 2026 20:29
Apply the latest plans-sheet copy deck: cancel-scheduled resubscribe only, yearly switch savings, compact current cards, and singular month-free.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The plans route never shipped a decoder, and the first client accepts only schema 2 on that same path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Git-on-my-level and others added 4 commits October 13, 2026 22:31
The merge of origin/main reformatted files this branch does not own, which grew the line-count ratchet against main.

Co-authored-by: Cursor <cursoragent@cursor.com>
Those paths belong to origin/main. The pinned Dart formatter rewrapped them during the merge, which the line-count ratchet counts as growth.

Co-authored-by: Cursor <cursoragent@cursor.com>
A scheduled cancellation was advertising the other interval, which checkout rejects. Monthly cards now check out the yearly price alone, and current cards send no bullets.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Git-on-my-level

Copy link
Copy Markdown
Collaborator Author

Monitor fix pass — both failing checks were inherited main red, resolved by merging main.

  • Root cause: Dart Analyze & Tests failed with 5 widget/service tests that also failed on unrelated PRs; upstream main was red. Main's 09772f9f (fix(app): restore green Flutter tests on main (#21302 #21340 #21341 #21343 follow-ups) #21425, "restore green Flutter tests on main") fixes exactly those tests. Mobile Release Eligibility only aggregated that failure.
  • Fix: merged 09772f9f into this lane (cfd8b85) — no storefront code changed; schema_version 2 contract and fixtures untouched (verified: blocked_line, compact, price cta_label intact in the merged OpenAPI; diff vs main remains the 34 storefront files).
  • Verified locally before push: storefront unit suites via backend/test.sh (85 + 60 passed), then the full bounded pre-push gate (56/56 manifest checks) against base 09772f9f.
  • CI on the lane head containing this merge: Dart Analyze & Tests SUCCESS, Mobile Release Eligibility SUCCESS.

@Git-on-my-level
Git-on-my-level marked this pull request as ready for review October 13, 2026 21:09
@Git-on-my-level
Git-on-my-level merged commit 39bc7b8 into main Oct 13, 2026
63 checks passed
@Git-on-my-level
Git-on-my-level deleted the lane/sub-w5-presentation-v2 branch October 13, 2026 21:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-13T21:19:19.569745Z 8c66931 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c66931824

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +259 to +261
if amount_minor is not None and interval in {'month', 'year'}:
period = 'mo' if interval == 'month' else 'yr'
price = money(amount_minor)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Render the current subscription in its actual currency

For a recognized JPY Plus or Unlimited subscription, build_storefront passes Stripe's zero-decimal unit_amount here without its currency, while money() always divides by 100 and prefixes $. For example, an active ¥4,800 subscription is reported as $48.00, causing the plans sheet to misstate the user's billing amount. Carry the Stripe currency into current_line and format zero-decimal currencies correctly, or omit unsupported amounts.

Useful? React with 👍 / 👎.

Comment on lines +50 to +56
def budget_units_for(plan_id: str) -> int | None:
"""Plan-catalog usage budget when declared, otherwise the presentation rate card."""
for name in ('USAGE_BUDGET_UNITS', 'PLAN_USAGE_BUDGET_UNITS'):
declared = getattr(plan_catalog, name, None)
if isinstance(declared, dict) and plan_id in declared:
return int(declared[plan_id])
value = load_copy().get('budget_units', {}).get(plan_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read usage budgets from the canonical plan catalog

When a paid ladder budget changes in plan_catalog.json, this lookup never sees it because the generated catalog exposes budgets under ENTITLEMENT_VERSIONS['ladder_v1']['monthly_budgets'], not either of these nonexistent top-level attributes. It therefore silently retains the duplicated literals in storefront_copy_v1.json, making hours_line and remaining-hours meter copy stale after an otherwise valid catalog update. Resolve the budget through the config.plan_catalog/rate-card API instead of maintaining a copied plan set.

AGENTS.md reference: backend/AGENTS.md:L155-L155

Useful? React with 👍 / 👎.

Comment on lines +357 to +361
action=action,
cta_label=cta_label(action=action, tier_plan=current_plan, current_plan=current_plan, locale=locale),
blocked_line=None,
compact=_compact(is_current=True, action=action, prices=current_prices),
prices=current_prices,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Suppress Resubscribe without a usable current price

When a non-legacy paid subscription is scheduled for cancellation and the current Stripe price cannot be retrieved, this fallback still emits action: checkout and cta_label: Resubscribe while prices is empty. A retained current price that is no longer purchasable similarly produces a CTA whose ID the checkout endpoint rejects. In either case the client renders a reactivation button that cannot succeed; gate the action and CTA on an exact current price that is both retrieved and accepted by the checkout guard.

Useful? React with 👍 / 👎.

Comment on lines +247 to +248
text = copy_for(locale)['current']
note = text['legacy_note'] if legacy else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive discontinued-plan copy from catalog lifecycle

Here legacy only means that the current price lacks a ladder-v1 entitlement binding, but the code treats it as proof that the plan is no longer sold. Current catalog plans such as Plus and Unlimited v2—including their recognized JPY prices—can therefore receive the false message “We don't sell this plan anymore.” Determine this note from the canonical plan lifecycle/storefront state, or use neutral legacy-entitlement copy instead.

AGENTS.md reference: backend/AGENTS.md:L155-L155

Useful? React with 👍 / 👎.

Comment on lines +287 to +288
want_switch = is_current and current_interval == 'month' and not resubscribe_only and 'year' in offered
prices = _prices_for(rows, offered=offered, locale=locale, switch_yearly=want_switch)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep current-tier prices tied to the subscriber's price

When a ladder-v1 price is retired and replaced, its entitlement binding remains recognized but purchasable(price_id) removes it from rows, so this current-tier card is built from the replacement prices instead of current_row. An annual subscriber can consequently see the new annual amount as their current price, while a monthly subscriber's “save” label is calculated against the new monthly list price rather than what they actually pay and can claim savings even when switching costs more. Use the stored current price for current-card display and as the monthly savings baseline.

Useful? React with 👍 / 👎.

This branch was successfully deployed

1 active deployment
staging - docs — 8c669318 Deployed Oct 13, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant