effective price is only highlighted if changed

This commit is contained in:
Priec
2026-06-22 11:10:12 +02:00
parent bf8f8e54c9
commit 088fcb60a1
6 changed files with 210 additions and 54 deletions

View File

@@ -31,6 +31,7 @@ use crate::{
};
const COMPANY: &str = "company";
const BUSINESS_AUDIENCE: &str = "business";
#[derive(Debug, Deserialize)]
struct PriceForm {
@@ -117,8 +118,6 @@ async fn show(
.all(&ctx.db)
.await?;
let assigned = assigned_profile_ids(&ctx, company.id).await?;
let profile_name: HashMap<i32, String> =
all_profiles.iter().map(|p| (p.id, p.name.clone())).collect();
let profiles_json: Vec<serde_json::Value> = all_profiles
.iter()
.map(|p| {
@@ -136,32 +135,30 @@ async fn show(
.order_by_asc(products::Column::Name)
.all(&ctx.db)
.await?;
// Two prices per product:
// - the generic business price a freshly-registered company sees (business
// baseline + business-audience profiles, no per-company deals), and
// - this company's effective price (its negotiated price + assigned profiles).
// The effective price is highlighted only when it differs from the generic one.
let business = pricing::audience_price_many(&ctx, &list, BUSINESS_AUDIENCE).await?;
let details = pricing::detail_many(&ctx, &list, Some(&company)).await?;
let rows: Vec<serde_json::Value> = list
.iter()
.zip(business.iter())
.zip(details.iter())
.map(|(product, d)| {
let covering: Vec<serde_json::Value> = d
.covering_profile_ids
.iter()
.map(|pid| json!({ "id": pid, "name": profile_name.get(pid) }))
.collect();
.map(|((product, b), d)| {
json!({
"product_id": product.id,
"name": product.name,
"currency": product.currency,
"regular_price": format_price(d.regular_cents),
"public_price": format_price(d.public_cents),
"on_public_sale": product.on_sale(),
"manual_price": d.manual_cents.map(format_price),
"auto_price": d.auto_cents.map(format_price),
"auto_profile_name": d.auto_profile_id.and_then(|pid| profile_name.get(&pid)),
"auto_profile_id": d.auto_profile_id,
"business_price": format_price(b.price_cents),
"business_reduced": b.price_cents < d.regular_cents,
"has_negotiated": d.manual_cents.is_some(),
"collision": d.collision,
"covering": covering,
"effective_price": format_price(d.price_cents),
"is_business": d.is_business,
"effective_differs": d.price_cents != b.price_cents,
})
})
.collect();
@@ -179,6 +176,75 @@ async fn show(
)
}
/// Dedicated per-product page for the negotiated price (and, when two assigned
/// profiles collide, the resolution selector). Mirrors the catalog "Set discount"
/// page but for a single company.
#[debug_handler]
async fn price_edit(
auth: auth::JWT,
jar: CookieJar,
ViewEngine(v): ViewEngine<TeraView>,
Path((id, product_id)): Path<(i32, i32)>,
Query(params): Query<HashMap<String, String>>,
State(ctx): State<AppContext>,
) -> Result<Response> {
guard::current_admin(auth, &ctx).await?;
let company = company_by_id(&ctx, id).await?;
let product = products::Entity::find_by_id(product_id)
.one(&ctx.db)
.await?
.ok_or_else(|| Error::NotFound)?;
let business =
pricing::audience_price_many(&ctx, std::slice::from_ref(&product), BUSINESS_AUDIENCE)
.await?;
let business_cents = business[0].price_cents;
let detail = pricing::detail_many(&ctx, std::slice::from_ref(&product), Some(&company)).await?;
let d = &detail[0];
// Names for the covering profiles, used by the collision resolution selector.
let covering: Vec<serde_json::Value> = if d.covering_profile_ids.is_empty() {
Vec::new()
} else {
let profiles = discount_profiles::Entity::find()
.filter(discount_profiles::Column::Id.is_in(d.covering_profile_ids.clone()))
.all(&ctx.db)
.await?;
let name: HashMap<i32, String> =
profiles.iter().map(|p| (p.id, p.name.clone())).collect();
d.covering_profile_ids
.iter()
.map(|pid| json!({ "id": pid, "name": name.get(pid) }))
.collect()
};
format::view(
&v,
"admin/customers/price_form.html",
json!({
"customer": { "id": company.id, "name": company.name },
"product": {
"id": product.id,
"name": product.name,
"currency": product.currency,
"regular_price": format_price(d.regular_cents),
"regular_cents": d.regular_cents,
"business_price": format_price(business_cents),
"business_reduced": business_cents < d.regular_cents,
"effective_price": format_price(d.price_cents),
"effective_differs": d.price_cents != business_cents,
},
"negotiated": d.manual_cents.map(format_price).unwrap_or_default(),
"has_negotiated": d.manual_cents.is_some(),
"collision": d.collision,
"covering": covering,
"auto_profile_id": d.auto_profile_id,
"error": params.get("error"),
"lang": current_lang(&jar),
}),
)
}
#[debug_handler]
async fn set_price(
auth: auth::JWT,
@@ -199,7 +265,7 @@ async fn set_price(
Ok(cents) if cents > 0 => cents,
_ => {
return format::redirect(&format!(
"/admin/customers/{id}?error=discount-must-be-positive"
"/admin/customers/{id}/prices/{product_id}/edit?error=discount-must-be-positive"
))
}
};
@@ -288,6 +354,10 @@ pub fn routes() -> Routes {
.add("/admin/customers", get(index))
.add("/admin/customers/{id}", get(show))
.add("/admin/customers/{id}/profiles", post(sync_profiles))
.add(
"/admin/customers/{id}/prices/{product_id}/edit",
get(price_edit),
)
.add("/admin/customers/{id}/prices/{product_id}", post(set_price))
.add(
"/admin/customers/{id}/prices/{product_id}/remove",