Functional Weave
Code in Rust

invest.pension-annual-allowance@1.0.0

impl/rust.rs

11,382 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::dates_uk_tax_year::uk_tax_year;  ← from dates.uk-tax-year ^1.0.0 · built alongside by fune
use super::invest_pension_annual_allowance_data::{
    AnnualAllowanceRule, ANNUAL_ALLOWANCE_RULES, ANNUAL_ALLOWANCE_RULES_HISTORY, ANNUAL_ALLOWANCE_RULES_HORIZON,
};
use super::money_amount::{money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

/// How one tax year comes out against its own allowance, given the carry-forward it can reach.
struct YearTest {
    label: String,
    standard: i64,
    allowance: i64,
    tapered: bool,
    mpaa: Option<i64>,
    alternative: Option<i64>,
    total: i64,
    basis: &'static str,
    carry_forward_used: i64,
    excess: i64,
    /// What this year leaves to carry forward, and what it was tested against.
    unused: i64,
    tested_allowance: i64,
    tested_input: i64,
}

struct Ledger {
    tax_year: String,
    allowance: i64,
    input: i64,
    unused: i64,
    used_by_later_years: i64,
    used_this_year: i64,
}

fn gbp(minor: i64) -> Money {
    Money {
        minor,
        currency: "GBP".to_string(),
    }
}

fn check_money(value: &Money, name: &str, label: &str) -> i64 {
    if value.currency != "GBP" {
        panic!("{} must be in GBP, received {} for {}", name, value.currency, label);
    }
    if value.minor < 0 {
        panic!(
            "{} must be a whole number of pence, not negative, received {} for {}",
            name, value.minor, label
        );
    }
    value.minor
}

fn rule_for(start_year: i64) -> (&'static AnnualAllowanceRule, String) {
    let start = format!("{:04}-04-06", start_year);
    let label = uk_tax_year(&start).label;
    for rule in ANNUAL_ALLOWANCE_RULES {
        if rule.valid_from <= start.as_str() && rule.valid_to.map(|to| start.as_str() <= to).unwrap_or(true) {
            return (rule, label);
        }
    }
    let why = match ANNUAL_ALLOWANCE_RULES_HORIZON {
        Some(h) if start.as_str() < h => format!(
            ": this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier years.",
            ANNUAL_ALLOWANCE_RULES_HISTORY, h
        ),
        _ => String::new(),
    };
    panic!("no annual allowance rules for the {} tax year{}", label, why);
}

fn test(start_year: i64, year: &PensionInputYear, carry_forward: i64) -> YearTest {
    let (rule, label) = rule_for(start_year);
    let mp = check_money(&year.money_purchase_input, "moneyPurchaseInput", &label);
    let other = check_money(&year.other_input, "otherInput", &label);
    let threshold = check_money(&year.threshold_income, "thresholdIncome", &label);
    let adjusted = check_money(&year.adjusted_income, "adjustedIncome", &label);
    let total = mp + other;
    if !year.member && total > 0 {
        panic!(
            "pension inputs in {} need membership of a registered pension scheme in that year",
            label
        );
    }

    // Taper: one pound off for every two of adjusted income over the limit,
    // rounded down to a whole pound, when threshold income is also over its limit.
    let mut allowance = rule.annual_allowance;
    let tapered = threshold > rule.threshold_income && adjusted > rule.adjusted_income;
    if tapered {
        let reduction = (adjusted - rule.adjusted_income) / 200 * 100;
        allowance = rule.minimum_tapered_allowance.max(allowance - reduction);
    }

    let default_used = carry_forward.min((total - allowance).max(0));
    let default_excess = (total - allowance - default_used).max(0);
    if year.mpaa_applies && mp > rule.money_purchase_allowance {
        // Carry-forward can top up the alternative allowance, never the MPAA.
        let alternative = (allowance - rule.money_purchase_allowance).max(0);
        let alt_used = carry_forward.min((other - alternative).max(0));
        let alt_excess = mp - rule.money_purchase_allowance + (other - alternative - alt_used).max(0);
        let use_alternative = alt_excess > default_excess;
        return YearTest {
            label,
            standard: rule.annual_allowance,
            allowance,
            tapered,
            mpaa: Some(rule.money_purchase_allowance),
            alternative: Some(alternative),
            total,
            basis: if use_alternative { "alternative" } else { "standard" },
            carry_forward_used: if use_alternative { alt_used } else { default_used },
            excess: if use_alternative { alt_excess } else { default_excess },
            unused: if year.member { (alternative - other).max(0) } else { 0 },
            tested_allowance: alternative,
            tested_input: other,
        };
    }
    YearTest {
        label,
        standard: rule.annual_allowance,
        allowance,
        tapered,
        mpaa: if year.mpaa_applies { Some(rule.money_purchase_allowance) } else { None },
        alternative: None,
        total,
        basis: "standard",
        carry_forward_used: default_used,
        excess: default_excess,
        unused: if year.member { (allowance - total).max(0) } else { 0 },
        tested_allowance: allowance,
        tested_input: total,
    }
}

fn available(entry: &Ledger) -> i64 {
    entry.unused - entry.used_by_later_years - entry.used_this_year
}

/// Take `amount` from the entries' unused allowance, earliest year first.
fn consume(entries: &mut [Ledger], amount: i64, this_year: bool) {
    let mut left = amount;
    for entry in entries.iter_mut() {
        let take = left.min(available(entry));
        if this_year {
            entry.used_this_year += take;
        } else {
            entry.used_by_later_years += take;
        }
        left -= take;
    }
}

/// The annual allowance for a UK tax year, and how much of the year's pension
/// input amounts is subject to the annual allowance charge.
///
/// The earlier years are replayed oldest first, because an excess in one of
/// them uses up unused allowance from the years before it (earliest first),
/// and that is allowance the year being tested can no longer carry forward.
/// A year's own allowance is used before any carried forward; carry-forward
/// is then taken from the earliest of the three previous years first.
///
/// # Panics
/// Panics on fewer than three previous years, a tax year the rules do not
/// cover, amounts not in GBP or negative, or inputs in a year without
/// scheme membership.
pub fn pension_annual_allowance(
    tax_year_date: &str,
    current: &PensionInputYear,
    previous_years: &[PensionInputYear],
) -> AnnualAllowanceResult {
    let year = uk_tax_year(tax_year_date);
    let start_year: i64 = year.start[..4].parse().unwrap();
    if previous_years.len() < 3 {
        panic!(
            "previousYears must hold at least the three tax years before this one, oldest first, received {}",
            previous_years.len()
        );
    }
    let count = previous_years.len() as i64;
    let mut ledger: Vec<Ledger> = Vec::new();
    for (i, previous) in previous_years.iter().enumerate() {
        let from = i.saturating_sub(3);
        let reach: i64 = ledger[from..i].iter().map(available).sum();
        let result = test(start_year - count + i as i64, previous, reach);
        consume(&mut ledger[from..i], result.carry_forward_used, false);
        ledger.push(Ledger {
            tax_year: result.label.clone(),
            allowance: result.tested_allowance,
            input: result.tested_input,
            unused: result.unused,
            used_by_later_years: 0,
            used_this_year: 0,
        });
    }
    let from = ledger.len() - 3;
    let carry_forward_available: i64 = ledger[from..].iter().map(available).sum();
    let result = test(start_year, current, carry_forward_available);
    consume(&mut ledger[from..], result.carry_forward_used, true);

    let carry_forward: Vec<CarryForwardYear> = ledger[from..]
        .iter()
        .map(|e| CarryForwardYear {
            tax_year: e.tax_year.clone(),
            allowance: gbp(e.allowance),
            input: gbp(e.input),
            unused: gbp(e.unused),
            used_by_later_years: gbp(e.used_by_later_years),
            used_this_year: gbp(e.used_this_year),
        })
        .collect();
    AnnualAllowanceResult {
        tax_year: result.label.clone(),
        standard_allowance: gbp(result.standard),
        allowance: gbp(result.allowance),
        tapered: result.tapered,
        money_purchase_allowance: result.mpaa.map(gbp),
        alternative_allowance: result.alternative.map(gbp),
        total_input: gbp(result.total),
        basis: result.basis.to_string(),
        carry_forward,
        carry_forward_available: gbp(carry_forward_available),
        carry_forward_used: gbp(result.carry_forward_used),
        excess: gbp(result.excess),
    }
}

fn optional_money(value: &Option<Money>) -> Value {
    match value {
        Some(m) => money_to_value(m),
        None => Value::Null,
    }
}

pub fn carry_forward_year_to_value(y: &CarryForwardYear) -> Value {
    Value::obj(vec![
        ("taxYear", Value::str(&y.tax_year)),
        ("allowance", money_to_value(&y.allowance)),
        ("input", money_to_value(&y.input)),
        ("unused", money_to_value(&y.unused)),
        ("usedByLaterYears", money_to_value(&y.used_by_later_years)),
        ("usedThisYear", money_to_value(&y.used_this_year)),
    ])
}

pub fn annual_allowance_result_to_value(r: &AnnualAllowanceResult) -> Value {
    Value::obj(vec![
        ("taxYear", Value::str(&r.tax_year)),
        ("standardAllowance", money_to_value(&r.standard_allowance)),
        ("allowance", money_to_value(&r.allowance)),
        ("tapered", Value::Bool(r.tapered)),
        ("moneyPurchaseAllowance", optional_money(&r.money_purchase_allowance)),
        ("alternativeAllowance", optional_money(&r.alternative_allowance)),
        ("totalInput", money_to_value(&r.total_input)),
        ("basis", Value::str(&r.basis)),
        ("carryForward", Value::Arr(r.carry_forward.iter().map(carry_forward_year_to_value).collect())),
        ("carryForwardAvailable", money_to_value(&r.carry_forward_available)),
        ("carryForwardUsed", money_to_value(&r.carry_forward_used)),
        ("excess", money_to_value(&r.excess)),
    ])
}

fn whole_money(v: &Value, name: &str) -> Money {
    // Refuse a fractional number of pence with the wording TypeScript and Python use.
    if let Value::Float(f) = v.get("minor") {
        if f.fract() != 0.0 {
            panic!("{} must be a whole number of pence, not negative, received {}", name, f);
        }
    }
    money_from_value(v)
}

pub fn pension_input_year_from_value(v: &Value) -> PensionInputYear {
    PensionInputYear {
        money_purchase_input: whole_money(v.get("moneyPurchaseInput"), "moneyPurchaseInput"),
        other_input: whole_money(v.get("otherInput"), "otherInput"),
        threshold_income: whole_money(v.get("thresholdIncome"), "thresholdIncome"),
        adjusted_income: whole_money(v.get("adjustedIncome"), "adjustedIncome"),
        mpaa_applies: v.get("mpaaApplies").as_bool(),
        member: v.get("member").as_bool(),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let current = pension_input_year_from_value(&args[1]);
    let previous: Vec<PensionInputYear> = args[2].as_arr().iter().map(pension_input_year_from_value).collect();
    annual_allowance_result_to_value(&pension_annual_allowance(args[0].as_str(), &current, &previous))
}