Functional Weave
Code in Python

charity.gift-aid-eligibility@1.0.0

impl/rust.rs

4,547 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::charity_gift_aid_eligibility_data::{BenefitLimit, BENEFIT_LIMITS, BENEFIT_LIMITS_HISTORY};  ← this capability’s own data, compiled from data/benefit-limits.json into the same file by fune build
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn is_iso_date(s: &str) -> bool {
    let b = s.as_bytes();
    b.len() == 10
        && b.iter().enumerate().all(|(i, c)| if i == 4 || i == 7 { *c == b'-' } else { c.is_ascii_digit() })
}

fn limits_on(on_date: &str) -> &'static BenefitLimit {
    for row in BENEFIT_LIMITS {
        if row.valid_from <= on_date && row.valid_to.map_or(true, |to| on_date <= to) {
            return row;
        }
    }
    if BENEFIT_LIMITS_HISTORY != "full" {
        if let Some(earliest) = BENEFIT_LIMITS.iter().map(|r| r.valid_from).min() {
            if on_date < earliest {
                panic!(
                    "no Gift Aid benefit limits on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.",
                    on_date, BENEFIT_LIMITS_HISTORY, earliest
                );
            }
        }
    }
    panic!("no Gift Aid benefit limits on {}", on_date);
}

fn check_gbp(name: &str, amount: &Money) {
    if amount.currency != "GBP" {
        panic!("Gift Aid amounts must be in GBP, received {} for {}", amount.currency, name);
    }
}

/// Whether a donation qualifies for Gift Aid (ITA 2007 ss416-418). The benefit
/// test is done on the exact limit, scaled by 10000, so 25% or 5% of an odd
/// number of pence is never rounded before the comparison.
///
/// # Panics
/// Panics on a non-GBP amount, a donation that is not positive, a negative
/// benefit, a malformed date, or a date with no limits on file.
pub fn gift_aid_eligibility(
    donation: &Money,
    benefit_value: &Money,
    prior_benefits_this_year: &Money,
    has_declaration: bool,
    donor_pays_enough_tax: bool,
    on_date: &str,
) -> GiftAidEligibility {
    check_gbp("donation", donation);
    check_gbp("benefitValue", benefit_value);
    check_gbp("priorBenefitsThisYear", prior_benefits_this_year);
    if donation.minor <= 0 {
        panic!("donation must be positive, received {}", donation.minor);
    }
    if benefit_value.minor < 0 {
        panic!("benefitValue must not be negative, received {}", benefit_value.minor);
    }
    if prior_benefits_this_year.minor < 0 {
        panic!("priorBenefitsThisYear must not be negative, received {}", prior_benefits_this_year.minor);
    }
    if !is_iso_date(on_date) {
        panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    let rule = limits_on(on_date);

    // The variable limit, in pence x 10000.
    let scaled_limit: i64 = if donation.minor <= rule.small_gift_up_to {
        donation.minor * rule.small_gift_basis_points
    } else {
        match (rule.middle_gift_up_to, rule.middle_gift_fixed) {
            (Some(up_to), Some(fixed)) if donation.minor <= up_to => fixed * 10000,
            _ => rule.excess_fixed * 10000 + (donation.minor - rule.excess_from) * rule.excess_basis_points,
        }
    };

    let mut reasons: Vec<String> = Vec::new();
    if !has_declaration {
        reasons.push("no-declaration".to_string());
    }
    if !donor_pays_enough_tax {
        reasons.push("insufficient-tax".to_string());
    }
    // "exceeds" and "is more than": a benefit exactly at a limit is allowed.
    if benefit_value.minor * 10000 > scaled_limit {
        reasons.push("benefit-over-limit".to_string());
    }
    if benefit_value.minor + prior_benefits_this_year.minor > rule.annual_limit {
        reasons.push("annual-benefits-over-limit".to_string());
    }

    GiftAidEligibility {
        eligible: reasons.is_empty(),
        reasons,
        benefit_limit: money(scaled_limit.div_euclid(10000), "GBP"),
        annual_benefit_limit: money(rule.annual_limit, "GBP"),
    }
}

pub fn gift_aid_eligibility_to_value(e: &GiftAidEligibility) -> Value {
    Value::obj(vec![
        ("eligible", Value::Bool(e.eligible)),
        ("reasons", Value::Arr(e.reasons.iter().map(|r| Value::str(r)).collect())),
        ("benefitLimit", money_to_value(&e.benefit_limit)),
        ("annualBenefitLimit", money_to_value(&e.annual_benefit_limit)),
    ])
}

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