feat(admin): derive a tier's Stripe product from its mapped price (BUNYIP-517) #516
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-517-derive-tier-product-from-price"
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 and why
Raised as a question about the Tier catalog mapping on
/admin/stripe: creating a price already associates it with a product, so why ask for the association again? The block is not a product-to-price association (Stripe knowsprice.product). It binds a bunyip membership tier to the Stripe object that represents it, and the binding is read both ways: forwards (advertise pricing on/pricing, open the $0 subscription behind a free/lifetime grant) and backwards (resolve_tier_for_productclassifies incomingcustomer.subscription.*events onto a tier). One binding per tier is genuinely needed. The redundancy was narrower: the form asked for both a price id and a product id, when the price already determines the product, and the two could disagree with nothing checking.What changed
list_prices) and writes the price's product id to the existing*_product_idcolumn.resolve_tier_for_productkeeps reading the stored product, so no Stripe call is added to the webhook path.Confirmed assumption + the lifetime_price_id column
Free and lifetime share the one $0
free_price_id(both grant paths open a $0 subscription on it vialive_free_price_id). Per that decision: the free / lifetime naming is unified between the catalog form and the Pricing tiers resolved-pricing block, and the unusedlifetime_price_idcolumn (added in20260429000045, never read by any resolver or written by any form) is dropped by a new migration, never by editing the committed one.TierConfigRowdrops the field, and the BUNYIP-512 archive tier resolution now matches lifetime by its derived product instead of a dedicated price column.Screenshot
The rendered catalog: price selects only, derived product read-only per tier, and the disagreement flag (on Standard) naming the stale stored product. Top block = mapped tiers, bottom = unmapped (
(none), "derived from the price on save"). Attached in the conversation.Tests
derive_product_for_price_returns_the_active_prices_product,..._refuses_an_archived_price_by_name,..._refuses_an_invisible_price_naming_the_app_tag.catalog_section_asks_for_price_only_and_shows_derived_product(price selects present, product-id inputs gone, derived shown) andcatalog_section_flags_a_stored_product_that_disagrees_with_the_price.resolve_tier_for_product_matches_the_stored_product_idslocks the webhook classifier as unchanged.just check-containergreen (fmt + clippy + full workspace tests). No user-facing doc described the six-field mapping (only a design gap-matrix that references file line ranges), so none needed updating.Note on the open Stripe-page PRs
This is off
main. It touches the catalog section, which the in-flight #514 (BUNYIP-516) and #515 (BUNYIP-513) do not, but it shares thestripe()page loader with them (one catalog-call line). A small conflict on that line is likely once those merge; happy to rebase when they land.🤖 Generated with Claude Code
https://claude.ai/code/session_01GkNbvEq6awuMRULFCiYKe3