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"),
)