feat(membership): consolidate plan name + lifetime card shape + merge with billing #97

Merged
YousifShkara merged 1 commit from feat/bunyip-upgrade-01-membership-plan into main 2026-06-10 07:57:07 +02:00
Owner

Three audit findings (1, 2, 9) all live in the same surface and ride one PR.

Finding 1 (plan name inconsistency): a lifetime member's Membership card
called the plan "Lifetime" in the badge AND "Standard" in the Plan line, the public Pricing card called the same product "Starter", and the Settings "Account Type" cell rendered the literal "subscriber". Four names for one thing across three pages.

fn tier_name(&SubscriptionTier) is now pub and the canonical helper for the plan-name string. The Settings cell at the Account Information card and the marketing-facing pricing card both call it. Renaming a tier ("Standard" -> "Starter", whatever the product team picks) is now a one-line change in tier_name that updates every consumer. checkout_success also dropped its inline match user.subscription_tier { ... } block and now calls tier_name(&user.subscription_tier), so the post-checkout confirmation lines up too.

BUSINESS's first feature bullet went from "Everything in Starter" to "Everything in the personal plan" so the marketing copy stays accurate after a tier rename.

Finding 2 (cancel buttons for lifetime members): the Membership card body now branches on lifetime BEFORE the has check. Lifetime members see only "Plan: <tier_name>" and "Access: Lifetime - no billing" - no price field, no next-billing field, no "Cancel Membership" or "Cancel Now" buttons. The has boolean also explicitly excludes lifetime so a lifetime user with a stray Active row from the legacy billing flow can never accidentally see cancel UI (defense in depth).

Finding 9 (Membership/Billing redundancy): the /billing handler is now a 308 permanent redirect to /membership. The Membership page absorbs the invoices table from the retired Billing page; it always renders so a one-off charge or refund still surfaces (empty-state copy covers the common lifetime-member case). The page title, H1, and tab title rename to "Membership & Billing"; the sidebar nav drops the standalone "Billing" entry and renames the remaining entry to "Membership & Billing".

axum::response::IntoResponse was added to the dashboard.rs import set so Redirect::permanent("/membership").into_response() resolves.

See docs/bunyip-upgrade/01-membership-plan-data.md for the full spec and verification.

Three audit findings (1, 2, 9) all live in the same surface and ride one PR. Finding 1 (plan name inconsistency): a lifetime member's Membership card called the plan "Lifetime" in the badge AND "Standard" in the Plan line, the public Pricing card called the same product "Starter", and the Settings "Account Type" cell rendered the literal "subscriber". Four names for one thing across three pages. `fn tier_name(&SubscriptionTier)` is now `pub` and the canonical helper for the plan-name string. The Settings cell at the Account Information card and the marketing-facing pricing card both call it. Renaming a tier ("Standard" -> "Starter", whatever the product team picks) is now a one-line change in `tier_name` that updates every consumer. `checkout_success` also dropped its inline `match user.subscription_tier { ... }` block and now calls `tier_name(&user.subscription_tier)`, so the post-checkout confirmation lines up too. `BUSINESS`'s first feature bullet went from "Everything in Starter" to "Everything in the personal plan" so the marketing copy stays accurate after a tier rename. Finding 2 (cancel buttons for lifetime members): the Membership card body now branches on `lifetime` BEFORE the `has` check. Lifetime members see only "Plan: <tier_name>" and "Access: Lifetime - no billing" - no price field, no next-billing field, no "Cancel Membership" or "Cancel Now" buttons. The `has` boolean also explicitly excludes `lifetime` so a lifetime user with a stray Active row from the legacy billing flow can never accidentally see cancel UI (defense in depth). Finding 9 (Membership/Billing redundancy): the `/billing` handler is now a 308 permanent redirect to `/membership`. The Membership page absorbs the invoices table from the retired Billing page; it always renders so a one-off charge or refund still surfaces (empty-state copy covers the common lifetime-member case). The page title, H1, and tab title rename to "Membership & Billing"; the sidebar nav drops the standalone "Billing" entry and renames the remaining entry to "Membership & Billing". `axum::response::IntoResponse` was added to the dashboard.rs import set so `Redirect::permanent("/membership").into_response()` resolves. See `docs/bunyip-upgrade/01-membership-plan-data.md` for the full spec and verification.
feat(membership): consolidate plan name + lifetime card shape + merge with billing
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt / clippy / build / test (pull_request) Successful in 1m2s
b6df86e8b6
Three audit findings (1, 2, 9) all live in the same surface and ride one PR.

Finding 1 (plan name inconsistency): a lifetime member's Membership card
called the plan "Lifetime" in the badge AND "Standard" in the Plan line, the public Pricing card called the same product "Starter", and the Settings "Account Type" cell rendered the literal "subscriber". Four names for one thing across three pages.

`fn tier_name(&SubscriptionTier)` is now `pub` and the canonical helper for the plan-name string. The Settings cell at the Account Information card and the marketing-facing pricing card both call it. Renaming a tier ("Standard" -> "Starter", whatever the product team picks) is now a one-line change in `tier_name` that updates every consumer. `checkout_success` also dropped its inline `match user.subscription_tier { ... }` block and now calls `tier_name(&user.subscription_tier)`, so the post-checkout confirmation lines up too.

`BUSINESS`'s first feature bullet went from "Everything in Starter" to "Everything in the personal plan" so the marketing copy stays accurate after a tier rename.

Finding 2 (cancel buttons for lifetime members): the Membership card body now branches on `lifetime` BEFORE the `has` check. Lifetime members see only "Plan: <tier_name>" and "Access: Lifetime - no billing" - no price field, no next-billing field, no "Cancel Membership" or "Cancel Now" buttons. The `has` boolean also explicitly excludes `lifetime` so a lifetime user with a stray Active row from the legacy billing flow can never accidentally see cancel UI (defense in depth).

Finding 9 (Membership/Billing redundancy): the `/billing` handler is now a 308 permanent redirect to `/membership`. The Membership page absorbs the invoices table from the retired Billing page; it always renders so a one-off charge or refund still surfaces (empty-state copy covers the common lifetime-member case). The page title, H1, and tab title rename to "Membership & Billing"; the sidebar nav drops the standalone "Billing" entry and renames the remaining entry to "Membership & Billing".

`axum::response::IntoResponse` was added to the dashboard.rs import set so `Redirect::permanent("/membership").into_response()` resolves.

See `docs/bunyip-upgrade/01-membership-plan-data.md` for the full spec and verification.
YousifShkara deleted branch feat/bunyip-upgrade-01-membership-plan 2026-06-10 07:57:07 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!97
No description provided.