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, ¤cy);
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, ¤cy)).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], ¤cy),
amount: money(after - before, ¤cy),
});
}
let amounts: Vec<Money> = refunded.iter().map(|r| r.amount.clone()).collect();
Refund {
total: sum_money(&amounts, ¤cy),
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))
}