fix(pricing): publish every mapped tier and name every silent cause #512

Merged
Claude-Run merged 1 commit from fix/BUNYIP-515-pricing-multi-tier-and-visible-reasons into main 2026-08-11 15:28:13 +02:00
Member

/pricing published only the standard tier: resolve read cfg.standard_price_id and nothing else, so an admin who had mapped the lifetime or early-adopter price had "configured pricing" by every visible signal and still got a 404. It now iterates all three mapped tiers (lifetime, early adopter, standard, in that order), emitting a card for each id that resolves to an active price with an amount. enabled stays !tiers.is_empty(), so a fully unmapped install still 404s and the public body shape is unchanged.

Five different failures used to collapse into the same silence, only one of which logged anything. Each branch now produces an UnpublishedReason (switch off, no tier mapped, Stripe not configured, Stripe list failed, and a per-tier price-unresolved carrying archived / no-amount / not-visible-under-app-tag), logs itself once at info for the deliberate states and warn for the misconfigured ones, and is served by the new admin-only GET /v1/admin/pricing/status. That endpoint is built from the same resolve the public page runs and bypasses the cache, so the diagnosis cannot drift from the behaviour. The Pricing tiers admin page renders it above the form: a success box naming the published tiers, or one error box per reason, including the app-tag case that makes a dashboard-created product's price invisible to list_prices.

The web tier stopped discarding its errors. calls::pricing also decoded the wrong shape: /v1/pricing answers with a bare body rather than the {success, data, meta} envelope, so the envelope-aware parse failed every decode and public_ctx's unwrap_or_default turned that into "pricing is unpublished" with nothing logged anywhere. parse_bare reads the documented public shape, and public_ctx, content::pricing, the docs pages, the onboarding gate and the Pricing tiers fetches all log before falling back, with the reason the fallback is correct stated at each site.

Cache invalidation now covers every admin action that can change the answer: update_stripe_config (the secret key and the app tag both decide which prices are visible), create_stripe_price and archive_stripe_price, alongside the existing update_tier_config. A source-scanning test in admin_stripe.rs fails the build if a future price-mutating handler (the BUNYIP-511 replace endpoint, say) skips the invalidate.

#BUNYIP-515

/pricing published only the standard tier: `resolve` read `cfg.standard_price_id` and nothing else, so an admin who had mapped the lifetime or early-adopter price had "configured pricing" by every visible signal and still got a 404. It now iterates all three mapped tiers (lifetime, early adopter, standard, in that order), emitting a card for each id that resolves to an active price with an amount. `enabled` stays `!tiers.is_empty()`, so a fully unmapped install still 404s and the public body shape is unchanged. Five different failures used to collapse into the same silence, only one of which logged anything. Each branch now produces an `UnpublishedReason` (switch off, no tier mapped, Stripe not configured, Stripe list failed, and a per-tier price-unresolved carrying archived / no-amount / not-visible-under-app-tag), logs itself once at info for the deliberate states and warn for the misconfigured ones, and is served by the new admin-only `GET /v1/admin/pricing/status`. That endpoint is built from the same resolve the public page runs and bypasses the cache, so the diagnosis cannot drift from the behaviour. The Pricing tiers admin page renders it above the form: a success box naming the published tiers, or one error box per reason, including the app-tag case that makes a dashboard-created product's price invisible to `list_prices`. The web tier stopped discarding its errors. `calls::pricing` also decoded the wrong shape: `/v1/pricing` answers with a bare body rather than the `{success, data, meta}` envelope, so the envelope-aware `parse` failed every decode and `public_ctx`'s `unwrap_or_default` turned that into "pricing is unpublished" with nothing logged anywhere. `parse_bare` reads the documented public shape, and `public_ctx`, `content::pricing`, the docs pages, the onboarding gate and the Pricing tiers fetches all log before falling back, with the reason the fallback is correct stated at each site. Cache invalidation now covers every admin action that can change the answer: `update_stripe_config` (the secret key and the app tag both decide which prices are visible), `create_stripe_price` and `archive_stripe_price`, alongside the existing `update_tier_config`. A source-scanning test in `admin_stripe.rs` fails the build if a future price-mutating handler (the BUNYIP-511 replace endpoint, say) skips the invalidate. #BUNYIP-515
fix(pricing): publish every mapped tier and name every silent cause
All checks were successful
E2E / PR gate (pull_request) Successful in 22s
Check / fmt + clippy + build + tests (pull_request) Successful in 14m52s
Create release / Create release from merged PR (pull_request) Has been skipped
0a30bcf83b
/pricing published only the standard tier: `resolve` read `cfg.standard_price_id` and nothing else, so an admin who had mapped the lifetime or early-adopter price had "configured pricing" by every visible signal and still got a 404. It now iterates all three mapped tiers (lifetime, early adopter, standard, in that order), emitting a card for each id that resolves to an active price with an amount. `enabled` stays `!tiers.is_empty()`, so a fully unmapped install still 404s and the public body shape is unchanged.

Five different failures used to collapse into the same silence, only one of which logged anything. Each branch now produces an `UnpublishedReason` (switch off, no tier mapped, Stripe not configured, Stripe list failed, and a per-tier price-unresolved carrying archived / no-amount / not-visible-under-app-tag), logs itself once at info for the deliberate states and warn for the misconfigured ones, and is served by the new admin-only `GET /v1/admin/pricing/status`. That endpoint is built from the same resolve the public page runs and bypasses the cache, so the diagnosis cannot drift from the behaviour. The Pricing tiers admin page renders it above the form: a success box naming the published tiers, or one error box per reason, including the app-tag case that makes a dashboard-created product's price invisible to `list_prices`.

The web tier stopped discarding its errors. `calls::pricing` also decoded the wrong shape: `/v1/pricing` answers with a bare body rather than the `{success, data, meta}` envelope, so the envelope-aware `parse` failed every decode and `public_ctx`'s `unwrap_or_default` turned that into "pricing is unpublished" with nothing logged anywhere. `parse_bare` reads the documented public shape, and `public_ctx`, `content::pricing`, the docs pages, the onboarding gate and the Pricing tiers fetches all log before falling back, with the reason the fallback is correct stated at each site.

Cache invalidation now covers every admin action that can change the answer: `update_stripe_config` (the secret key and the app tag both decide which prices are visible), `create_stripe_price` and `archive_stripe_price`, alongside the existing `update_tier_config`. A source-scanning test in `admin_stripe.rs` fails the build if a future price-mutating handler (the BUNYIP-511 replace endpoint, say) skips the invalidate.

#BUNYIP-515
Claude-Run deleted branch fix/BUNYIP-515-pricing-multi-tier-and-visible-reasons 2026-08-11 15:28:13 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!512
No description provided.