education.student-loan-repayment@1.0.0
impl/rust.rs
4,759 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::education_student_loan_repayment_data::{
UNEARNED_INCOME_LIMITS, UNEARNED_INCOME_LIMITS_HISTORY, UNEARNED_INCOME_LIMITS_HORIZON,
};
use super::money_amount::{money, money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
use super::payroll_student_loan_data::{
STUDENT_LOAN_THRESHOLDS, STUDENT_LOAN_THRESHOLDS_HISTORY, STUDENT_LOAN_THRESHOLDS_HORIZON,
};
fn gbp(value: &Money, name: &str) -> i64 {
if value.currency != "GBP" {
panic!("{} must be in GBP, received {}", name, value.currency);
}
if value.minor < 0 {
panic!("{} must not be negative, received {}", name, value.minor);
}
value.minor
}
/// One plan's repayment for a tax year, from the whole year's income.
///
/// The threshold is payroll.student-loan's row in force on 6 April of the tax
/// year; the repayment drops its pence, as HMRC does.
///
/// # Panics
/// Panics on a non-GBP or negative amount, an unknown plan, a malformed date,
/// or a tax year the data does not cover.
pub fn student_loan_repayment(
earned_income: &Money,
unearned_income: &Money,
plan: &str,
tax_year_date: &str,
deducted_through_paye: &Money,
) -> StudentLoanRepayment {
let earned = gbp(earned_income, "earnedIncome");
let unearned = gbp(unearned_income, "unearnedIncome");
let deducted = gbp(deducted_through_paye, "deductedThroughPaye");
if !matches!(plan, "plan-1" | "plan-2" | "plan-4" | "plan-5" | "postgraduate") {
panic!("unknown student loan plan \"{}\"", plan);
}
let year = uk_tax_year(tax_year_date);
let start = year.start.as_str();
let rule = STUDENT_LOAN_THRESHOLDS
.iter()
.find(|r| r.plan == plan && start >= r.valid_from && r.valid_to.map_or(true, |to| start <= to));
let rule = match rule {
Some(r) => r,
None => {
if let Some(horizon) = STUDENT_LOAN_THRESHOLDS_HORIZON {
if STUDENT_LOAN_THRESHOLDS_HISTORY != "full" && start < horizon {
panic!(
"no student loan threshold for {} in tax year {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.",
plan, year.label, STUDENT_LOAN_THRESHOLDS_HISTORY, horizon
);
}
}
panic!("no student loan threshold for {} in tax year {}", plan, year.label)
}
};
let limit = UNEARNED_INCOME_LIMITS
.iter()
.find(|r| start >= r.valid_from && r.valid_to.map_or(true, |to| start <= to));
let limit = match limit {
Some(r) => r,
None => {
if let Some(horizon) = UNEARNED_INCOME_LIMITS_HORIZON {
if UNEARNED_INCOME_LIMITS_HISTORY != "full" && start < horizon {
panic!(
"no unearned income limit for tax year {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.",
year.label, UNEARNED_INCOME_LIMITS_HISTORY, horizon
);
}
}
panic!("no unearned income limit for tax year {}", year.label)
}
};
let counted = earned + if unearned > limit.limit { unearned } else { 0 };
let excess = counted as i128 - rule.annual_threshold as i128;
let pounds = if excess <= 0 { 0 } else { (excess * rule.basis_points as i128) / (10000 * 100) };
let repayment = (pounds * 100) as i64;
StudentLoanRepayment {
tax_year: year.label.clone(),
threshold: money(rule.annual_threshold, "GBP"),
basis_points: rule.basis_points,
counted_income: money(counted, "GBP"),
repayment: money(repayment, "GBP"),
deducted_through_paye: money(deducted, "GBP"),
balance: money(repayment - deducted, "GBP"),
}
}
pub fn student_loan_repayment_to_value(r: &StudentLoanRepayment) -> Value {
Value::obj(vec![
("taxYear", Value::str(&r.tax_year)),
("threshold", money_to_value(&r.threshold)),
("basisPoints", Value::Int(r.basis_points)),
("countedIncome", money_to_value(&r.counted_income)),
("repayment", money_to_value(&r.repayment)),
("deductedThroughPaye", money_to_value(&r.deducted_through_paye)),
("balance", money_to_value(&r.balance)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
student_loan_repayment_to_value(&student_loan_repayment(
&money_from_value(&args[0]),
&money_from_value(&args[1]),
args[2].as_str(),
args[3].as_str(),
&money_from_value(&args[4]),
))
}