Functional Weave
Code in TypeScript

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]),
    ))
}