Functional Weave
Code in TypeScript

construction.valuation@1.0.0

impl/rust.rs

5,191 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::{cis_deduction, cis_deduction_to_value, CisDeduction};  ← from construction.cis-deduction ^1.0.0 · built alongside by fune
use super::construction_retention::{retention, retention_to_value};  ← from construction.retention ^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, subtract_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

/// An interim valuation, from the cumulative value of the work to what is paid.
///
/// The order is the whole point: retention is taken on the cumulative gross,
/// previous certificates come off the net, VAT goes on the amount due, and the
/// CIS deduction comes off the labour in the amount due, never off the VAT.
/// Each step is its own capability; this one only fixes the order.
///
/// # Panics
/// Panics on mixed currencies, a reverse charge without a standard or reduced
/// VAT category, a negative amount due under CIS, and whatever the retention,
/// VAT and CIS capabilities refuse.
pub fn interim_valuation(input: &ValuationInput) -> Valuation {
    let gross_valuation = add_money(&input.work_done, &input.materials_on_site);
    let held = retention(&gross_valuation, input.retention_basis_points, &input.stage);
    let net_valuation = subtract_money(&gross_valuation, &held.held);
    let amount_due = subtract_money(&net_valuation, &input.previously_certified);
    let currency = amount_due.currency.clone();

    let mut vat = money(0, &currency);
    let mut reverse_charge_vat = money(0, &currency);
    let mut vat_basis_points = 0;
    match &input.vat_category {
        None => {
            if input.reverse_charge {
                panic!("the reverse charge needs a VAT category: the supplier must be VAT registered");
            }
        }
        Some(category) => {
            if input.reverse_charge && category != "STANDARD" && category != "REDUCED" {
                panic!(
                    "the reverse charge applies only to STANDARD or REDUCED supplies, received {}",
                    category
                );
            }
            let breakdown = add_vat(&amount_due, "GB", category, &input.valuation_date);
            vat_basis_points = breakdown.basis_points;
            if input.reverse_charge {
                reverse_charge_vat = breakdown.tax;
            } else {
                vat = breakdown.tax;
            }
        }
    }

    let cis: Option<CisDeduction> = match &input.cis_status {
        None => None,
        Some(status) => {
            if amount_due.minor < 0 {
                panic!(
                    "the amount due is negative ({}), so there is no payment to make a CIS deduction from",
                    amount_due.minor
                );
            }
            Some(cis_deduction(
                &amount_due,
                &input.cis_materials,
                status,
                &input.valuation_date,
                "down",
            ))
        }
    };

    let payable = add_money(&amount_due, &vat);
    let amount_payable = match &cis {
        None => payable,
        Some(c) => subtract_money(&payable, &c.deduction),
    };
    Valuation {
        gross_valuation,
        retention: held,
        net_valuation,
        previously_certified: input.previously_certified.clone(),
        amount_due,
        vat,
        vat_basis_points,
        reverse_charge_vat,
        cis,
        amount_payable,
    }
}

fn optional_string(v: &Value) -> Option<String> {
    if v.is_null() {
        None
    } else {
        Some(v.as_str().to_string())
    }
}

pub fn valuation_input_from_value(v: &Value) -> ValuationInput {
    ValuationInput {
        work_done: money_from_value(v.get("workDone")),
        materials_on_site: money_from_value(v.get("materialsOnSite")),
        retention_basis_points: v.get("retentionBasisPoints").as_i64(),
        stage: v.get("stage").as_str().to_string(),
        previously_certified: money_from_value(v.get("previouslyCertified")),
        cis_status: optional_string(v.get("cisStatus")),
        cis_materials: money_from_value(v.get("cisMaterials")),
        vat_category: optional_string(v.get("vatCategory")),
        reverse_charge: v.get("reverseCharge").as_bool(),
        valuation_date: v.get("valuationDate").as_str().to_string(),
    }
}

pub fn valuation_to_value(v: &Valuation) -> Value {
    Value::obj(vec![
        ("grossValuation", money_to_value(&v.gross_valuation)),
        ("retention", retention_to_value(&v.retention)),
        ("netValuation", money_to_value(&v.net_valuation)),
        ("previouslyCertified", money_to_value(&v.previously_certified)),
        ("amountDue", money_to_value(&v.amount_due)),
        ("vat", money_to_value(&v.vat)),
        ("vatBasisPoints", Value::Int(v.vat_basis_points)),
        ("reverseChargeVat", money_to_value(&v.reverse_charge_vat)),
        (
            "cis",
            match &v.cis {
                None => Value::Null,
                Some(c) => cis_deduction_to_value(c),
            },
        ),
        ("amountPayable", money_to_value(&v.amount_payable)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    valuation_to_value(&interim_valuation(&valuation_input_from_value(&args[0])))
}