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,
|