feat(admin): tier/pricing config cohesion - mapping home, checkout-trial, (none) clearing, per-tier visibility (BUNYIP-527) #525
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-527-tier-config-cohesion"
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
The BUNYIP-526 follow-ups (mapping location, checkout-trial reconciliation, "(none)" semantics), plus a per-tier visibility toggle requested during the work. Directions were confirmed with the ticket owner before building.
Changes
1. Catalog mapping moves back to the Pricing tiers page. Reverses the BUNYIP-417/524 consolidation:
/admin/tier-settingsnow owns slots, per-tier trials, the tier -> Stripe price mapping, per-tier visibility, and the publish switch + live status./admin/stripekeeps only raw Stripe (connection, products, prices, webhooks). The catalog form posts to the new/admin/tier-settings/catalogroute.2. Checkout trial relocated + relabelled (interim). The trial actually granted at checkout is a single global
stripe_config.trial_period_days; the per-tier*_trial_daysare only advertised on/pricing, not granted (create_checkout_sessiontakes no per-tier length - honouring the advertised per-tier trial needs a dunite-stripe change, tracked separately). Until then the field moves off the Stripe Checkout block onto the Pricing tiers page as Trial applied at checkout, labelled to say it is the one trial applied for every tier. The Pricing tiers save writes both backends (tier config + stripe config), validating up front so a bad field persists nothing.3. "(none)" actually clears a mapping. The price selects submit
""for "(none)" and the save always sends the price fields, so an empty one reaches the API as an explicit empty string.TierConfigRepository::updatereads the six Stripe id columns three-state (omittedkeeps,""clears to NULL, an id sets) viaCASEinstead ofCOALESCE, andupdate_tier_configclears a tier's derived product alongside a cleared price. Previously "(none)" was a silent no-op.4. Per-tier visibility (new, requested mid-work). Each tier gets a Show this tier on the pricing page toggle, backed by new
tier_config.{lifetime,early_adopter,standard}_visiblecolumns (migration20260812000010, NOT NULL default true). The/pricingresolve drops a hidden tier even when mapped, underneath the global publish switch; hiding is deliberate, so it produces no unpublished reason.Verification
just check-containergreen (fmt, clippy -D warnings, workspace tests, incl. the clear-body/visibility-toggle tests and the reworked publish-switch test).PricingTier/TierConfigResponsegain their fields with serde defaults (visibility defaults true), so a one-release skew never renders a tier as wrongly hidden.Out of scope (still tracked)
Making Stripe checkout honour the per-tier advertised trial (needs a dunite-stripe change to pass a trial-days override into
create_checkout_session).🤖 Generated with Claude Code
https://claude.ai/code/session_01GkNbvEq6awuMRULFCiYKe3
Follow-ups flagged out of BUNYIP-526, plus a per-tier visibility toggle requested during the work. Directions were confirmed with the ticket owner before implementing. Catalog mapping moves back to the Pricing tiers page. BUNYIP-417/524 had consolidated the tier -> Stripe price mapping (and the publish switch + live status) onto the Stripe page; this reverses that so the Pricing tiers page owns everything about bunyip's own tiers - slot limits, per-tier trial lengths, the price mapping, per-tier visibility, and the publish switch - while the Stripe page keeps only raw Stripe (connection, products, prices, webhooks). The catalog section is rendered from the Pricing tiers page and its form posts to the new /admin/tier-settings/catalog route (redirects back there). Checkout trial relocates and is relabelled (interim). The trial actually granted at checkout is a single global stripe_config.trial_period_days; the per-tier *_trial_days are only advertised on /pricing, not granted (create_checkout_session takes no per-tier length - honouring the advertised per-tier trial needs a dunite-stripe change, tracked separately). Until then the field moves off the Stripe Checkout block onto the Pricing tiers page as "Trial applied at checkout", labelled to say it is the one trial applied for every tier versus the per-tier advertised lengths above it. The Pricing tiers save now writes both backends (tier config for slots/trials, stripe config for the checkout trial), validating everything up front so a bad field persists nothing. "(none)" now actually clears a mapping. The catalog price selects submit "" for "(none)", and the save always sends the price fields, so an empty one reaches the API as an explicit empty string. TierConfigRepository::update reads the six Stripe id columns as three-state - omitted keeps, "" clears to NULL, an id sets - via CASE instead of COALESCE, and update_tier_config clears a tier's derived product alongside a cleared price. Previously "(none)" was a silent no-op that kept the old mapping. Per-tier visibility (new). Each tier gets a "Show this tier on the pricing page" toggle in its catalog card, backed by new tier_config.{lifetime,early_adopter,standard}_visible columns (migration 20260812000010, NOT NULL default true so existing mapped tiers keep showing). The public /pricing resolve drops a hidden tier even when it maps to a usable price, underneath the global publish switch; hiding a tier is a deliberate state, so it produces no unpublished reason. just check-container is green (fmt, clippy, workspace tests); the migration-immutability and serde-compat guards pass. The web PricingTier/TierConfigResponse gain their new fields with serde defaults (visibility defaults true) so a one-release skew never renders a tier as wrongly hidden or sold out. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkNbvEq6awuMRULFCiYKe3