Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import Tuple
from .lending_amortisation_schedule import amortisation_schedule, period_interest ← from lending.amortisation-schedule ^1.0.0 · built alongside by fune
from .lending_loan_payment import payment_count ← from lending.loan-payment ^1.0.0 · built alongside by fune
from .lending_overpayment_effect_types import OverpaymentEffect
from .math_round_div import RoundingMode ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
def _pay_off(
balance: int, annual_rate_basis_points: int, payments_per_year: int, payment: int, term: int
) -> Tuple[int, int, int]:
"""Run a balance down at a fixed payment, with the schedule's rounding,
until it is clear or the term ends: (payments, final payment, interest).
As in lending.amortisation-schedule, the last payment of the term is
whatever clears the balance."""
owed = balance
payments = 0
interest = 0
final_payment = 0
while owed > 0:
accrued = period_interest(owed, annual_rate_basis_points, payments_per_year)
if payment <= accrued:
raise ValueError("the payment of %d does not cover the interest of %d" % (payment, accrued))
due = owed + accrued if payments + 1 == term or owed + accrued <= payment else payment
owed -= due - accrued
interest += accrued
payments += 1
final_payment = due
return payments, final_payment, interest
def overpayment_effect(
balance: Money,
annual_rate_basis_points: int,
remaining_term_months: int,
payments_per_year: int,
payment: Money,
overpayment: Money,
mode: RoundingMode,
) -> OverpaymentEffect:
"""The two things a lender offers after a lump-sum overpayment, side by
side: keep paying the same amount and finish sooner, or keep the same end
date and pay less each period. Both are built period by period with the
same rounding as lending.amortisation-schedule, and compared with carrying
on as if nothing had been overpaid.
"""
term = payment_count(annual_rate_basis_points, remaining_term_months, payments_per_year)
currency = balance.currency
for other in (payment, overpayment):
if other.currency != currency:
raise ValueError("currency mismatch: %s and %s" % (currency, other.currency))
if payment.minor <= 0:
raise ValueError("payment must be greater than zero, received %r" % (payment.minor,))
if overpayment.minor <= 0:
raise ValueError("overpayment must be greater than zero, received %r" % (overpayment.minor,))
if overpayment.minor >= balance.minor:
raise ValueError("overpayment must be less than the balance; paying it all off is an early settlement")
new_balance = balance.minor - overpayment.minor
base_n, _, base_interest = _pay_off(balance.minor, annual_rate_basis_points, payments_per_year, payment.minor, term)
short_n, short_final, short_interest = _pay_off(
new_balance, annual_rate_basis_points, payments_per_year, payment.minor, term
)
lower = amortisation_schedule(
money(new_balance, currency), annual_rate_basis_points, remaining_term_months, payments_per_year, mode
)
return OverpaymentEffect(
new_balance=money(new_balance, currency),
baseline_payments=base_n,
baseline_interest=money(base_interest, currency),
reduced_term_payments=short_n,
reduced_term_final_payment=money(short_final, currency),
reduced_term_interest=money(short_interest, currency),
payments_saved=base_n - short_n,
reduced_payment=lower.payment,
reduced_payment_interest=lower.total_interest,
)