0 is out of stock and nothing is available from now on
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled

This commit is contained in:
Priec
2026-06-22 16:48:28 +02:00
parent 6828854f24
commit 681c88f85d
15 changed files with 140 additions and 39 deletions

View File

@@ -14,7 +14,7 @@ pub struct Model {
pub label: String,
pub position: i32,
pub sku: Option<String>,
pub stock: i32,
pub stock: Option<i32>,
pub price_cents: i64,
pub sale_price_cents: Option<i64>,
pub business_sale_price_cents: Option<i64>,

View File

@@ -65,11 +65,15 @@ pub async fn place(
.one(&txn)
.await?
.ok_or_else(|| Error::BadRequest("a product is no longer available".to_string()))?;
if variant.stock < *qty {
return Err(Error::BadRequest(format!(
"not enough stock for {}",
product.name
)));
// Tracked variants can't oversell; untracked ones (stock = None) are
// always available and never decremented.
if let Some(on_hand) = variant.stock {
if on_hand < *qty {
return Err(Error::BadRequest(format!(
"not enough stock for {}",
product.name
)));
}
}
currency = product.currency.clone();
// Snapshot the price the buyer actually pays — public sale or, for a
@@ -78,9 +82,11 @@ pub async fn place(
let unit_price_cents = pricing::price_variant(ctx, &variant, user).await?.price_cents;
subtotal += unit_price_cents * i64::from(*qty);
let mut active = variant.clone().into_active_model();
active.stock = Set(variant.stock - *qty);
active.update(&txn).await?;
if let Some(on_hand) = variant.stock {
let mut active = variant.clone().into_active_model();
active.stock = Set(Some(on_hand - *qty));
active.update(&txn).await?;
}
snapshots.push((product.id, variant.id, product.name, variant.label, unit_price_cents, *qty));
}

View File

@@ -45,6 +45,30 @@ impl Model {
pub fn business_on_sale(&self) -> bool {
matches!(self.business_sale_price_cents, Some(sale) if sale < self.price_cents)
}
/// Whether the variant's inventory is tracked. A `None` stock means
/// "available, not tracked" (always purchasable, unlimited).
#[must_use]
pub fn tracked(&self) -> bool {
self.stock.is_some()
}
/// Whether the variant can currently be bought: untracked variants are always
/// available; tracked ones need a positive quantity on hand.
#[must_use]
pub fn in_stock(&self) -> bool {
self.stock.map_or(true, |s| s > 0)
}
/// Clamp a desired quantity to what's available: capped at the tracked stock,
/// or left as-is (only floored at 0) when untracked.
#[must_use]
pub fn cap(&self, qty: i32) -> i32 {
match self.stock {
Some(s) => qty.clamp(0, s),
None => qty.max(0),
}
}
}
// implement your write-oriented logic here