fix(stripe): lock the real checkout price from the Stripe API (BUNYIP-215) #237

Merged
longjacksonle merged 1 commit from fix/bunyip-215-pricelock-line-items into main 2026-06-26 04:00:07 +02:00

What

Fixes BUNYIP-215: the price-lock feature stored placeholder data instead of the real purchased price.

Root cause

handle_checkout_completed locked the member's "price for life" by reading session["line_items"]["data"][0]["price"]["id"] and amount_total directly off the checkout.session.completed webhook payload. Stripe does not embed line_items in that payload unless they are explicitly expanded, so the price id always fell back to the placeholder "price_default" and the amount could fall back to a hardcoded 300. Every checkout locked junk; any later logic reading locked_price_id (re-pricing, plan migration, grandfathering) operated on a bogus id.

Fix

  • Add StripeService::get_checkout_session_price(session_id) (bunyip-domain), which retrieves the session with line_items and line_items.data.price expanded and returns the first line item's price id plus its amount (unit_amount, falling back to the line item's amount_total).
  • The checkout handler now calls it and locks the real price id and amount, threading the existing StripeService from the webhook entry point into handle_checkout_completed.
  • A transient Stripe API error propagates so Stripe retries the delivery; the BUNYIP-210 claim/finalize fence (already merged) makes that retry safe.
  • The silent "price_default" / 300 fallbacks are removed: a session with no resolvable line-item price (should not happen for a completed checkout) logs an error and leaves the price unlocked rather than persisting placeholder data, and still activates membership.

New StripeCheckoutPrice response struct added to crates/bunyip-domain/src/models/stripe.rs.

Testing

  • cargo clippy -p bunyip-domain -p bunyip-api --all-targets clean; cargo fmt --check clean.
  • Queries are async-stripe API calls plus runtime sqlx, so no .sqlx/ offline cache regen.
  • No automated regression test: bunyip-api has no DB-backed Rust handler harness and this path also needs a Stripe API stub; covered by the same test-infra follow-up flagged in BUNYIP-210.

Relates to BUNYIP-210.

## What Fixes **BUNYIP-215**: the price-lock feature stored placeholder data instead of the real purchased price. ## Root cause `handle_checkout_completed` locked the member's "price for life" by reading `session["line_items"]["data"][0]["price"]["id"]` and `amount_total` directly off the `checkout.session.completed` webhook payload. Stripe does not embed `line_items` in that payload unless they are explicitly expanded, so the price id always fell back to the placeholder `"price_default"` and the amount could fall back to a hardcoded `300`. Every checkout locked junk; any later logic reading `locked_price_id` (re-pricing, plan migration, grandfathering) operated on a bogus id. ## Fix - Add `StripeService::get_checkout_session_price(session_id)` (bunyip-domain), which retrieves the session with `line_items` and `line_items.data.price` expanded and returns the first line item's price id plus its amount (`unit_amount`, falling back to the line item's `amount_total`). - The checkout handler now calls it and locks the real price id and amount, threading the existing `StripeService` from the webhook entry point into `handle_checkout_completed`. - A transient Stripe API error propagates so Stripe retries the delivery; the BUNYIP-210 claim/finalize fence (already merged) makes that retry safe. - The silent `"price_default"` / `300` fallbacks are removed: a session with no resolvable line-item price (should not happen for a completed checkout) logs an error and leaves the price unlocked rather than persisting placeholder data, and still activates membership. New `StripeCheckoutPrice` response struct added to `crates/bunyip-domain/src/models/stripe.rs`. ## Testing - `cargo clippy -p bunyip-domain -p bunyip-api --all-targets` clean; `cargo fmt --check` clean. - Queries are async-stripe API calls plus runtime sqlx, so no `.sqlx/` offline cache regen. - No automated regression test: bunyip-api has no DB-backed Rust handler harness and this path also needs a Stripe API stub; covered by the same test-infra follow-up flagged in BUNYIP-210. Relates to BUNYIP-210.
fix(stripe): lock the real checkout price from the Stripe API (BUNYIP-215)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 1m5s
Check / fmt + clippy + build + tests (pull_request) Successful in 18m33s
Create release / Create release from merged PR (pull_request) Has been skipped
541c9adc83
`handle_checkout_completed` locked a member's "price for life" by reading `session["line_items"]["data"][0]["price"]["id"]` and `amount_total` off the `checkout.session.completed` webhook payload. But Stripe does not embed `line_items` in that payload unless they are explicitly expanded, so the price id always fell back to the placeholder `"price_default"` and the amount could fall back to a hardcoded `300`. Every checkout therefore locked junk instead of the real purchased price.

Add `StripeService::get_checkout_session_price`, which retrieves the session with `line_items` and `line_items.data.price` expanded and returns the first line item's price id plus its amount (`unit_amount`, falling back to the line item's `amount_total`). The checkout handler now calls it and locks the real price id and amount. A transient Stripe API error propagates so Stripe retries the delivery, which the BUNYIP-210 claim/finalize fence now makes safe.

The silent `"price_default"` and `300` fallbacks are gone: a session with no resolvable line-item price (should not happen for a genuinely completed checkout) logs an error and leaves the price unlocked rather than persisting placeholder data, and still activates membership.

No automated regression test: bunyip-api has no DB-backed Rust handler harness and this path also needs a Stripe API stub; covered by the same follow-up flagged in BUNYIP-210.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-06-26 03:42:07 +02:00
longjacksonle deleted branch fix/bunyip-215-pricelock-line-items 2026-06-26 04:00:08 +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!237
No description provided.