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::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 is_iso_date(value: &str) -> bool {
let bytes = value.as_bytes();
bytes.len() == 10
&& bytes[4] == b'-'
&& bytes[7] == b'-'
&& bytes
.iter()
.enumerate()
.all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}
/// The student or postgraduate loan deduction for one pay period.
///
/// The period threshold is the annual one scaled exactly by weeks/52 or
/// months/12 (Education (Student Loans) (Repayment) Regulations 2009 reg.
/// 44(2)), not HMRC's printed weekly and monthly figures, which are that
/// fraction cut to the penny. The deduction then drops its pence (reg. 44(3)),
/// so the whole calculation stays in integers: pence times the period
/// denominator.
///
/// # Panics
/// Panics on non-GBP or negative earnings, an unknown plan or frequency, or a
/// date no threshold covers.
pub fn student_loan(earnings: &Money, plan: &str, frequency: &str, pay_date: &str) -> Money {
if earnings.currency != "GBP" {
panic!("student loan deductions must be in GBP, received {}", earnings.currency);
}
if earnings.minor < 0 {
panic!("earnings must not be negative, received {}", earnings.minor);
}
if !matches!(plan, "plan-1" | "plan-2" | "plan-4" | "plan-5" | "postgraduate") {
panic!("unknown student loan plan \"{}\"", plan);
}
let (periods, per_year): (i128, i128) = match frequency {
"weekly" => (1, 52),
"fortnightly" => (2, 52),
"four-weekly" => (4, 52),
"monthly" => (1, 12),
other => panic!("unknown pay frequency \"{}\"", other),
};
if !is_iso_date(pay_date) {
panic!("payDate must be an ISO date (YYYY-MM-DD), received \"{}\"", pay_date);
}
let rule = STUDENT_LOAN_THRESHOLDS.iter().find(|r| {
r.plan == plan && pay_date >= r.valid_from && r.valid_to.map_or(true, |to| pay_date <= to)
});
let rule = match rule {
Some(r) => r,
None => {
if let Some(horizon) = STUDENT_LOAN_THRESHOLDS_HORIZON {
if STUDENT_LOAN_THRESHOLDS_HISTORY != "full" && pay_date < horizon {
panic!(
"no student loan threshold for {} on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.",
plan, pay_date, STUDENT_LOAN_THRESHOLDS_HISTORY, horizon
);
}
}
panic!("no student loan threshold for {} on {}", plan, pay_date)
}
};
// Everything below is in pence x per_year, so the threshold is exact.
let excess = earnings.minor as i128 * per_year - rule.annual_threshold as i128 * periods;
if excess <= 0 {
return money(0, "GBP");
}
let pounds = (excess * rule.basis_points as i128) / (10000 * per_year * 100);
money((pounds * 100) as i64, "GBP")
}
pub fn fune_vector(args: &[Value]) -> Value {
money_to_value(&student_loan(
&money_from_value(&args[0]),
args[1].as_str(),
args[2].as_str(),
args[3].as_str(),
))
}