updated wording

This commit is contained in:
2026-06-17 16:39:43 +02:00
parent 8b1b9cedc2
commit 7a90ced98e
14 changed files with 257 additions and 79 deletions
+41 -9
View File
@@ -226,8 +226,15 @@ struct UpdateItemReq {
#[validate(length(max = 1000))]
note: Option<String>,
status: Option<String>,
target_price: Option<Decimal>,
/// Absent = leave as-is; `null` = clear the target (→ notify on any drop);
/// a value = set the target.
#[serde(default, deserialize_with = "double_option")]
target_price: Option<Option<Decimal>>,
position: Option<i32>,
/// Manual ISO 4217 currency override (e.g. "EUR"). Wins over the fetched
/// currency for items the conversion heuristic mislabels.
#[validate(length(equal = 3, message = "currency must be a 3-letter code"))]
currency: Option<String>,
}
async fn update_item(
@@ -244,15 +251,27 @@ async fn update_item(
}
}
// Ownership enforced via the join to lists.user_id.
let currency = req
.currency
.map(|c| c.trim().to_uppercase())
.filter(|c| !c.is_empty());
// target_price: None = leave; Some(v) = set (v may be NULL to clear).
let set_target = req.target_price.is_some();
let target_val = req.target_price.flatten();
// Ownership enforced via the join to lists.user_id. A currency override
// both latches (currency_override) and takes effect immediately (currency).
let item = sqlx::query_as::<_, Item>(&format!(
"UPDATE items i SET
title = COALESCE($3, i.title),
url = COALESCE($4, i.url),
note = COALESCE($5, i.note),
status = COALESCE($6::item_status, i.status),
target_price = COALESCE($7, i.target_price),
position = COALESCE($8, i.position)
title = COALESCE($3, i.title),
url = COALESCE($4, i.url),
note = COALESCE($5, i.note),
status = COALESCE($6::item_status, i.status),
target_price = CASE WHEN $7 THEN $8 ELSE i.target_price END,
position = COALESCE($9, i.position),
currency_override = COALESCE($10, i.currency_override),
currency = COALESCE($10, i.currency)
FROM lists l
WHERE i.id = $1 AND i.list_id = l.id AND l.user_id = $2
RETURNING {ITEM_COLS_I}"
@@ -263,8 +282,10 @@ async fn update_item(
.bind(opt_trim(req.url))
.bind(opt_trim(req.note))
.bind(req.status)
.bind(req.target_price)
.bind(set_target)
.bind(target_val)
.bind(req.position)
.bind(currency)
.fetch_optional(&state.pool)
.await?
.ok_or(AppError::NotFound)?;
@@ -354,3 +375,14 @@ async fn item_history(
fn opt_trim(s: Option<String>) -> Option<String> {
s.map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
}
/// Deserialize a present-but-nullable field into `Option<Option<T>>`, so an
/// explicit JSON `null` (`Some(None)`) is distinguishable from an absent field
/// (`None`, handled by `#[serde(default)]`). Lets a PATCH clear a value.
fn double_option<'de, T, D>(de: D) -> Result<Option<Option<T>>, D::Error>
where
T: Deserialize<'de>,
D: serde::Deserializer<'de>,
{
Ok(Some(Option::<T>::deserialize(de)?))
}