from .dates_uk_tax_year import uk_tax_year from .education_student_loan_repayment_data import UNEARNED_INCOME_LIMITS, UNEARNED_INCOME_LIMITS_HISTORY, UNEARNED_INCOME_LIMITS_HORIZON from .education_student_loan_repayment_types import StudentLoanRepayment from .money_amount import Money, money from .payroll_student_loan_data import STUDENT_LOAN_THRESHOLDS, STUDENT_LOAN_THRESHOLDS_HISTORY, STUDENT_LOAN_THRESHOLDS_HORIZON 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"), )