Functional Weave
Code in Python

construction.cis-deduction@1.0.2

impl/rust.rs

4,288 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::construction_cis_deduction_data::{CIS_RATES, CIS_RATES_HISTORY};  ← this capability’s own data, compiled from data/cis-rates.json into the same file by fune build
use super::money_add::subtract_money;  ← from money.add ^1.0.0 · built alongside by fune
use super::money_amount::{money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate;  ← from money.apply-rate ^1.0.0 · built alongside by fune

fn is_iso_date(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() == 10
        && bytes[4] == b'-'
        && bytes[7] == b'-'
        && bytes
            .iter()
            .enumerate()
            .all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}

fn rate_on(status: &str, on_date: &str) -> i64 {
    let mut best: Option<(i64, &'static str)> = None;
    let mut earliest: Option<&'static str> = None;
    for rule in CIS_RATES {
        if rule.status != status {
            continue;
        }
        if earliest.map_or(true, |e| rule.valid_from < e) {
            earliest = Some(rule.valid_from);
        }
        if on_date < rule.valid_from {
            continue;
        }
        if let Some(valid_to) = rule.valid_to {
            if on_date > valid_to {
                continue;
            }
        }
        if best.map_or(true, |(_, from)| rule.valid_from > from) {
            best = Some((rule.basis_points, rule.valid_from));
        }
    }
    if let Some((basis_points, _)) = best {
        return basis_points;
    }
    // A history=current build has dropped the old rows; say so rather than
    // answering an old payment at today's rate.
    if CIS_RATES_HISTORY != "full" {
        if let Some(earliest) = earliest {
            if on_date < earliest {
                panic!(
                    "no CIS rate for {} on {}: this build was installed with history={}, so it only carries rates from {}. Reinstall with history=full for older payments.",
                    status, on_date, CIS_RATES_HISTORY, earliest
                );
            }
        }
    }
    panic!("no CIS rate for {} on {}", status, on_date)
}

/// The CIS deduction a contractor withholds from a subcontractor's payment.
///
/// The rate applies only to the labour: VAT is left out by taking the payment
/// net of VAT, and materials (with consumables, non-travel fuel, plant hire and
/// prefabricated materials) are taken off before the rate is applied. Applying
/// 20% to the whole invoice is the classic over-deduction.
///
/// # Panics
/// Panics on an unknown status, a malformed date, a negative payment or
/// materials, materials above the payment, mixed currencies, or a date no rate
/// covers.
pub fn cis_deduction(payment: &Money, materials: &Money, status: &str, on_date: &str, mode: &str) -> CisDeduction {
    if status != "gross" && status != "standard" && status != "higher" {
        panic!("unknown CIS status \"{}\"", status);
    }
    if !is_iso_date(on_date) {
        panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    if payment.minor < 0 {
        panic!("payment must not be negative, received {}", payment.minor);
    }
    if materials.minor < 0 {
        panic!("materials must not be negative, received {}", materials.minor);
    }
    let labour = subtract_money(payment, materials);
    if labour.minor < 0 {
        panic!(
            "materials ({}) cannot exceed the payment ({})",
            materials.minor, payment.minor
        );
    }
    let basis_points = rate_on(status, on_date);
    let deduction = apply_rate(&labour, basis_points, mode);
    CisDeduction {
        payment: payment.clone(),
        materials: materials.clone(),
        labour,
        basis_points,
        net_payment: subtract_money(payment, &deduction),
        deduction,
    }
}

pub fn cis_deduction_to_value(d: &CisDeduction) -> Value {
    Value::obj(vec![
        ("payment", money_to_value(&d.payment)),
        ("materials", money_to_value(&d.materials)),
        ("labour", money_to_value(&d.labour)),
        ("basisPoints", Value::Int(d.basis_points)),
        ("deduction", money_to_value(&d.deduction)),
        ("netPayment", money_to_value(&d.net_payment)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    cis_deduction_to_value(&cis_deduction(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
        args[2].as_str(),
        args[3].as_str(),
        args[4].as_str(),
    ))
}