Functional Weave
Code in Rust

retail.basket-total@1.0.0

impl/rust.rs

7,595 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::finance_tax_add_vat::{add_vat, vat_breakdown_to_value, VatBreakdown};  ← from finance.tax.add-vat ^1.0.0 · built alongside by fune
use super::finance_tax_remove_vat::remove_vat;  ← from finance.tax.remove-vat ^1.0.0 · built alongside by fune
use super::money_allocate::allocate;  ← from money.allocate ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_sum::sum_money;  ← from money.sum ^1.0.0 · built alongside by fune
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<PromoLine> = 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<i64> = priced.lines.iter().map(|l| l.net.minor).collect();
    let shares: Vec<Money> = if basket_discount.minor == 0 {
        after_promotions.iter().map(|_| money(0, &currency)).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<i64> = Vec::new();
    let mut totals: Vec<BasketLineTotal> = Vec::new();
    for (i, line) in lines.iter().enumerate() {
        let v = vat(&money(after_promotions[i] - shares[i].minor, &currency), &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<BasketVatGroup> = groups
        .iter()
        .map(|&(bp, n, t)| BasketVatGroup {
            basis_points: bp,
            net: money(n, &currency),
            tax: money(t, &currency),
        })
        .collect();

    let mut nets: Vec<Money> = totals.iter().map(|t| t.net.clone()).collect();
    nets.push(delivery_vat.net.clone());
    let mut taxes: Vec<Money> = totals.iter().map(|t| t.tax.clone()).collect();
    taxes.push(delivery_vat.tax.clone());
    let mut grosses: Vec<Money> = 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, &currency),
        tax: sum_money(&taxes, &currency),
        total: sum_money(&grosses, &currency),
    }
}

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<BasketLine> = args[0].as_arr().iter().map(basket_line_from_value).collect();
    let promotions: Vec<Promotion> = 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(),
    ))
}