Functional Weave
Code in Python

property.completion-statement@1.0.0

impl/rust.rs

5,570 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::finance_proration::prorate;  ← from finance.proration ^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

/// A buyer's completion statement. Each outgoing is split at the end of the
/// completion day (the seller owns the property until then) by
/// finance.proration, so the two shares always add back to the charge.
///
/// # Panics
/// Panics on mixed currencies, a negative price, charge or fee, a deposit
/// outside 0..=price, a malformed date, or a period that ends before it starts.
pub fn completion_statement(
    price: &Money,
    deposit_paid: &Money,
    completion_date: &str,
    apportionments: &[PeriodCharge],
    fees: &[FeeLine],
) -> CompletionStatement {
    let currency = price.currency.as_str();
    let same = |m: &Money, what: &str| {
        if m.currency != currency {
            panic!("currency mismatch: {} is {}, the price is {}", what, m.currency, currency);
        }
    };
    same(deposit_paid, "depositPaid");
    if price.minor < 0 {
        panic!("price must not be negative, received {}", price.minor);
    }
    if deposit_paid.minor < 0 || deposit_paid.minor > price.minor {
        panic!("depositPaid must be between 0 and the price, received {}", deposit_paid.minor);
    }
    let mut balance = price.minor - deposit_paid.minor;
    let mut lines: Vec<ApportionmentLine> = Vec::new();
    for charge in apportionments {
        same(&charge.amount, &format!("\"{}\"", charge.label));
        if charge.amount.minor < 0 {
            panic!("\"{}\" must not be negative, received {}", charge.label, charge.amount.minor);
        }
        let total_days = days_between(&charge.period_start, &charge.period_end) + 1;
        if total_days < 1 {
            panic!(
                "\"{}\": periodEnd {} is before periodStart {}",
                charge.label, charge.period_end, charge.period_start
            );
        }
        // The seller owns the property to the end of the completion day.
        let up_to_completion = days_between(&charge.period_start, completion_date) + 1;
        let seller_days = total_days.min(up_to_completion.max(0));
        let split = prorate(&charge.amount, total_days, seller_days);
        let adjustment = if charge.paid_by_seller { split.unused.minor } else { -split.used.minor };
        balance += adjustment;
        lines.push(ApportionmentLine {
            label: charge.label.clone(),
            seller_days,
            buyer_days: total_days - seller_days,
            adjustment: money(adjustment, currency),
        });
    }
    let mut fees_total = 0i64;
    let mut fee_lines: Vec<FeeLine> = Vec::new();
    for fee in fees {
        same(&fee.amount, &format!("\"{}\"", fee.label));
        if fee.amount.minor < 0 {
            panic!("\"{}\" must not be negative, received {}", fee.label, fee.amount.minor);
        }
        fees_total += fee.amount.minor;
        fee_lines.push(FeeLine { label: fee.label.clone(), amount: money(fee.amount.minor, currency) });
    }
    CompletionStatement {
        price: money(price.minor, currency),
        deposit_paid: money(deposit_paid.minor, currency),
        apportionments: lines,
        balance_to_seller: money(balance, currency),
        fees: fee_lines,
        fees_total: money(fees_total, currency),
        total_due: money(balance + fees_total, currency),
    }
}

pub fn period_charge_from_value(v: &Value) -> PeriodCharge {
    PeriodCharge {
        label: v.get("label").as_str().to_string(),
        amount: money_from_value(v.get("amount")),
        period_start: v.get("periodStart").as_str().to_string(),
        period_end: v.get("periodEnd").as_str().to_string(),
        paid_by_seller: v.get("paidBySeller").as_bool(),
    }
}

pub fn fee_line_from_value(v: &Value) -> FeeLine {
    FeeLine { label: v.get("label").as_str().to_string(), amount: money_from_value(v.get("amount")) }
}

fn fee_line_to_value(fee: &FeeLine) -> Value {
    Value::obj(vec![("label", Value::str(&fee.label)), ("amount", money_to_value(&fee.amount))])
}

pub fn completion_statement_to_value(s: &CompletionStatement) -> Value {
    Value::obj(vec![
        ("price", money_to_value(&s.price)),
        ("depositPaid", money_to_value(&s.deposit_paid)),
        (
            "apportionments",
            Value::Arr(
                s.apportionments
                    .iter()
                    .map(|l| {
                        Value::obj(vec![
                            ("label", Value::str(&l.label)),
                            ("sellerDays", Value::Int(l.seller_days)),
                            ("buyerDays", Value::Int(l.buyer_days)),
                            ("adjustment", money_to_value(&l.adjustment)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("balanceToSeller", money_to_value(&s.balance_to_seller)),
        ("fees", Value::Arr(s.fees.iter().map(fee_line_to_value).collect())),
        ("feesTotal", money_to_value(&s.fees_total)),
        ("totalDue", money_to_value(&s.total_due)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let charges: Vec<PeriodCharge> = args[3].as_arr().iter().map(period_charge_from_value).collect();
    let fees: Vec<FeeLine> = args[4].as_arr().iter().map(fee_line_from_value).collect();
    completion_statement_to_value(&completion_statement(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
        args[2].as_str(),
        &charges,
        &fees,
    ))
}