invest.pension-annual-allowance@1.0.1
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(), ¤t, &previous))
}