fix(pricing): show each tier's own trial and reflect slot availability on /pricing (BUNYIP-526) #522
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-526-per-tier-trial-and-availability"
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
Fixes the public
/pricingpage, which misrepresented the tiers using data the app already stored.The bug
Changes
trial_daysthatGET /v1/pricingalready returned. Paid tiers state their own trial ("Free 90-day trial, then price locked for life"); Lifetime never says "trial". The subhead drops the number ("No credit card required.").PublicPricingTiergainsavailable+slots_remaining, computed from the slot caps in tier config and the livecount_tier_assignmentsusage. Standard is uncapped (always available, no count); Lifetime / Early Adopter sell out once usage reaches the cap (remaining clamped to zero when oversold). The usage rides inside the cached resolve (same TTL as prices); a count failure degrades to "available" rather than 500ing the public page.PricingTiergains the two fields with serde defaults (availabledefaultstrue, so an older API never renders a tier as wrongly sold out); the wire-compat guard passes.Out of scope (flagged on the ticket for separate decisions)
Verification
just check-containergreen (fmt, clippy -D warnings, 250 web + API pricing tests: per-tier trial rendering, Lifetime's no-trial wording, sold-out CTA, scarcity line, and the availability computationused >= cap => unavailable, Standard unlimited).🤖 Generated with Claude Code
https://claude.ai/code/session_01GkNbvEq6awuMRULFCiYKe3
The public /pricing page misrepresented the tiers with data the app already had. The subhead read "Start free for 75 days" (the Standard tier's trial) above all three cards as if it were global: wrong for Early Adopter, which has its own trial length, and meaningless for Lifetime, a $0 price-locked membership with no trial. And slot-limited tiers (Lifetime, Early Adopter) that were sold out still showed a normal sign-up CTA, inviting a purchase that could not be honoured. Per-tier trial. GET /v1/pricing already returns each tier's own trial_days; the card now renders it instead of one global number. Each paid tier states its own trial ("Free 90-day trial, then price locked for life"). Lifetime is a price-locked membership, not a trial, so its line never says "trial". The subhead drops the single number and reads "No credit card required." Availability from slots. PublicPricingTier gains available + slots_remaining, computed from the slot caps in tier config and the live count_tier_assignments usage (Standard is uncapped, so always available with no count; Lifetime and Early Adopter sell out once usage reaches the cap, clamping remaining to zero when oversold). public_pricing and admin_pricing_status now read that usage; the count rides inside the cached resolve, so it refreshes on the same TTL as the prices and a count failure degrades to "available" rather than 500ing the public page. A sold-out limited tier renders a disabled "Sold out" CTA instead of a sign-up link; an available limited tier with slots left shows an honest "Only N spots left" line; Standard always shows a live CTA. Rule chosen: show-as-sold-out, not hide, so the tier ladder does not silently change shape. The web PricingTier gains the two fields with serde defaults (available defaults true so an older API never renders a tier as wrongly sold out); the wire-compat guard passes. Out of scope, flagged on the ticket for separate decisions: moving the tier -> Stripe catalog mapping back to the Pricing tiers page (it was deliberately consolidated onto the Stripe page in BUNYIP-417/524), reconciling the Stripe Checkout "Trial period (days)" field with the per-tier trial days (needs the checkout flow verified first), and the catalog "(none)" price option's semantics. just check-container is green (fmt, clippy, 250 web + api pricing tests, including per-tier trial rendering, Lifetime's no-trial wording, the sold-out CTA, the scarcity line, and the availability computation). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkNbvEq6awuMRULFCiYKe3