now products have different options, like different parameters
This commit is contained in:
@@ -19,7 +19,8 @@ use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
|
||||
use crate::models::{
|
||||
account_discount_profiles, account_product_prices, account_product_resolutions,
|
||||
audience_discount_profiles, discount_profile_products, discount_profiles, products, users,
|
||||
audience_discount_profiles, discount_profile_products, discount_profiles, product_variants,
|
||||
users,
|
||||
};
|
||||
use crate::shared::money::apply_discount_bp;
|
||||
|
||||
@@ -219,7 +220,7 @@ async fn load_b2b(ctx: &AppContext, user_id: i32) -> Result<B2bContext> {
|
||||
.all(&ctx.db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| (r.product_id, r.discount_profile_id))
|
||||
.map(|r| (r.variant_id, r.discount_profile_id))
|
||||
.collect();
|
||||
|
||||
Ok(B2bContext {
|
||||
@@ -236,13 +237,16 @@ struct PricingCtx {
|
||||
b2b: Option<B2bContext>,
|
||||
}
|
||||
|
||||
/// Resolve the per-company automated price for a product: the single covering
|
||||
/// profile, the admin-resolved winner on a collision, or (unresolved) the
|
||||
/// biggest discount, flagged.
|
||||
/// Resolve the per-company automated price for a variant. Discount profiles
|
||||
/// cover the *product* (`product_id`); the admin's collision resolution is set
|
||||
/// per *variant* (`variant_id`). Result: the single covering profile, the
|
||||
/// admin-resolved winner on a collision, or (unresolved) the biggest discount,
|
||||
/// flagged.
|
||||
fn company_auto(
|
||||
b2b: &B2bContext,
|
||||
regular_cents: i64,
|
||||
product_id: i32,
|
||||
variant_id: i32,
|
||||
) -> (Option<i64>, Option<i32>, bool, Vec<i32>) {
|
||||
let covering = b2b.profiles.covering(product_id);
|
||||
let ids: Vec<i32> = covering.iter().map(|p| p.id).collect();
|
||||
@@ -254,7 +258,7 @@ fn company_auto(
|
||||
} else {
|
||||
match b2b
|
||||
.resolutions
|
||||
.get(&product_id)
|
||||
.get(&variant_id)
|
||||
.and_then(|rid| covering.iter().find(|p| p.id == *rid).copied())
|
||||
{
|
||||
Some(resolved) => (resolved, false),
|
||||
@@ -276,12 +280,14 @@ fn company_auto(
|
||||
)
|
||||
}
|
||||
|
||||
fn detail_for(product: &products::Model, pc: &PricingCtx) -> PriceDetail {
|
||||
let regular = product.price_cents;
|
||||
fn detail_for_variant(variant: &product_variants::Model, pc: &PricingCtx) -> PriceDetail {
|
||||
let regular = variant.price_cents;
|
||||
let product_id = variant.product_id;
|
||||
|
||||
// Personal price: public sale + personal-audience profiles, lowest wins.
|
||||
let personal_sale = product.effective_price_cents();
|
||||
let personal_profile = pc.personal.best_price(regular, product.id);
|
||||
// Personal price: the variant's own public sale + personal-audience profiles
|
||||
// (which cover the product), lowest wins.
|
||||
let personal_sale = variant.effective_price_cents();
|
||||
let personal_profile = pc.personal.best_price(regular, product_id);
|
||||
let personal_effective = personal_profile
|
||||
.map_or(personal_sale, |p| personal_sale.min(p));
|
||||
|
||||
@@ -289,12 +295,13 @@ fn detail_for(product: &products::Model, pc: &PricingCtx) -> PriceDetail {
|
||||
return PriceDetail::public_only(regular, personal_effective);
|
||||
};
|
||||
|
||||
// Business candidates (company accounts only).
|
||||
let manual = b2b.manual.get(&product.id).copied();
|
||||
// Business candidates (company accounts only). The negotiated price is keyed
|
||||
// to the variant; the baseline business sale lives on the variant.
|
||||
let manual = b2b.manual.get(&variant.id).copied();
|
||||
let (auto_cents, auto_profile_id, collision, covering_ids) =
|
||||
company_auto(b2b, regular, product.id);
|
||||
let business_global = product.business_sale_price_cents;
|
||||
let business_profile = pc.business.best_price(regular, product.id);
|
||||
company_auto(b2b, regular, product_id, variant.id);
|
||||
let business_global = variant.business_sale_price_cents;
|
||||
let business_profile = pc.business.best_price(regular, product_id);
|
||||
let business_best = lowest([manual, auto_cents, business_global, business_profile]);
|
||||
|
||||
let priced = decide(regular, personal_effective, business_best);
|
||||
@@ -328,14 +335,14 @@ async fn load_pricing_ctx(ctx: &AppContext, user: Option<&users::Model>) -> Resu
|
||||
})
|
||||
}
|
||||
|
||||
/// Full breakdowns for many products for `user`, batching per-request lookups.
|
||||
pub async fn detail_many(
|
||||
/// Full breakdowns for many variants for `user`, batching per-request lookups.
|
||||
pub async fn detail_variants(
|
||||
ctx: &AppContext,
|
||||
list: &[products::Model],
|
||||
list: &[product_variants::Model],
|
||||
user: Option<&users::Model>,
|
||||
) -> Result<Vec<PriceDetail>> {
|
||||
let pc = load_pricing_ctx(ctx, user).await?;
|
||||
Ok(list.iter().map(|p| detail_for(p, &pc)).collect())
|
||||
Ok(list.iter().map(|v| detail_for_variant(v, &pc)).collect())
|
||||
}
|
||||
|
||||
/// Effective prices for a whole audience using only the global layers (the
|
||||
@@ -343,9 +350,9 @@ pub async fn detail_many(
|
||||
/// 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(
|
||||
pub async fn audience_price_variants(
|
||||
ctx: &AppContext,
|
||||
list: &[products::Model],
|
||||
list: &[product_variants::Model],
|
||||
audience: &str,
|
||||
) -> Result<Vec<PricedProduct>> {
|
||||
let personal = load_audience(ctx, AUDIENCE_PERSONAL).await?;
|
||||
@@ -367,7 +374,7 @@ pub async fn audience_price_many(
|
||||
business,
|
||||
b2b,
|
||||
};
|
||||
Ok(list.iter().map(|p| detail_for(p, &pc).priced()).collect())
|
||||
Ok(list.iter().map(|v| detail_for_variant(v, &pc).priced()).collect())
|
||||
}
|
||||
|
||||
/// Like [`audience_price_many`], but prices against an *unsaved* set of profile
|
||||
@@ -376,9 +383,9 @@ pub async fn audience_price_many(
|
||||
/// checkboxes, before they hit Save. For the business tab the personal layer
|
||||
/// stays the persisted one (businesses get the lower of personal/business), and
|
||||
/// only the business layer is replaced by the previewed selection.
|
||||
pub async fn audience_price_many_preview(
|
||||
pub async fn audience_price_variants_preview(
|
||||
ctx: &AppContext,
|
||||
list: &[products::Model],
|
||||
list: &[product_variants::Model],
|
||||
audience: &str,
|
||||
selected_profile_ids: Vec<i32>,
|
||||
) -> Result<Vec<PricedProduct>> {
|
||||
@@ -401,26 +408,26 @@ pub async fn audience_price_many_preview(
|
||||
business,
|
||||
b2b,
|
||||
};
|
||||
Ok(list.iter().map(|p| detail_for(p, &pc).priced()).collect())
|
||||
Ok(list.iter().map(|v| detail_for_variant(v, &pc).priced()).collect())
|
||||
}
|
||||
|
||||
/// Price one product for `user` (`None` = anonymous/public).
|
||||
pub async fn price_for(
|
||||
/// Price one variant for `user` (`None` = anonymous/public).
|
||||
pub async fn price_variant(
|
||||
ctx: &AppContext,
|
||||
product: &products::Model,
|
||||
variant: &product_variants::Model,
|
||||
user: Option<&users::Model>,
|
||||
) -> Result<PricedProduct> {
|
||||
let detail = detail_many(ctx, std::slice::from_ref(product), user).await?;
|
||||
let detail = detail_variants(ctx, std::slice::from_ref(variant), user).await?;
|
||||
Ok(detail[0].priced())
|
||||
}
|
||||
|
||||
/// Price many products for `user`, batching the per-request lookups.
|
||||
pub async fn price_many(
|
||||
/// Price many variants for `user`, batching the per-request lookups.
|
||||
pub async fn price_variants(
|
||||
ctx: &AppContext,
|
||||
list: &[products::Model],
|
||||
list: &[product_variants::Model],
|
||||
user: Option<&users::Model>,
|
||||
) -> Result<Vec<PricedProduct>> {
|
||||
Ok(detail_many(ctx, list, user)
|
||||
Ok(detail_variants(ctx, list, user)
|
||||
.await?
|
||||
.iter()
|
||||
.map(PriceDetail::priced)
|
||||
|
||||
Reference in New Issue
Block a user