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 Optional, Sequence
from .dates_add_days import add_days ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months ← from dates.add-months ^1.0.0 · built alongside by fune
from .dates_days_between import days_between ← from dates.days-between ^1.0.0 · built alongside by fune
from .lending_early_settlement_types import DatedAmount, EarlySettlement, SettlementDeferral
from .math_fractional_power import FIXED_SCALE, pow_fixed, root_fixed ← from math.fractional-power ^1.0.0 · built alongside by fune
from .money_amount import money ← from money.amount ^1.0.0 · built alongside by fune
def _settlement_date(
advances: Sequence[DatedAmount],
repayments: Sequence[DatedAmount],
notice_received_iso: str,
requested_settlement_iso: Optional[str],
deferral: SettlementDeferral,
) -> str:
"""The settlement date: 28 days after the creditor receives the notice
(regulation 5(1)(a)), or a later date the notice names, then deferred by a
month or 30 days where the creditor elects and regulation 6 allows it
(credit repaid over more than a year from the first drawdown)."""
date = add_days(notice_received_iso, 28)
if requested_settlement_iso is not None:
days_between(requested_settlement_iso, requested_settlement_iso)
if requested_settlement_iso > date:
date = requested_settlement_iso
if deferral == "none":
return date
if deferral not in ("one-month", "thirty-days"):
raise ValueError('unknown deferral "%s": expected none, one-month or thirty-days' % (deferral,))
first = min(a.date for a in advances)
last = max(r.date for r in repayments)
if last <= add_months(first, 12):
raise ValueError("deferral is only allowed when the credit is repaid over more than a year (regulation 6)")
return add_months(date, 1) if deferral == "one-month" else add_days(date, 30)
def early_settlement(
advances: Sequence[DatedAmount],
repayments: Sequence[DatedAmount],
apr_basis_points: int,
notice_received_iso: str,
requested_settlement_iso: Optional[str],
deferral: SettlementDeferral,
) -> EarlySettlement:
"""The amount a debtor pays to settle a regulated consumer credit agreement
early, and the rebate, per the Consumer Credit (Early Settlement)
Regulations 2004, regulation 4(1).
The rebate is the scheduled repayments falling due after the settlement
date less sum A_i (1 + r)^a_i - sum B_j (1 + r)^b_j over the advances and
the repayments made before the settlement date, each carried forward to
the settlement date at the APR. That difference is the settlement figure.
Times are days / 365 of a year.
"""
if len(advances) == 0:
raise ValueError("advances must not be empty")
if len(repayments) == 0:
raise ValueError("repayments must not be empty")
b = apr_basis_points
if isinstance(b, bool) or not isinstance(b, int) or b < 0 or b > 1000000:
raise ValueError("aprBasisPoints must be between 0 and 1000000, received %r" % (b,))
currency = advances[0].amount.currency
for flow in list(advances) + list(repayments):
days_between(flow.date, flow.date)
if flow.amount.currency != currency:
raise ValueError("currency mismatch: %s and %s" % (currency, flow.amount.currency))
if flow.amount.minor <= 0:
raise ValueError("every amount must be greater than zero, received %r" % (flow.amount.minor,))
date = _settlement_date(advances, repayments, notice_received_iso, requested_settlement_iso, deferral)
# (1 + APR)^(1/365), once; each flow is then a whole power of it.
daily = root_fixed(FIXED_SCALE * (10000 + b) // 10000, 365)
def carried(flow: DatedAmount) -> int:
return flow.amount.minor * pow_fixed(daily, days_between(flow.date, date))
owed = 0
drawn = False
for advance in advances:
if advance.date < date:
owed += carried(advance)
drawn = True
if not drawn:
raise ValueError("no credit was advanced before the settlement date %s" % (date,))
remaining = 0
for repayment in repayments:
if repayment.date < date:
owed -= carried(repayment)
else:
remaining += repayment.amount.minor
# Rounded down: the rebate must be at least the one the formula gives.
formula = owed // FIXED_SCALE if owed > 0 else 0
settlement = min(formula, remaining)
return EarlySettlement(
settlement_date=date,
settlement_amount=money(settlement, currency),
remaining_repayments=money(remaining, currency),
rebate=money(remaining - settlement, currency),
)