diff --git a/src/models/orders.rs b/src/models/orders.rs index d934e16..96fe261 100644 --- a/src/models/orders.rs +++ b/src/models/orders.rs @@ -54,6 +54,23 @@ pub async fn place( details: Checkout, user: Option<&users::Model>, ) -> Result { + // Resolve the price of every line *before* opening the transaction. Pricing + // loads its context from the connection pool; doing it while the order + // transaction holds a connection would acquire a second one and can exhaust + // the pool (it times out under contention or a small pool). Prices are stable + // within a request, so this snapshot is what we charge; stock is still + // re-validated against the transaction below. + let line_variants = product_variants::Entity::find() + .filter(product_variants::Column::Id.is_in(items.iter().map(|(id, _)| *id))) + .all(&ctx.db) + .await?; + let priced = pricing::price_variants(ctx, &line_variants, user).await?; + let price_by_variant: std::collections::HashMap = line_variants + .iter() + .zip(priced) + .map(|(v, p)| (v.id, p.price_cents)) + .collect(); + let txn = ctx.db.begin().await?; let mut subtotal: i64 = 0; @@ -78,10 +95,12 @@ pub async fn place( ))); } } - // Snapshot the price the buyer actually pays — public sale or, for a - // business account, their negotiated/lowest price (same resolver the - // cart and storefront use). - let unit_price_cents = pricing::price_variant(ctx, &variant, user).await?.price_cents; + // The price the buyer actually pays — public sale or, for a business + // account, their negotiated/lowest price (resolved above, outside the + // transaction, with the same resolver the cart and storefront use). + let unit_price_cents = *price_by_variant + .get(&variant.id) + .ok_or_else(|| Error::BadRequest("an item is no longer available".to_string()))?; subtotal += unit_price_cents * i64::from(*qty); if let Some(on_hand) = variant.stock {