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

@@ -102,7 +102,8 @@ struct VariantInput {
id: Option<i32>,
label: String,
sku: Option<String>,
stock: i32,
/// `None` = available but not inventory-tracked.
stock: Option<i32>,
price_cents: i64,
business_sale_cents: Option<i64>,
position: i32,
@@ -156,11 +157,17 @@ fn parse_variants(form: &MultipartForm) -> Result<Vec<VariantInput>> {
}
let sku = form.text(&format!("variants[{i}][sku]"));
let stock = form
.text(&format!("variants[{i}][stock]"))
.and_then(|s| s.parse::<i32>().ok())
.filter(|n| *n >= 0)
.ok_or_else(|| Error::BadRequest("each option needs a stock quantity".to_string()))?;
// Stock is optional: blank means "available, not tracked". A value must
// be a non-negative integer.
let stock = match form.text(&format!("variants[{i}][stock]")) {
None => None,
Some(raw) => Some(
raw.parse::<i32>()
.ok()
.filter(|n| *n >= 0)
.ok_or_else(|| Error::BadRequest("stock must be 0 or more".to_string()))?,
),
};
let business_sale_cents = parse_optional_sale(form, i, "business_sale", price_cents)?;
let id = form
.text(&format!("variants[{i}][id]"))
@@ -315,12 +322,22 @@ async fn index(
let category_name = product
.category_id
.and_then(|id| category_name.get(&id).cloned());
let total_stock: i32 = variants.iter().map(|v| v.stock).sum();
// Stock column: total across tracked variants, or "∞" when any option is
// untracked (always available).
let stock_display = if variants.iter().any(|v| !v.tracked()) {
"".to_string()
} else {
variants
.iter()
.filter_map(|v| v.stock)
.sum::<i32>()
.to_string()
};
rows.push(product_row(
product,
priced,
variants.len(),
total_stock,
stock_display,
image,
category_name,
));
@@ -349,7 +366,7 @@ fn product_row(
product: &products::Model,
effective: &pricing::PricedProduct,
variant_count: usize,
total_stock: i32,
stock_display: String,
image: Option<String>,
category_name: Option<String>,
) -> serde_json::Value {
@@ -358,7 +375,7 @@ fn product_row(
"name": product.name,
"slug": product.slug,
"currency": product.currency,
"stock": total_stock,
"stock": stock_display,
"variant_count": variant_count,
"has_options": variant_count > 1,
"published": product.published,

View File

@@ -97,9 +97,9 @@ async fn add(
let mut items = parse_cart(&jar);
let add_qty = form.quantity.unwrap_or(1).max(1);
if let Some(entry) = items.iter_mut().find(|(id, _)| *id == variant.id) {
entry.1 = (entry.1 + add_qty).min(variant.stock);
entry.1 = variant.cap(entry.1 + add_qty);
} else {
items.push((variant.id, add_qty.min(variant.stock)));
items.push((variant.id, variant.cap(add_qty)));
}
items.retain(|(_, qty)| *qty > 0);
@@ -128,13 +128,14 @@ async fn update(
headers: HeaderMap,
Form(form): Form<UpdateForm>,
) -> Result<Response> {
let stock = published_variant(&ctx, form.variant_id)
.await?
.map(|(v, _)| v.stock)
.unwrap_or(0);
// Clamp the requested quantity to what's available (no cap for untracked
// variants); a removed variant clamps to 0 and drops out below.
let clamped = match published_variant(&ctx, form.variant_id).await? {
Some((variant, _)) => variant.cap(form.quantity),
None => 0,
};
let mut items = parse_cart(&jar);
let clamped = form.quantity.clamp(0, stock);
if let Some(entry) = items.iter_mut().find(|(id, _)| *id == form.variant_id) {
entry.1 = clamped;
}
@@ -208,7 +209,7 @@ pub(crate) async fn resolve_cart(
let Some((variant, product)) = published_variant(ctx, id).await? else {
continue;
};
let qty = qty.clamp(0, variant.stock);
let qty = variant.cap(qty);
if qty == 0 {
continue;
}