use super::funejson::Value; use super::dates_uk_tax_year::uk_tax_year; 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}; /// 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, alternative: Option, 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 = 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 = 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) -> 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 = 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)) }