Functional Weave
Code in Python

payroll.student-loan@1.0.2

impl/python.py

2,890 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.

import re

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  ← this capability’s own data, compiled from data/student-loan-thresholds.json into the same file by fune build
from .payroll_student_loan_types import StudentLoanPlan
from .payroll_tax_period_types import PayFrequency

ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
PLANS = ("plan-1", "plan-2", "plan-4", "plan-5", "postgraduate")
PERIODS = {"weekly": (1, 52), "fortnightly": (2, 52), "four-weekly": (4, 52), "monthly": (1, 12)}


def student_loan(earnings: Money, plan: StudentLoanPlan, frequency: PayFrequency, pay_date: str) -> Money:
    """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.
    """
    if earnings.currency != "GBP":
        raise ValueError("student loan deductions must be in GBP, received %s" % (earnings.currency,))
    if earnings.minor < 0:
        raise ValueError("earnings must not be negative, received %s" % (earnings.minor,))
    if plan not in PLANS:
        raise ValueError('unknown student loan plan "%s"' % (plan,))
    if frequency not in PERIODS:
        raise ValueError('unknown pay frequency "%s"' % (frequency,))
    if not isinstance(pay_date, str) or not ISO_DATE.fullmatch(pay_date):
        raise ValueError('payDate must be an ISO date (YYYY-MM-DD), received "%s"' % (pay_date,))
    periods, per_year = PERIODS[frequency]

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

    # Everything below is in pence x per_year, so the threshold is exact.
    excess = earnings.minor * per_year - rule.annual_threshold * periods
    if excess <= 0:
        return money(0, "GBP")
    pounds = (excess * rule.basis_points) // (10000 * per_year * 100)
    return money(pounds * 100, "GBP")