Checkout reads the price's Stripe mapping a second time and repeats checks the offer decision has already passed #203

Open
opened 2026-10-09 16:02:11 +00:00 by cgalo5758 · 0 comments
Owner

What a contributor runs into

Checkout (HandleCheckout, internal/server/billing.go) reads the posted price's Stripe price mapping twice in one request.

The first read belongs to the offer decision, the function that decides what the Products page offers and what Checkout sells (plans.Offer and plans.OffLadder, internal/plans/offer.go). checkoutOffer builds the decision's input with postedPrice (internal/server/plan_offer.go), which reads the product's prices and the default price's mapping through computePriceReadiness. The price counts as purchasable only when it is the product's default and its mapping carries a Stripe id the current key can reach (readinessPurchasable). Checkout goes on only when the decision allows it. So every request that gets past this point names the default price, and that price has a mapping with a reachable Stripe id.

The second read is checkoutPriceMapping, which calls GetPriceMappingByPriceID for the same price to get the Stripe id. It answers 422 in three cases: there is no row, the row has no Stripe id, or MappingReachable says the key cannot reach it. The first read has just checked the same three conditions, on the same row and with the same key mode. A request reaches these refusals only when the mapping row changes between the two reads. The comments above the refusal still describe it as the check that keeps an unreachable price away from Stripe.

TestCheckoutRefusesPriceOutsideTheKeyEnvironment (internal/server/billing_checkout_environment_db_test.go) looks like a test of those refusals. In fact the offer decision answers each 422 it checks, before checkoutPriceMapping runs.

Why it costs

The rule for a purchasable price is written twice: in readinessPurchasable and in the conditions of checkoutPriceMapping. Tests reach only the first. A change to the rule has to be made in both places, and a change made only in the second has no effect that any test can see.

Every Checkout request makes one extra query, and the second read does not close the race it can catch. The customer mapping read, and a possible customer create at Stripe, already come after it and before the call that creates the Checkout Session. A mapping that changes in that time still reaches Stripe.

Where

internal/server/billing.go (checkoutPriceMapping, HandleCheckout); internal/server/plan_offer.go (postedPrice); internal/server/product_readiness.go (computePriceReadiness).

Done when

  • Checkout reads the posted price's mapping once, in the offer decision's read, and creates the Checkout Session with the Stripe id from that row.
  • Nothing after the decision repeats a condition the decision has already checked.
  • The rule for a purchasable price is written only in readinessPurchasable.
## What a contributor runs into Checkout (`HandleCheckout`, `internal/server/billing.go`) reads the posted price's Stripe price mapping twice in one request. The first read belongs to the offer decision, the function that decides what the Products page offers and what Checkout sells (`plans.Offer` and `plans.OffLadder`, `internal/plans/offer.go`). `checkoutOffer` builds the decision's input with `postedPrice` (`internal/server/plan_offer.go`), which reads the product's prices and the default price's mapping through `computePriceReadiness`. The price counts as purchasable only when it is the product's default and its mapping carries a Stripe id the current key can reach (`readinessPurchasable`). Checkout goes on only when the decision allows it. So every request that gets past this point names the default price, and that price has a mapping with a reachable Stripe id. The second read is `checkoutPriceMapping`, which calls `GetPriceMappingByPriceID` for the same price to get the Stripe id. It answers 422 in three cases: there is no row, the row has no Stripe id, or `MappingReachable` says the key cannot reach it. The first read has just checked the same three conditions, on the same row and with the same key mode. A request reaches these refusals only when the mapping row changes between the two reads. The comments above the refusal still describe it as the check that keeps an unreachable price away from Stripe. `TestCheckoutRefusesPriceOutsideTheKeyEnvironment` (`internal/server/billing_checkout_environment_db_test.go`) looks like a test of those refusals. In fact the offer decision answers each 422 it checks, before `checkoutPriceMapping` runs. ## Why it costs The rule for a purchasable price is written twice: in `readinessPurchasable` and in the conditions of `checkoutPriceMapping`. Tests reach only the first. A change to the rule has to be made in both places, and a change made only in the second has no effect that any test can see. Every Checkout request makes one extra query, and the second read does not close the race it can catch. The customer mapping read, and a possible customer create at Stripe, already come after it and before the call that creates the Checkout Session. A mapping that changes in that time still reaches Stripe. ## Where `internal/server/billing.go` (`checkoutPriceMapping`, `HandleCheckout`); `internal/server/plan_offer.go` (`postedPrice`); `internal/server/product_readiness.go` (`computePriceReadiness`). ## Done when - Checkout reads the posted price's mapping once, in the offer decision's read, and creates the Checkout Session with the Stripe id from that row. - Nothing after the decision repeats a condition the decision has already checked. - The rule for a purchasable price is written only in `readinessPurchasable`.
cgalo5758 added the
kind
debt
area/billing
labels 2026-10-09 16:02:11 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wiki-cafe/member-console#203