Rename the subscription vocabulary to membership #486

Merged
nrupard merged 3 commits from refactor/BUNYIP-488-membership-vocabulary into main 2026-08-07 19:25:51 +02:00
Owner

Closes BUNYIP-488. Depends on DUNITE-7 for the final state (one line, noted below).

PSA Systems sells a membership, not a subscription. "Subscription" came from the original Stripe-shaped migration, and the partial rename in 20c6c62 converted only membership_status, so subscription_tier and subscription_override_by kept flowing Stripe's noun through the JSON API into the web tier.

Migration

One new migration renames the three users columns and the index. Rename rather than recreate: committed migrations are immutable (BUNYIP-293) and the tier string values (lifetime, free, early_adopter, standard) are unchanged, so no data moves and deployed databases survive.

Verified by replaying the full migration history against a throwaway postgres 18.2 container: all migrations applied cleanly, and the resulting users table has membership_override_by / membership_status / membership_tier with no subscription-prefixed column and the index renamed to idx_users_membership_status.

Pre-checked for stale references the rename could not follow: no views, functions, or stored policies reference these columns (the sole policy keys on user_id), and .sqlx/ has zero hits, so no offline-cache regeneration is needed.

Classified sweep

Site Disposition
users.subscription_status / _tier / _override_by, idx_users_subscription_status Renamed in a new migration
#[sqlx(rename)] / #[serde(rename)] on membership_status Removed, now redundant
subscription_tier field, subscription_tier_enum, assign_/reset_/upgrade_/admin_set_subscription_tier Renamed to the membership_* forms
subscription_tier JSON field (api <-> web) Renamed, both sides in this commit
Copy: "Finalizing your subscription", pricing and story prose Renamed to membership
Doc comments and the seed error string "unknown subscription tier" Renamed to membership
SubscriptionTier type Aliased to MembershipTier pending DUNITE-7
stripe_subscription_id, customer.subscription.*, handle_subscription_{created,updated,deleted}, create_free_subscription, get_customer_subscription, cancel_subscription, reactivate_subscription, sweep_stripe_subscriptions, Stripe*Subscription* types, admin-console Stripe copy Retained: these name Stripe objects, and maybe_create_lifetime_subscription literally creates a $0 Stripe subscription
EventSource / event-bus "subscriptions" Retained: pub/sub, a different domain sense
Migration filenames in billing-m1-gap-matrix.md Retained: immutable history

Business tier removal

A single instance has one tier axis. Business versus personal is an instance boundary (PSA Systems versus a8n Tools), not a tier. The backing column was already dropped in 20260327000032, but a Business pricing card, its BUSINESS feature array, and BUNYIP_SHOW_BUSINESS_PRICING survived, advertising a tier with no enum variant, no column, and no Stripe price.

The flag defaulted to false in the code and in compose.yml, compose.dev.yml, and .env.example, so this changes nothing visible in production. PERSONAL is renamed STANDARD_FEATURES. Prose about the business layer Bunyip provides is product positioning and stays.

Follow-up in scope on the issue

After DUNITE-7 merges: bump the dunite rev in the three crates/*/Cargo.toml files, re-run the member-scoped cargo update, and delete the temporary MembershipTier alias in crates/bunyip-domain/src/models/user.rs.

Checks

just check-container green (fmt, clippy -D warnings, 186 tests). scripts/check-migration-immutability.sh green: no committed migration modified, renamed, or deleted.

Closes BUNYIP-488. Depends on DUNITE-7 for the final state (one line, noted below). PSA Systems sells a **membership**, not a subscription. "Subscription" came from the original Stripe-shaped migration, and the partial rename in `20c6c62` converted only `membership_status`, so `subscription_tier` and `subscription_override_by` kept flowing Stripe's noun through the JSON API into the web tier. ## Migration One new migration renames the three `users` columns and the index. Rename rather than recreate: committed migrations are immutable (BUNYIP-293) and the tier string values (`lifetime`, `free`, `early_adopter`, `standard`) are unchanged, so no data moves and deployed databases survive. Verified by replaying the full migration history against a throwaway postgres 18.2 container: all migrations applied cleanly, and the resulting `users` table has `membership_override_by` / `membership_status` / `membership_tier` with no `subscription`-prefixed column and the index renamed to `idx_users_membership_status`. Pre-checked for stale references the rename could not follow: no views, functions, or stored policies reference these columns (the sole policy keys on `user_id`), and `.sqlx/` has zero hits, so no offline-cache regeneration is needed. ## Classified sweep | Site | Disposition | |---|---| | `users.subscription_status` / `_tier` / `_override_by`, `idx_users_subscription_status` | **Renamed** in a new migration | | `#[sqlx(rename)]` / `#[serde(rename)]` on `membership_status` | **Removed**, now redundant | | `subscription_tier` field, `subscription_tier_enum`, `assign_/reset_/upgrade_/admin_set_subscription_tier` | **Renamed** to the `membership_*` forms | | `subscription_tier` JSON field (api <-> web) | **Renamed**, both sides in this commit | | Copy: "Finalizing your subscription", pricing and story prose | **Renamed** to membership | | Doc comments and the seed error string "unknown subscription tier" | **Renamed** to membership | | `SubscriptionTier` type | **Aliased** to `MembershipTier` pending DUNITE-7 | | `stripe_subscription_id`, `customer.subscription.*`, `handle_subscription_{created,updated,deleted}`, `create_free_subscription`, `get_customer_subscription`, `cancel_subscription`, `reactivate_subscription`, `sweep_stripe_subscriptions`, `Stripe*Subscription*` types, admin-console Stripe copy | **Retained**: these name Stripe objects, and `maybe_create_lifetime_subscription` literally creates a $0 Stripe subscription | | `EventSource` / event-bus "subscriptions" | **Retained**: pub/sub, a different domain sense | | Migration filenames in `billing-m1-gap-matrix.md` | **Retained**: immutable history | ## Business tier removal A single instance has one tier axis. Business versus personal is an instance boundary (PSA Systems versus a8n Tools), not a tier. The backing column was already dropped in `20260327000032`, but a Business pricing card, its `BUSINESS` feature array, and `BUNYIP_SHOW_BUSINESS_PRICING` survived, advertising a tier with no enum variant, no column, and no Stripe price. The flag defaulted to `false` in the code and in `compose.yml`, `compose.dev.yml`, and `.env.example`, so this changes nothing visible in production. `PERSONAL` is renamed `STANDARD_FEATURES`. Prose about the business *layer* Bunyip provides is product positioning and stays. ## Follow-up in scope on the issue After DUNITE-7 merges: bump the dunite `rev` in the three `crates/*/Cargo.toml` files, re-run the member-scoped `cargo update`, and delete the temporary `MembershipTier` alias in `crates/bunyip-domain/src/models/user.rs`. ## Checks `just check-container` green (fmt, clippy `-D warnings`, 186 tests). `scripts/check-migration-immutability.sh` green: no committed migration modified, renamed, or deleted.
refactor: rename the subscription vocabulary to membership
All checks were successful
E2E PR gate / E2E / PR gate (pull_request) Successful in 23s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m37s
2f21e78883
PSA Systems sells a membership, not a subscription. "Subscription" entered the schema from the original Stripe-shaped migration and a partial rename in 20c6c62 converted only membership_status, leaving subscription_tier and subscription_override_by speaking Stripe's noun all the way through the JSON API into the web tier.

A new migration renames users.subscription_status, subscription_tier, and subscription_override_by plus the idx_users_subscription_status index. Rename rather than recreate: committed migrations are immutable (BUNYIP-293) and the tier string values are unchanged, so no data moves and deployed databases survive. Verified by replaying the full migration history against a throwaway postgres 18.2 and asserting the resulting users table carries no subscription-prefixed column.

The redundant sqlx/serde rename shims on membership_status are gone now that the column and field agree. MembershipTier is bridged with a temporary alias to dunite's SubscriptionTier until DUNITE-7 lands and the pinned rev is bumped.

Stripe's noun is retained where it names Stripe objects: stripe_subscription_id, the customer.subscription.* events, handle_subscription_{created,updated,deleted}, the StripeService methods, and the admin-console copy describing Stripe products and prices. SSE event-bus "subscriptions" are also untouched.

Also removes the dangling Business tier. A single instance has one tier axis; business versus personal is an instance boundary (PSA Systems versus a8n Tools), not a tier. The backing column was dropped in 20260327000032, but a Business pricing card, its BUSINESS feature array, and the BUNYIP_SHOW_BUSINESS_PRICING flag survived, advertising a tier with no enum variant, no column, and no Stripe price. The flag defaulted to false in code and in every compose file, so removing it changes nothing visible in production. The PERSONAL feature array is renamed STANDARD_FEATURES.

#BUNYIP-488

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge main into refactor/BUNYIP-488-membership-vocabulary
Some checks failed
Check / fmt + clippy + build + tests (pull_request) Failing after 7s
E2E / PR gate (pull_request) Successful in 29s
0ef8ae694a
Resolve conflicts from 37 commits of main. main is the newer base: it already removed the business pricing tier and de-fictionalized the marketing copy (BUNYIP-487), refactored the pricing page to a data-driven pricing_content(&PricingResponse), and moved the event bus to the dunite-events alias (DEV-528). Kept main's structure in event_bus.rs, content.rs, public.rs, and .env.example, and re-applied this branch's subscription->membership vocabulary rename on top. Fixed the auto-merge breakage where main-added code still used the pre-rename names: SubscriptionTier -> MembershipTier and the subscription_tier field -> membership_tier in bunyip-web api/types.rs, views/layout.rs, and handlers/content.rs, plus the doc-comment vocabulary in bunyip-api handlers/pricing.rs. fmt + clippy (-D warnings, all targets) green.

#BUNYIP-488
nrupard scheduled this pull request to auto merge when all checks succeed 2026-08-07 19:06:53 +02:00
fix(migrations): move the membership rename off the colliding version
All checks were successful
E2E / PR gate (pull_request) Successful in 15s
Check / fmt + clippy + build + tests (pull_request) Successful in 13m9s
Create release / Create release from merged PR (pull_request) Has been skipped
705a45df1e
The merge from main brought in 20260805000010_add_tier_config_pricing_enabled (already on main, immutable), which collides with this branch's 20260805000010_rename_subscription_to_membership. sqlx would treat two files at one version as a single migration, so CI's version check fails. Renamed the branch-local, not-yet-applied rename migration to 20260805000020. Content is byte-identical (a file rename), so no data behaviour changes; scripts/check-migration-versions.nu now passes.

#BUNYIP-488
nrupard deleted branch refactor/BUNYIP-488-membership-vocabulary 2026-08-07 19:25:52 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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!486
No description provided.