diff --git a/assets/views/admin/catalog/discounts.html b/assets/views/admin/catalog/discounts.html index 94d71da..9e569cb 100644 --- a/assets/views/admin/catalog/discounts.html +++ b/assets/views/admin/catalog/discounts.html @@ -62,6 +62,7 @@ {{ ui::th(label=t(key="product", lang=lang | default(value='sk'))) }} {{ ui::th(label=t(key="price", lang=lang | default(value='sk'))) }} {{ ui::th(label=t(key="sale-price", lang=lang | default(value='sk'))) }} + {{ ui::th(label=t(key="effective-price", lang=lang | default(value='sk'))) }} {{ ui::th(label=t(key="status", lang=lang | default(value='sk'))) }} {{ ui::th(label=t(key="actions", lang=lang | default(value='sk')), align="text-right") }} @@ -83,6 +84,14 @@ {% endif %} + + {% if product.effective_reduced %} + {{ product.effective_price }} {{ product.currency }} + (−{{ product.effective_percent_off }}%) + {% else %} + {{ product.effective_price }} {{ product.currency }} + {% endif %} + {% if on_sale %} {{ ui::badge(label=t(key="on-sale", lang=lang | default(value='sk')), variant="danger") }} diff --git a/src/controllers/admin_discounts.rs b/src/controllers/admin_discounts.rs index 9ec1b5e..2f0ffbd 100644 --- a/src/controllers/admin_discounts.rs +++ b/src/controllers/admin_discounts.rs @@ -25,6 +25,7 @@ use crate::{ shared::{ guard, money::{format_bp, format_price, parse_percent, parse_price_to_cents}, + pricing, }, }; @@ -89,9 +90,9 @@ fn percent_off(regular_cents: i64, sale_cents: i64) -> i64 { off.round() as i64 } -/// Row shape for the discounts list, carrying both audiences' values so the -/// template can show whichever tab is active. -fn list_row(product: &products::Model) -> serde_json::Value { +/// Row shape for the discounts list, carrying both audiences' per-product values +/// plus the resolved effective price for the active tab (after profiles). +fn list_row(product: &products::Model, effective: &pricing::PricedProduct) -> serde_json::Value { json!({ "id": product.id, "name": product.name, @@ -106,6 +107,11 @@ fn list_row(product: &products::Model) -> serde_json::Value { "business_percent_off": product .business_sale_price_cents .map(|sale| percent_off(product.price_cents, sale)), + // The price this audience actually pays after the per-product discount + // and any applied profiles. + "effective_price": format_price(effective.price_cents), + "effective_reduced": effective.is_reduced(), + "effective_percent_off": percent_off(product.price_cents, effective.price_cents), }) } @@ -123,7 +129,12 @@ async fn index( .order_by_asc(products::Column::Name) .all(&ctx.db) .await?; - let rows: Vec = list.iter().map(list_row).collect(); + let effective = pricing::audience_price_many(&ctx, &list, audience).await?; + let rows: Vec = list + .iter() + .zip(effective.iter()) + .map(|(product, priced)| list_row(product, priced)) + .collect(); // Profiles applied globally to this audience, plus all profiles to choose from. let assigned: HashSet = audience_discount_profiles::Entity::find() diff --git a/src/shared/pricing.rs b/src/shared/pricing.rs index d39c5ce..6f70d15 100644 --- a/src/shared/pricing.rs +++ b/src/shared/pricing.rs @@ -338,6 +338,38 @@ pub async fn detail_many( Ok(list.iter().map(|p| detail_for(p, &pc)).collect()) } +/// Effective prices for a whole audience using only the global layers (the +/// per-product sale + audience-assigned profiles + the business baseline), with +/// no specific company's per-company deals. Used by the discounts admin page to +/// preview what each tab's discounts produce. `audience` is "personal" or +/// "business". +pub async fn audience_price_many( + ctx: &AppContext, + list: &[products::Model], + audience: &str, +) -> Result> { + let personal = load_audience(ctx, AUDIENCE_PERSONAL).await?; + let (business, b2b) = if audience == AUDIENCE_BUSINESS { + ( + load_audience(ctx, AUDIENCE_BUSINESS).await?, + // A generic company with no per-company profiles/negotiated prices. + Some(B2bContext { + manual: HashMap::new(), + profiles: LoadedProfiles::empty(), + resolutions: HashMap::new(), + }), + ) + } else { + (LoadedProfiles::empty(), None) + }; + let pc = PricingCtx { + personal, + business, + b2b, + }; + Ok(list.iter().map(|p| detail_for(p, &pc).priced()).collect()) +} + /// Price one product for `user` (`None` = anonymous/public). pub async fn price_for( ctx: &AppContext,