use super::funejson::Value; use super::finance_tax_add_vat::{add_vat, vat_breakdown_to_value, VatBreakdown}; use super::finance_tax_remove_vat::remove_vat; use super::money_allocate::allocate; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::money_sum::sum_money; use super::retail_promotion_apply::{ applied_promotion_to_value, apply_promotions, promotion_from_value, PromoLine, Promotion, }; /// The checkout: promotions, then the basket discount shared across the /// lines, then VAT per line on what the line really costs, then delivery. /// /// No arithmetic lives here; the order of operations is the whole function. /// /// # Panics /// Panics on mixed currencies, negative discounts or delivery, a discount /// bigger than the basket, or anything its dependencies refuse. #[allow(clippy::too_many_arguments)] pub fn basket_total( lines: &[BasketLine], promotions: &[Promotion], basket_discount: &Money, delivery: &Money, delivery_tax_category: &str, prices_include_vat: bool, jurisdiction: &str, on_date: &str, ) -> Basket { let promo_lines: Vec = lines .iter() .map(|l| PromoLine { sku: l.sku.clone(), unit_price: l.unit_price.clone(), quantity: l.quantity, }) .collect(); let priced = apply_promotions(&promo_lines, promotions); let currency = priced.subtotal.currency.clone(); for (name, amount) in [("basketDiscount", basket_discount), ("delivery", delivery)] { if amount.currency != currency { panic!("currency mismatch: {} and {}", currency, amount.currency); } if amount.minor < 0 { panic!("{} must not be negative, received {}", name, amount.minor); } } if basket_discount.minor > priced.total.minor { panic!( "basketDiscount {} is more than the basket's {} after promotions", basket_discount.minor, priced.total.minor ); } let after_promotions: Vec = priced.lines.iter().map(|l| l.net.minor).collect(); let shares: Vec = if basket_discount.minor == 0 { after_promotions.iter().map(|_| money(0, ¤cy)).collect() } else { allocate(basket_discount, &after_promotions) }; let vat = |amount: &Money, category: &str| -> VatBreakdown { if prices_include_vat { remove_vat(amount, jurisdiction, category, on_date) } else { add_vat(amount, jurisdiction, category, on_date) } }; let mut rates: Vec = Vec::new(); let mut totals: Vec = Vec::new(); for (i, line) in lines.iter().enumerate() { let v = vat(&money(after_promotions[i] - shares[i].minor, ¤cy), &line.tax_category); rates.push(v.basis_points); totals.push(BasketLineTotal { sku: line.sku.clone(), quantity: line.quantity, gross: priced.lines[i].gross.clone(), promotion_discount: priced.lines[i].discount.clone(), basket_discount: shares[i].clone(), net: v.net, tax: v.tax, total: v.gross, }); } let delivery_vat = vat(delivery, delivery_tax_category); // Grouped by rate, lowest first, so the receipt's VAT summary is stable. let mut groups: Vec<(i64, i64, i64)> = Vec::new(); let mut add = |bp: i64, net: i64, tax: i64| match groups.iter_mut().find(|g| g.0 == bp) { Some(g) => { g.1 += net; g.2 += tax; } None => groups.push((bp, net, tax)), }; for (i, t) in totals.iter().enumerate() { add(rates[i], t.net.minor, t.tax.minor); } if delivery.minor != 0 { add(delivery_vat.basis_points, delivery_vat.net.minor, delivery_vat.tax.minor); } groups.sort_by_key(|g| g.0); let vat_breakdown: Vec = groups .iter() .map(|&(bp, n, t)| BasketVatGroup { basis_points: bp, net: money(n, ¤cy), tax: money(t, ¤cy), }) .collect(); let mut nets: Vec = totals.iter().map(|t| t.net.clone()).collect(); nets.push(delivery_vat.net.clone()); let mut taxes: Vec = totals.iter().map(|t| t.tax.clone()).collect(); taxes.push(delivery_vat.tax.clone()); let mut grosses: Vec = totals.iter().map(|t| t.total.clone()).collect(); grosses.push(delivery_vat.gross.clone()); Basket { lines: totals, promotions: priced.applied, delivery: delivery_vat, vat_breakdown, subtotal: priced.subtotal, promotion_discount: priced.discount, basket_discount: basket_discount.clone(), net: sum_money(&nets, ¤cy), tax: sum_money(&taxes, ¤cy), total: sum_money(&grosses, ¤cy), } } pub fn basket_line_from_value(v: &Value) -> BasketLine { BasketLine { sku: v.get("sku").as_str().to_string(), unit_price: money_from_value(v.get("unitPrice")), quantity: v.get("quantity").as_i64(), tax_category: v.get("taxCategory").as_str().to_string(), } } pub fn basket_to_value(b: &Basket) -> Value { Value::obj(vec![ ( "lines", Value::Arr( b.lines .iter() .map(|l| { Value::obj(vec![ ("sku", Value::str(&l.sku)), ("quantity", Value::Int(l.quantity)), ("gross", money_to_value(&l.gross)), ("promotionDiscount", money_to_value(&l.promotion_discount)), ("basketDiscount", money_to_value(&l.basket_discount)), ("net", money_to_value(&l.net)), ("tax", money_to_value(&l.tax)), ("total", money_to_value(&l.total)), ]) }) .collect(), ), ), ("promotions", Value::Arr(b.promotions.iter().map(applied_promotion_to_value).collect())), ("delivery", vat_breakdown_to_value(&b.delivery)), ( "vatBreakdown", Value::Arr( b.vat_breakdown .iter() .map(|g| { Value::obj(vec![ ("basisPoints", Value::Int(g.basis_points)), ("net", money_to_value(&g.net)), ("tax", money_to_value(&g.tax)), ]) }) .collect(), ), ), ("subtotal", money_to_value(&b.subtotal)), ("promotionDiscount", money_to_value(&b.promotion_discount)), ("basketDiscount", money_to_value(&b.basket_discount)), ("net", money_to_value(&b.net)), ("tax", money_to_value(&b.tax)), ("total", money_to_value(&b.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let lines: Vec = args[0].as_arr().iter().map(basket_line_from_value).collect(); let promotions: Vec = args[1].as_arr().iter().map(promotion_from_value).collect(); basket_to_value(&basket_total( &lines, &promotions, &money_from_value(&args[2]), &money_from_value(&args[3]), args[4].as_str(), args[5].as_bool(), args[6].as_str(), args[7].as_str(), )) }