Functional Weave
Code in Rust

retail.refund-calculate@1.0.0

impl/rust.rs

5,445 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::math_round_div::round_div;  ← from math.round-div ^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

/// Refund returned units at what they were actually paid: the basket discount
/// is shared across every sale line first, then each line refunds by the
/// difference of cumulative shares, so a line fully returned in several
/// visits refunds exactly what it cost.
///
/// # Panics
/// Panics on an empty sale, mixed currencies, a discount bigger than the
/// basket, or a return of more units than remain.
pub fn calculate_refund(lines: &[SaleLine], basket_discount: &Money, returns: &[ReturnLine]) -> Refund {
    if lines.is_empty() {
        panic!("a sale needs at least one line");
    }
    let currency = lines[0].net.currency.clone();
    for line in lines {
        if line.net.currency != currency {
            panic!("currency mismatch: {} and {}", currency, line.net.currency);
        }
        if line.quantity < 1 {
            panic!("quantity must be 1 or more, received {}", line.quantity);
        }
        if line.net.minor < 0 {
            panic!("net must not be negative, received {}", line.net.minor);
        }
        if line.returned_before < 0 || line.returned_before > line.quantity {
            panic!(
                "returnedBefore must be from 0 to {}, received {}",
                line.quantity, line.returned_before
            );
        }
    }
    if basket_discount.currency != currency {
        panic!("currency mismatch: {} and {}", currency, basket_discount.currency);
    }
    let nets: Vec<Money> = lines.iter().map(|l| l.net.clone()).collect();
    let basket = sum_money(&nets, &currency);
    if basket_discount.minor < 0 {
        panic!("basketDiscount must not be negative, received {}", basket_discount.minor);
    }
    if basket_discount.minor > basket.minor {
        panic!(
            "basketDiscount {} is more than the lines' total {}",
            basket_discount.minor, basket.minor
        );
    }

    let shares: Vec<Money> = if basket_discount.minor == 0 {
        lines.iter().map(|_| money(0, &currency)).collect()
    } else {
        let ratios: Vec<i64> = lines.iter().map(|l| l.net.minor).collect();
        allocate(basket_discount, &ratios)
    };
    let paid: Vec<i64> = lines
        .iter()
        .zip(shares.iter())
        .map(|(l, s)| l.net.minor - s.minor)
        .collect();

    let mut seen: Vec<i64> = Vec::new();
    let mut refunded: Vec<RefundLine> = Vec::new();
    for r in returns {
        if r.line < 0 || r.line >= lines.len() as i64 {
            panic!("no line {} in the sale", r.line);
        }
        if seen.contains(&r.line) {
            panic!("line {} is returned twice in one refund", r.line);
        }
        seen.push(r.line);
        if r.quantity < 1 {
            panic!("return quantity must be 1 or more, received {}", r.quantity);
        }
        let index = r.line as usize;
        let line = &lines[index];
        if line.returned_before + r.quantity > line.quantity {
            panic!(
                "cannot return {} of line {}: {} bought, {} already returned",
                r.quantity, r.line, line.quantity, line.returned_before
            );
        }
        let before = round_div(paid[index] * line.returned_before, line.quantity, "half-up");
        let after = round_div(
            paid[index] * (line.returned_before + r.quantity),
            line.quantity,
            "half-up",
        );
        refunded.push(RefundLine {
            line: r.line,
            sku: line.sku.clone(),
            quantity: r.quantity,
            paid: money(paid[index], &currency),
            amount: money(after - before, &currency),
        });
    }

    let amounts: Vec<Money> = refunded.iter().map(|r| r.amount.clone()).collect();
    Refund {
        total: sum_money(&amounts, &currency),
        lines: refunded,
    }
}

pub fn sale_line_from_value(v: &Value) -> SaleLine {
    SaleLine {
        sku: v.get("sku").as_str().to_string(),
        quantity: v.get("quantity").as_i64(),
        net: money_from_value(v.get("net")),
        returned_before: v.get("returnedBefore").as_i64(),
    }
}

pub fn refund_to_value(r: &Refund) -> Value {
    Value::obj(vec![
        (
            "lines",
            Value::Arr(
                r.lines
                    .iter()
                    .map(|l| {
                        Value::obj(vec![
                            ("line", Value::Int(l.line)),
                            ("sku", Value::str(&l.sku)),
                            ("quantity", Value::Int(l.quantity)),
                            ("paid", money_to_value(&l.paid)),
                            ("amount", money_to_value(&l.amount)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("total", money_to_value(&r.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let lines: Vec<SaleLine> = args[0].as_arr().iter().map(sale_line_from_value).collect();
    let returns: Vec<ReturnLine> = args[2]
        .as_arr()
        .iter()
        .map(|v| ReturnLine {
            line: v.get("line").as_i64(),
            quantity: v.get("quantity").as_i64(),
        })
        .collect();
    refund_to_value(&calculate_refund(&lines, &money_from_value(&args[1]), &returns))
}