Functional Weave
Code in Python

charity.gasds@1.0.2

impl/rust.rs

4,841 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_gasds_data::{GasdsLimit, GASDS_LIMITS, GASDS_LIMITS_HISTORY};  ← this capability’s own data, compiled from data/gasds-limits.json into the same file by fune build
use super::charity_gift_aid::gift_aid;  ← from charity.gift-aid ^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 limits_on(on_date: &str) -> &'static GasdsLimit {
    for row in GASDS_LIMITS {
        if row.valid_from <= on_date && row.valid_to.map_or(true, |to| on_date <= to) {
            return row;
        }
    }
    if GASDS_LIMITS_HISTORY != "full" {
        if let Some(earliest) = GASDS_LIMITS.iter().map(|r| r.valid_from).min() {
            if on_date < earliest {
                panic!(
                    "no GASDS limits on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.",
                    on_date, GASDS_LIMITS_HISTORY, earliest
                );
            }
        }
    }
    panic!("no GASDS limits on {}", on_date);
}

fn check_amount(name: &str, amount: &Money) {
    if amount.currency != "GBP" {
        panic!("GASDS amounts must be in GBP, received {} for {}", amount.currency, name);
    }
    if amount.minor < 0 {
        panic!("{} must not be negative, received {}", name, amount.minor);
    }
}

/// The GASDS top-up for one tax year (Small Charitable Donations Act 2012 s1):
/// paid on small donations up to the lower of 10 times the Gift Aid donations
/// claimed in the same year (the matching rule) and the specified amount,
/// which for a charity active in more than one community building may be the
/// sum of each building's local donations, up to £8,000 each (s6).
///
/// # Panics
/// Panics on a non-GBP or negative amount, a malformed date, a date with no
/// limits on file, community buildings before 2017-18, or community building
/// donations that add up to more than the small donations.
pub fn gasds(
    small_donations: &Money,
    gift_aid_donations: &Money,
    community_building_donations: &[Money],
    on_date: &str,
) -> GasdsClaim {
    check_amount("smallDonations", small_donations);
    check_amount("giftAidDonations", gift_aid_donations);
    for d in community_building_donations {
        check_amount("communityBuildingDonations", d);
    }
    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);
    if !community_building_donations.is_empty() && !rule.community_buildings {
        panic!("community building claims before 6 April 2017 are not supported, received a claim for {}", on_date);
    }
    let in_buildings: i64 = community_building_donations.iter().map(|d| d.minor).sum();
    if in_buildings > small_donations.minor {
        panic!(
            "communityBuildingDonations total {}, more than smallDonations of {}",
            in_buildings, small_donations.minor
        );
    }

    let mut specified = rule.specified_amount;
    let mut used_community_buildings = false;
    // s6 applies only to a charity active in more than one community building.
    if community_building_donations.len() >= 2 {
        let buildings: i64 = community_building_donations
            .iter()
            .map(|d| d.minor.min(rule.specified_amount))
            .sum();
        if buildings > specified {
            specified = buildings;
            used_community_buildings = true;
        }
    }
    let maximum = (10 * gift_aid_donations.minor).min(specified);
    let eligible = small_donations.minor.min(maximum);
    GasdsClaim {
        eligible_donations: money(eligible, "GBP"),
        top_up: gift_aid(&money(eligible, "GBP"), on_date).tax_reclaimable,
        maximum_donations: money(maximum, "GBP"),
        specified_amount: money(specified, "GBP"),
        small_donation_limit: money(rule.small_donation_limit, "GBP"),
        used_community_buildings,
    }
}

pub fn gasds_claim_to_value(c: &GasdsClaim) -> Value {
    Value::obj(vec![
        ("eligibleDonations", money_to_value(&c.eligible_donations)),
        ("topUp", money_to_value(&c.top_up)),
        ("maximumDonations", money_to_value(&c.maximum_donations)),
        ("specifiedAmount", money_to_value(&c.specified_amount)),
        ("smallDonationLimit", money_to_value(&c.small_donation_limit)),
        ("usedCommunityBuildings", Value::Bool(c.used_community_buildings)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let buildings: Vec<Money> = args[2].as_arr().iter().map(money_from_value).collect();
    gasds_claim_to_value(&gasds(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
        &buildings,
        args[3].as_str(),
    ))
}