feat(tiers): verify early-adopter/standard trials + add members-by-tier admin view #314
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-291-membership-tiers"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
BUNYIP-291: verify the early-adopter (90-day) vs standard (30-day) trial assignment and the no-second-trial-on-resubscribe behavior, merge the duplicate cancel control, and add the net-new admin members-by-tier view.
AC1 - trial assignment reads settings, not constants (verified + tested)
The signup grant already read
early_adopter_slots/early_adopter_trial_days/standard_trial_daysfrom the admin tier settings, not a hardcoded constant. To lock and test the 90-vs-30 boundary without a database, the inline logic is extracted into pure methods onSubscriptionTier:select(lifetime slots first, then early-adopter, then standard; exclusive boundary) andtrial_days(windows from settings;Nonefor lifetime/free). Both call sites (AuthService::maybe_grant_initial_tierandUserRepository::assign_subscription_tier) now use them, so behavior is unchanged and single-sourced. Unit tests pin the boundary and that lengths flow from config.AC2 - trial labeled at signup
The
InitialTierGrantedaudit entry now recordstrial_daysand a humantrial_label("Early Adopter - 90-day trial" / "Standard - 30-day trial"). The membership page shows a tier-labeled trial banner instead of only a generic "Trial" badge.AC3 - single cancel control
The page rendered two sibling cancel buttons ("Cancel Membership" at period end, "Cancel Now" immediate). Per product decision they are merged into one "Cancel Membership" disclosure that offers both modes as a labeled choice ("Cancel at period end - keep access until " / "Cancel immediately - lose access now"), reusing the existing
/membership/canceland/membership/cancel-nowhandlers. One visible cancel control; underlying behavior unchanged.AC3 (resubscribe) / AC5 - no second free trial
Verified: checkout eligibility is
!has_used_trial, andhas_used_trialis set by thecheckout.session.completedwebhook once a trial session finalizes, so a cancel + resubscribe bills immediately. Extracted asUser::trial_eligible()(computed before the customer lookup partially moves the user) with a unit test covering first-timer vs returning member.AC4 - members-by-tier admin view (net-new)
GET /v1/admin/membershipsgains an optionaltierfilter (early_adopter / standard / lifetime / free) taking precedence overstatus, orderedcreated_at ASCso early-adopter slot holders read in claim order (occupancy is referenceable via the existing tier column; no change to grant logic needed). Query held as a testable const.Tests
cargo fmt --all --check,cargo clippy --workspace --all-targets(clean), andcargo test --workspace --liball pass. New unit tests: slot boundary, trial-days-from-settings, trial label, resubscribe-no-new-trial, and the members-by-tier SQL (tier predicate + soft-delete exclusion + claim-order sort).Closes BUNYIP-291.
BUNYIP-291 AC2. Two places now name which trial an account received rather than leaving it implicit: - The InitialTierGranted audit entry records `trial_days` and a human `trial_label` ("Early Adopter - 90-day trial" / "Standard - 30-day trial") alongside the tier, so the grant is auditable at signup time. - The membership page shows a tier-labeled trial banner ("Early Adopter trial - N days remaining" / "Standard trial - ...") instead of only the generic "Trial" badge, so the member can see which trial they hold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>BUNYIP-291 AC3. The subscriptions page rendered two sibling cancel buttons ("Cancel Membership" = at period end, "Cancel Now" = immediate). Replace them with a single "Cancel Membership" disclosure that, when opened, offers the two distinct modes as a labeled choice: "Cancel at period end - keep access until <date>" and "Cancel immediately - lose access now". Both reuse the existing /membership/cancel and /membership/cancel-now handlers, so the underlying behavior is unchanged; the page now presents exactly one visible cancel control. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>BUNYIP-291 AC4 (frontend). The admin Memberships page gains tier filter tabs (All / Early Adopter / Standard / Lifetime) that drive the new API `tier` filter; unknown tier slugs fall back to the unfiltered list. When viewing the Early Adopter tab it fetches the tier config and shows an occupancy banner ("N of M early-adopter slots filled", plus an "All slots filled" badge when the pool is full), so an admin can see whether every slot is taken and, in the list below (ordered by claim order server-side), exactly who holds them. The pager carries the active tier so paging stays within the filter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>