Functional Weave
Code in TypeScript

charity.gift-aid@1.0.2

impl/rust.rs

3,133 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_data::{BASIC_RATE, BASIC_RATE_HISTORY};  ← this capability’s own data, compiled from data/basic-rate.json into the same file by fune build
use super::math_round_div::round_div;  ← from math.round-div ^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

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 basic_rate(on_date: &str) -> i64 {
    for row in BASIC_RATE {
        if row.valid_from <= on_date && row.valid_to.map_or(true, |to| on_date <= to) {
            return row.basis_points;
        }
    }
    // A history=current build carries only the rate in force; answering an old
    // gift with today's rate would be a quiet wrong claim, so say why instead.
    if BASIC_RATE_HISTORY != "full" {
        if let Some(earliest) = BASIC_RATE.iter().map(|r| r.valid_from).min() {
            if on_date < earliest {
                panic!(
                    "no Gift Aid basic rate on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.",
                    on_date, BASIC_RATE_HISTORY, earliest
                );
            }
        }
    }
    panic!("no Gift Aid basic rate on {}", on_date);
}

/// The tax a charity reclaims on a Gift Aid donation. The donor is treated as
/// having given the gift after deducting basic rate tax (ITA 2007 s414), so the
/// charity reclaims donation x rate / (100% - rate): 25p per £1 at 20%. It is
/// always the UK basic rate, for Scottish and Welsh taxpayers too.
///
/// # Panics
/// Panics on a non-GBP or negative donation, a malformed date, or a date with
/// no basic rate on file.
pub fn gift_aid(donation: &Money, on_date: &str) -> GiftAid {
    if donation.currency != "GBP" {
        panic!("Gift Aid is claimed in GBP, received {}", donation.currency);
    }
    if donation.minor < 0 {
        panic!("donation must not be negative, received {}", donation.minor);
    }
    if !is_iso_date(on_date) {
        panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    let rate = basic_rate(on_date);
    // Rounded down: a claim may never exceed the tax treated as deducted.
    let tax = round_div(donation.minor * rate, 10000 - rate, "down");
    GiftAid {
        donation: money(donation.minor, "GBP"),
        tax_reclaimable: money(tax, "GBP"),
        gross: money(donation.minor + tax, "GBP"),
        basic_rate_basis_points: rate,
    }
}

pub fn gift_aid_to_value(g: &GiftAid) -> Value {
    Value::obj(vec![
        ("donation", money_to_value(&g.donation)),
        ("taxReclaimable", money_to_value(&g.tax_reclaimable)),
        ("gross", money_to_value(&g.gross)),
        ("basicRateBasisPoints", Value::Int(g.basic_rate_basis_points)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = args[0].get("minor") {
        if f.fract() != 0.0 {
            panic!("money() takes integer minor units, received {}", f);
        }
    }
    gift_aid_to_value(&gift_aid(&money_from_value(&args[0]), args[1].as_str()))
}