Functional Weave
Code in TypeScript

finance.invoice.calculate@1.0.1

impl/rust.rs

4,984 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_invoice_line_total::line_total;  ← from finance.invoice.line-total ^1.0.0 · built alongside by fune
use super::finance_tax_add_vat::add_vat;  ← from finance.tax.add-vat ^1.0.0 · built alongside by fune
use super::money_add::add_money;  ← from money.add ^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

/// 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<CalculatedLine> = 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<Money> = calculated.iter().map(|l| l.net.clone()).collect();
    let taxes: Vec<Money> = calculated.iter().map(|l| l.tax.clone()).collect();
    let subtotal = sum_money(&nets, &currency);
    let tax_total = sum_money(&taxes, &currency);

    // 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<TaxGroup> = 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<InvoiceLine> = 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(),
    ))
}