Functional Weave
Code in TypeScript

construction.vat-reverse-charge@1.0.2

impl/rust.rs

5,431 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_vat_reverse_charge_data::{
    ReverseChargeRule, REVERSE_CHARGE_RULES, REVERSE_CHARGE_RULES_HISTORY, REVERSE_CHARGE_RULES_HORIZON,
};

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 not_applied(reason: &str) -> ReverseChargeDecision {
    ReverseChargeDecision {
        applies: false,
        reason: reason.to_string(),
        invoice_wording: None,
    }
}

/// Whether the UK VAT domestic reverse charge for building and construction
/// services (VATA 1994 s.55A) applies to one supply.
///
/// The conditions are checked in a fixed order and the first that fails is the
/// reason, so the answer is the same in every language and explains itself.
///
/// # Panics
/// Panics on a malformed date, a share outside 0..=10000 or an unknown category.
pub fn domestic_reverse_charge(supply: &ReverseChargeSupply) -> ReverseChargeDecision {
    if !is_iso_date(&supply.supply_date) {
        panic!(
            "supplyDate must be an ISO date (YYYY-MM-DD), received \"{}\"",
            supply.supply_date
        );
    }
    let share = supply.reverse_charge_share_basis_points;
    if !(0..=10000).contains(&share) {
        panic!(
            "reverseChargeShareBasisPoints must be between 0 and 10000, received {}",
            share
        );
    }
    let category = supply.vat_category.as_str();
    if !["standard", "reduced", "zero", "exempt"].contains(&category) {
        panic!(
            "unknown vatCategory \"{}\": expected standard, reduced, zero or exempt",
            category
        );
    }

    let date = supply.supply_date.as_str();
    let mut rule: Option<&ReverseChargeRule> = None;
    for row in REVERSE_CHARGE_RULES.iter() {
        if row.jurisdiction != "GB" || date < row.valid_from {
            continue;
        }
        if let Some(valid_to) = row.valid_to {
            if date > valid_to {
                continue;
            }
        }
        if rule.map_or(true, |r| row.valid_from > r.valid_from) {
            rule = Some(row);
        }
    }
    let rule = match rule {
        Some(rule) => rule,
        None => {
            // A pruned build must not answer "before the reverse charge started"
            // for a date it simply no longer carries rules for.
            if let Some(horizon) = REVERSE_CHARGE_RULES_HORIZON {
                if date < horizon {
                    panic!(
                        "no reverse charge rule for {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full.",
                        date, REVERSE_CHARGE_RULES_HISTORY, horizon
                    );
                }
            }
            return not_applied("before-start");
        }
    };

    if !supply.supplier_vat_registered {
        return not_applied("supplier-not-vat-registered");
    }
    if !supply.customer_vat_registered {
        return not_applied("customer-not-vat-registered");
    }
    if !supply.reported_under_cis {
        return not_applied("not-reported-under-cis");
    }
    if category != "standard" && category != "reduced" {
        return not_applied("not-standard-or-reduced");
    }
    if supply.employment_business {
        return not_applied("employment-business");
    }
    if supply.end_user_notified {
        return not_applied("end-user-notified");
    }
    if share <= rule.disregard_basis_points {
        return not_applied("five-percent-disregard");
    }
    ReverseChargeDecision {
        applies: true,
        reason: "reverse-charge".to_string(),
        invoice_wording: Some(rule.invoice_wording.to_string()),
    }
}

pub fn reverse_charge_supply_from_value(v: &Value) -> ReverseChargeSupply {
    let share = v.get("reverseChargeShareBasisPoints");
    if let Value::Float(f) = share {
        panic!("reverseChargeShareBasisPoints must be an integer, received {}", f);
    }
    let date = v.get("supplyDate");
    if !matches!(date, Value::Str(_)) {
        panic!("supplyDate must be an ISO date (YYYY-MM-DD), received \"{:?}\"", date);
    }
    ReverseChargeSupply {
        supply_date: date.as_str().to_string(),
        supplier_vat_registered: v.get("supplierVatRegistered").as_bool(),
        customer_vat_registered: v.get("customerVatRegistered").as_bool(),
        reported_under_cis: v.get("reportedUnderCis").as_bool(),
        vat_category: v.get("vatCategory").as_str().to_string(),
        employment_business: v.get("employmentBusiness").as_bool(),
        end_user_notified: v.get("endUserNotified").as_bool(),
        reverse_charge_share_basis_points: share.as_i64(),
    }
}

pub fn reverse_charge_decision_to_value(decision: &ReverseChargeDecision) -> Value {
    Value::obj(vec![
        ("applies", Value::Bool(decision.applies)),
        ("reason", Value::str(&decision.reason)),
        (
            "invoiceWording",
            match &decision.invoice_wording {
                Some(text) => Value::str(text),
                None => Value::Null,
            },
        ),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    reverse_charge_decision_to_value(&domestic_reverse_charge(&reverse_charge_supply_from_value(&args[0])))
}