Functional Weave
Code in Rust

insurance.instalment-plan@1.0.0

impl/python.py

3,067 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 List

from .insurance_instalment_plan_types import InstalmentPlan
from .lending_apr import apr  ← from lending.apr ^1.0.0 · built alongside by fune
from .lending_apr_types import CreditFlow
from .money_amount import Money, assert_same_currency, money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_apply_rate import apply_rate  ← from money.apply-rate ^1.0.0 · built alongside by fune


def _is_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def instalment_plan(
    premium: Money,
    deposit: Money,
    instalment_count: int,
    credit_charge_basis_points: int,
    first_instalment_month: int,
) -> InstalmentPlan:
    """A premium paid by monthly instalments.

    The credit charge is a flat percentage of the amount financed (premium
    less deposit), the way UK insurers quote it. The instalments are the
    amount financed plus the charge, split so every penny is accounted for:
    equal instalments rounded down, with the odd pennies on the first. The APR
    is then worked from those exact cash flows by lending.apr, because a flat
    10% repaid over ten months costs far more than 10% a year.
    """
    assert_same_currency(premium, deposit)
    if not _is_int(instalment_count) or instalment_count < 1:
        raise ValueError("instalmentCount must be a whole number of 1 or more, received %s" % (instalment_count,))
    if not _is_int(credit_charge_basis_points) or credit_charge_basis_points < 0:
        raise ValueError(
            "creditChargeBasisPoints must be a whole number of 0 or more, received %s" % (credit_charge_basis_points,)
        )
    if not _is_int(first_instalment_month) or first_instalment_month < 0:
        raise ValueError(
            "firstInstalmentMonth must be a whole number of 0 or more, received %s" % (first_instalment_month,)
        )
    if deposit.minor < 0:
        raise ValueError("deposit must not be negative, received %d" % (deposit.minor,))
    if deposit.minor >= premium.minor:
        raise ValueError("the deposit leaves nothing to pay by instalments")
    if instalment_count == 1 and first_instalment_month == 0:
        raise ValueError("a single instalment at inception is payment in full, not credit")
    c = premium.currency
    financed = money(premium.minor - deposit.minor, c)
    charge = apply_rate(financed, credit_charge_basis_points, "half-up")
    repayable = financed.minor + charge.minor
    each = repayable // instalment_count
    if each < 1:
        raise ValueError("each instalment must be at least one minor unit")
    first = repayable - each * (instalment_count - 1)
    instalments: List[Money] = []
    repayments: List[CreditFlow] = []
    for i in range(instalment_count):
        amount = money(first if i == 0 else each, c)
        instalments.append(amount)
        repayments.append(CreditFlow(period=first_instalment_month + i, amount=amount))
    return InstalmentPlan(
        deposit=deposit,
        amount_financed=financed,
        credit_charge=charge,
        instalments=instalments,
        total_payable=money(deposit.minor + repayable, c),
        apr=apr([CreditFlow(period=0, amount=financed)], repayments, 12),
    )