Functional Weave
Code in Rust

education.student-loan-repayment@1.0.1

impl/python.py

3,824 bytes · the Python 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.

from .dates_uk_tax_year import uk_tax_year  ← from dates.uk-tax-year ^1.0.0 · built alongside by fune
from .education_student_loan_repayment_data import UNEARNED_INCOME_LIMITS, UNEARNED_INCOME_LIMITS_HISTORY, UNEARNED_INCOME_LIMITS_HORIZON  ← this capability’s own data, compiled from data/unearned-income-limits.json into the same file by fune build
from .education_student_loan_repayment_types import StudentLoanRepayment
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune
from .payroll_student_loan_data import STUDENT_LOAN_THRESHOLDS, STUDENT_LOAN_THRESHOLDS_HISTORY, STUDENT_LOAN_THRESHOLDS_HORIZON  ← payroll.student-loan’s rule data (^1.0.0) · built alongside by fune
from .payroll_student_loan_types import StudentLoanPlan

PLANS = ("plan-1", "plan-2", "plan-4", "plan-5", "postgraduate")


def _gbp(value: Money, name: str) -> int:
    if value.currency != "GBP":
        raise ValueError("%s must be in GBP, received %s" % (name, value.currency))
    if isinstance(value.minor, bool) or not isinstance(value.minor, int) or value.minor < 0:
        raise ValueError("%s must not be negative, received %s" % (name, value.minor))
    return value.minor


def student_loan_repayment(
    earned_income: Money,
    unearned_income: Money,
    plan: StudentLoanPlan,
    tax_year_date: str,
    deducted_through_paye: Money,
) -> StudentLoanRepayment:
    """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.
    """
    earned = _gbp(earned_income, "earnedIncome")
    unearned = _gbp(unearned_income, "unearnedIncome")
    deducted = _gbp(deducted_through_paye, "deductedThroughPaye")
    if plan not in PLANS:
        raise ValueError('unknown student loan plan "%s"' % (plan,))
    year = uk_tax_year(tax_year_date)
    start = year.start

    rule = next(
        (
            r
            for r in STUDENT_LOAN_THRESHOLDS
            if r.plan == plan and start >= r.valid_from and (r.valid_to is None or start <= r.valid_to)
        ),
        None,
    )
    if rule is None:
        if STUDENT_LOAN_THRESHOLDS_HISTORY != "full" and STUDENT_LOAN_THRESHOLDS_HORIZON is not None and start < STUDENT_LOAN_THRESHOLDS_HORIZON:
            raise ValueError(
                "no student loan threshold for %s in tax year %s: this build was installed with history=%s, so it only carries rules from %s. "
                "Reinstall with history=full for earlier tax years."
                % (plan, year.label, STUDENT_LOAN_THRESHOLDS_HISTORY, STUDENT_LOAN_THRESHOLDS_HORIZON)
            )
        raise ValueError("no student loan threshold for %s in tax year %s" % (plan, year.label))
    limit = next(
        (r for r in UNEARNED_INCOME_LIMITS if start >= r.valid_from and (r.valid_to is None or start <= r.valid_to)),
        None,
    )
    if limit is None:
        if UNEARNED_INCOME_LIMITS_HISTORY != "full" and UNEARNED_INCOME_LIMITS_HORIZON is not None and start < UNEARNED_INCOME_LIMITS_HORIZON:
            raise ValueError(
                "no unearned income limit for tax year %s: this build was installed with history=%s, so it only carries rules from %s. "
                "Reinstall with history=full for earlier tax years."
                % (year.label, UNEARNED_INCOME_LIMITS_HISTORY, UNEARNED_INCOME_LIMITS_HORIZON)
            )
        raise ValueError("no unearned income limit for tax year %s" % (year.label,))

    counted = earned + (unearned if unearned > limit.limit else 0)
    excess = counted - rule.annual_threshold
    pounds = 0 if excess <= 0 else (excess * rule.basis_points) // (10000 * 100)
    repayment = pounds * 100
    return StudentLoanRepayment(
        tax_year=year.label,
        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"),
    )