use super::funejson::Value; use super::finance_invoice_line_total::line_total; use super::finance_tax_add_vat::add_vat; use super::money_add::add_money; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::money_sum::sum_money; /// Calculate a complete invoice. /// /// Note what this function does not contain: no multiplication, no /// percentages, no rounding, no rate table. Each of those is a capability that /// is already pinned by its own vectors. What is left here is the part that is /// genuinely about invoices - the order of operations, and how tax is grouped. /// /// VAT is calculated per line and then grouped by rate, which keeps each /// line's tax reconcilable against that line. /// /// # Panics /// Panics if `lines` is empty or the lines are not all in one currency. pub fn calculate_invoice(lines: &[InvoiceLine], jurisdiction: &str, invoice_date: &str) -> Invoice { if lines.is_empty() { panic!("an invoice needs at least one line"); } let currency = lines[0].unit_price.currency.clone(); let mut calculated: Vec = Vec::with_capacity(lines.len()); for line in lines { let net = line_total(&line.unit_price, line.quantity, line.discount_basis_points); let vat = add_vat(&net, jurisdiction, &line.tax_category, invoice_date); calculated.push(CalculatedLine { description: line.description.clone(), net: vat.net, tax: vat.tax, gross: vat.gross, basis_points: vat.basis_points, }); } let nets: Vec = calculated.iter().map(|l| l.net.clone()).collect(); let taxes: Vec = calculated.iter().map(|l| l.tax.clone()).collect(); let subtotal = sum_money(&nets, ¤cy); let tax_total = sum_money(&taxes, ¤cy); // Grouped by rate and ordered by rate, so the summary block on a printed // invoice is stable between runs and between languages. let mut tax_breakdown: Vec = Vec::new(); for line in &calculated { match tax_breakdown .iter_mut() .find(|group| group.basis_points == line.basis_points) { Some(group) => { group.taxable = add_money(&group.taxable, &line.net); group.tax = add_money(&group.tax, &line.tax); } None => tax_breakdown.push(TaxGroup { basis_points: line.basis_points, taxable: line.net.clone(), tax: line.tax.clone(), }), } } tax_breakdown.sort_by_key(|group| group.basis_points); Invoice { currency, lines: calculated, total: add_money(&subtotal, &tax_total), subtotal, tax_total, tax_breakdown, } } pub fn invoice_line_from_value(v: &Value) -> InvoiceLine { InvoiceLine { description: v.get("description").as_str().to_string(), unit_price: money_from_value(v.get("unitPrice")), quantity: v.get("quantity").as_i64(), discount_basis_points: v.get("discountBasisPoints").as_i64(), tax_category: v.get("taxCategory").as_str().to_string(), } } pub fn invoice_to_value(invoice: &Invoice) -> Value { Value::obj(vec![ ("currency", Value::str(&invoice.currency)), ( "lines", Value::Arr( invoice .lines .iter() .map(|line| { Value::obj(vec![ ("description", Value::str(&line.description)), ("net", money_to_value(&line.net)), ("tax", money_to_value(&line.tax)), ("gross", money_to_value(&line.gross)), ("basisPoints", Value::Int(line.basis_points)), ]) }) .collect(), ), ), ("subtotal", money_to_value(&invoice.subtotal)), ("taxTotal", money_to_value(&invoice.tax_total)), ("total", money_to_value(&invoice.total)), ( "taxBreakdown", Value::Arr( invoice .tax_breakdown .iter() .map(|group| { Value::obj(vec![ ("basisPoints", Value::Int(group.basis_points)), ("taxable", money_to_value(&group.taxable)), ("tax", money_to_value(&group.tax)), ]) }) .collect(), ), ), ]) } pub fn fune_vector(args: &[Value]) -> Value { let lines: Vec = args[0].as_arr().iter().map(invoice_line_from_value).collect(); invoice_to_value(&calculate_invoice( &lines, args[1].as_str(), args[2].as_str(), )) }