use super::funejson::Value; use super::dates_uk_tax_year::uk_tax_year; 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}; 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]), )) }