feat(pricing): drive /pricing from the admin Pricing tiers page #487
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-487-admin-driven-pricing"
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?
The public /pricing page and the admin tier configuration were two unrelated sources of truth, and the public one was hardcoded: the amount was the literal "$3", the hero said "Start free for 14 days" while
tier_config.standard_trial_daysdefaults to 30, and a second "Business" card behindBUNYIP_SHOW_BUSINESS_PRICINGadvertised a tier that exists nowhere inSubscriptionTierortier_config. The page also advertised organizations, which the product does not have: there is no orgs table and no migration creates one.The admin page becomes the single source and the public page a read-only projection of it. A new migration adds
tier_config.pricing_enabled boolean not null default false, carried throughTierConfig,TierConfigRepository::update,UpdateTierConfigRequest, and the tier-config response (asTierConfigWithPricing, a flattened superset of the dunite-sharedTierConfigResponse, so the shared shape keeps one definition).has_db_overridescounts the new column, otherwise enabling pricing and changing nothing else would fall back to env on the next restart and silently unpublish the page. The admin page is relabelled "Pricing tiers" in its heading, browser title, and nav entry; the route stays /admin/tier-settings so existing bookmarks keep working.A new public
GET /v1/pricingreturns the switch, the standard trial length, and the tiers that resolve to a usable Stripe price, with each amount read from the pricetier_config.standard_price_idmaps to. The advertised price is therefore incapable of disagreeing with the charged price. The payload is cached behind a TTL slot so a public page cannot generate a Stripe call per visit, andupdate_tier_configinvalidates it, so changing the mapped price id in the admin panel (from either the Pricing tiers page or the Stripe catalog form, which posts to the same endpoint) changes the public page with no redeploy and no TTL wait. The route is deliberately absent fromrate_limit_floor::EXEMPT_PATHS, so the default per-IP cap applies./pricing returns 404 when the switch is off or when no tier resolves, and the nav link, footer link, and homepage "See pricing" button are gated on exactly the same condition, so no rendered page links to a 404. The trial length in the pricing hero, the homepage hero chip, and the homepage CTA all come from
standard_trial_days.Removals: the
BUSINESSconst, the second pricing card and itsshow_businessbranch, theshow_business_pricingconfig field, its env read, the compose and .env.example entries, and the three test fixtures that set it. The org copy is removed or rewritten in the Personal bullet list, the Standard tagline, both Our Story sentences, and the homepage feature card (now "Membership and entitlements", which the product actually does). The roadmap entries keep their wording, and the IP-enrichmentorganizationfield (the ASN owner) is untouched. A source-scanning guard insecurity.rsfails the build if any removed string reappears, and render-level tests assert the pricing page and homepage feature cards carry no org claim.#BUNYIP-487