Functional Weave
Code in TypeScript

finance.accruals-prepayments@1.0.0

impl/rust.rs

3,299 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::dates_overlap::overlap_days;  ← from dates.overlap ^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

/// Split an invoice across accounting periods by days of service.
///
/// money.allocate does the split with each period's days as the weight, so the
/// shares add back to the invoice exactly. A negative amount is split as its
/// positive and negated, so a credit note mirrors its invoice period by period.
///
/// # Panics
/// Panics on malformed dates, a service that ends before it starts, periods
/// that overlap, are out of order or leave days of service uncovered.
pub fn allocate_to_periods(
    amount: &Money,
    service_start: &str,
    service_end: &str,
    periods: &[AccountingPeriod],
) -> Vec<PeriodAllocation> {
    let service_days = days_between(service_start, service_end) + 1;
    if service_days < 1 {
        panic!("service ends {}, before it starts {}", service_end, service_start);
    }
    if periods.is_empty() {
        panic!("at least one accounting period is needed");
    }
    let mut days: Vec<i64> = Vec::with_capacity(periods.len());
    for (index, period) in periods.iter().enumerate() {
        if days_between(&period.start, &period.end) < 0 {
            panic!("period {} ends before it starts", period.label);
        }
        if index > 0 && days_between(&periods[index - 1].end, &period.start) < 1 {
            panic!("period {} overlaps or precedes the period before it", period.label);
        }
        days.push(overlap_days(service_start, service_end, &period.start, &period.end, true));
    }
    let covered: i64 = days.iter().sum();
    if covered != service_days {
        panic!(
            "the accounting periods cover {} of the {} days of service",
            covered, service_days
        );
    }

    let negative = amount.minor < 0;
    let magnitude = money(if negative { -amount.minor } else { amount.minor }, &amount.currency);
    let shares = allocate(&magnitude, &days);
    periods
        .iter()
        .enumerate()
        .map(|(index, period)| PeriodAllocation {
            label: period.label.clone(),
            days: days[index],
            amount: if negative {
                money(-shares[index].minor, &amount.currency)
            } else {
                shares[index].clone()
            },
        })
        .collect()
}

pub fn accounting_period_from_value(v: &Value) -> AccountingPeriod {
    AccountingPeriod {
        label: v.get("label").as_str().to_string(),
        start: v.get("start").as_str().to_string(),
        end: v.get("end").as_str().to_string(),
    }
}

pub fn period_allocation_to_value(a: &PeriodAllocation) -> Value {
    Value::obj(vec![
        ("label", Value::str(&a.label)),
        ("days", Value::Int(a.days)),
        ("amount", money_to_value(&a.amount)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let periods: Vec<AccountingPeriod> = args[3].as_arr().iter().map(accounting_period_from_value).collect();
    Value::Arr(
        allocate_to_periods(&money_from_value(&args[0]), args[1].as_str(), args[2].as_str(), &periods)
            .iter()
            .map(period_allocation_to_value)
            .collect(),
    )
}