dynamic prices with dicounts

This commit is contained in:
Priec
2026-06-22 08:47:22 +02:00
parent e98c70aa63
commit 262ec1bfdb
3 changed files with 56 additions and 4 deletions

View File

@@ -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") }}
</tr>
@@ -83,6 +84,14 @@
<span class="text-on-surface/40 dark:text-on-surface-dark/40"></span>
{% endif %}
</td>
<td class="px-4 py-3 tabular-nums">
{% if product.effective_reduced %}
<span class="font-medium text-primary dark:text-primary-dark">{{ product.effective_price }} {{ product.currency }}</span>
<span class="ml-1 text-xs text-on-surface/60 dark:text-on-surface-dark/60">({{ product.effective_percent_off }}%)</span>
{% else %}
{{ product.effective_price }} {{ product.currency }}
{% endif %}
</td>
<td class="px-4 py-3">
{% if on_sale %}
{{ ui::badge(label=t(key="on-sale", lang=lang | default(value='sk')), variant="danger") }}

View File

@@ -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<serde_json::Value> = list.iter().map(list_row).collect();
let effective = pricing::audience_price_many(&ctx, &list, audience).await?;
let rows: Vec<serde_json::Value> = 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<i32> = audience_discount_profiles::Entity::find()

View File

@@ -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<Vec<PricedProduct>> {
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,