use super::funejson::Value; use super::math_round_div::round_div; use super::money_allocate::allocate; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::money_sum::sum_money; /// 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 = 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 = if basket_discount.minor == 0 { lines.iter().map(|_| money(0, ¤cy)).collect() } else { let ratios: Vec = lines.iter().map(|l| l.net.minor).collect(); allocate(basket_discount, &ratios) }; let paid: Vec = lines .iter() .zip(shares.iter()) .map(|(l, s)| l.net.minor - s.minor) .collect(); let mut seen: Vec = Vec::new(); let mut refunded: Vec = 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 = 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 = args[0].as_arr().iter().map(sale_line_from_value).collect(); let returns: Vec = 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)) }