Functional Weave
Code in TypeScript

lending.overpayment-effect@1.0.1

impl/python.py

3,510 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 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,
    )