Functional Weave
Code in Rust

finance.aged-debt@1.0.0

impl/rust.rs

4,122 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::dates_days_between::days_between;  ← from dates.days-between ^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};  ← from money.amount ^1.0.0 · built alongside by fune

/// Bucket open invoices by how many days past their due date they are.
///
/// Age runs from the due date, not the invoice date: an invoice due today is
/// current. Every bucket is returned, empty or not, so a report's columns are
/// fixed.
///
/// # Panics
/// Panics on bad bucket bounds, a malformed date or mixed currencies.
pub fn aged_debt(invoices: &[OpenInvoice], as_at: &str, bucket_days: &[i64], currency: &str) -> AgedDebt {
    if bucket_days.is_empty() {
        panic!("bucketDays needs at least one boundary");
    }
    let mut previous = 0;
    for &bound in bucket_days {
        if bound <= previous {
            panic!(
                "bucketDays must be whole days, ascending, from 1: received {} after {}",
                bound, previous
            );
        }
        previous = bound;
    }

    let zero = money(0, currency);
    let mut buckets: Vec<AgedDebtBucket> = vec![AgedDebtBucket {
        label: "current".to_string(),
        from_days: None,
        to_days: Some(0),
        total: zero.clone(),
        references: Vec::new(),
    }];
    let mut start = 1;
    for &bound in bucket_days {
        buckets.push(AgedDebtBucket {
            label: format!("{}-{}", start, bound),
            from_days: Some(start),
            to_days: Some(bound),
            total: zero.clone(),
            references: Vec::new(),
        });
        start = bound + 1;
    }
    buckets.push(AgedDebtBucket {
        label: format!("{}+", start - 1),
        from_days: Some(start),
        to_days: None,
        total: zero.clone(),
        references: Vec::new(),
    });

    let mut total = zero;
    for invoice in invoices {
        let overdue = days_between(&invoice.due_date, as_at);
        let mut index = 0;
        if overdue > 0 {
            index = bucket_days.len() + 1;
            for (i, &bound) in bucket_days.iter().enumerate() {
                if overdue <= bound {
                    index = i + 1;
                    break;
                }
            }
        }
        let bucket = &mut buckets[index];
        bucket.total = add_money(&bucket.total, &invoice.outstanding);
        bucket.references.push(invoice.reference.clone());
        total = add_money(&total, &invoice.outstanding);
    }
    AgedDebt {
        as_at: as_at.to_string(),
        buckets,
        total,
    }
}

fn optional_int(value: Option<i64>) -> Value {
    match value {
        Some(n) => Value::Int(n),
        None => Value::Null,
    }
}

pub fn open_invoice_from_value(v: &Value) -> OpenInvoice {
    OpenInvoice {
        reference: v.get("reference").as_str().to_string(),
        due_date: v.get("dueDate").as_str().to_string(),
        outstanding: money_from_value(v.get("outstanding")),
    }
}

pub fn aged_debt_to_value(report: &AgedDebt) -> Value {
    Value::obj(vec![
        ("asAt", Value::str(&report.as_at)),
        (
            "buckets",
            Value::Arr(
                report
                    .buckets
                    .iter()
                    .map(|b| {
                        Value::obj(vec![
                            ("label", Value::str(&b.label)),
                            ("fromDays", optional_int(b.from_days)),
                            ("toDays", optional_int(b.to_days)),
                            ("total", money_to_value(&b.total)),
                            ("references", Value::Arr(b.references.iter().map(|r| Value::str(r)).collect())),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("total", money_to_value(&report.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let invoices: Vec<OpenInvoice> = args[0].as_arr().iter().map(open_invoice_from_value).collect();
    let bounds: Vec<i64> = args[2].as_arr().iter().map(|v| v.as_i64()).collect();
    aged_debt_to_value(&aged_debt(&invoices, args[1].as_str(), &bounds, args[3].as_str()))
}