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, days_between ← from dates.add-days ^1.0.0 · built alongside by fune
from .finance_late_payment_interest import late_payment_interest ← from finance.late-payment-interest ^1.0.0 · built alongside by fune
from .legal_statutory_interest_data import (
COUNTY_COURT_THRESHOLDS,
COUNTY_COURT_THRESHOLDS_HISTORY,
JUDGMENT_RATES,
JUDGMENT_RATES_HISTORY,
)
from .legal_statutory_interest_types import InterestBasis, StatutoryInterest
from .math_round_div import round_div ← 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
_YEAR = 10000 * 365
_MAX_EXACT = 2**53 - 1
def _horizon(history: str, froms: Sequence[str], on_date: str, what: str) -> None:
# A history=current build keeps only rows still in force; answering an older
# date with today's rule is the failure dated data exists to prevent.
if history == "full" or len(froms) == 0:
return
earliest = min(froms)
if on_date < earliest:
raise ValueError(
"no %s on %s: this build was installed with history=%s, so it only carries rules from %s. "
"Reinstall with history=full for older judgments." % (what, on_date, history, earliest)
)
def _in_force(on_date: str, valid_from: str, valid_to: Optional[str]) -> bool:
return on_date >= valid_from and (valid_to is None or on_date <= valid_to)
def _rate_on(on_date: str) -> int:
for row in JUDGMENT_RATES:
if _in_force(on_date, row.valid_from, row.valid_to):
return row.basis_points
_horizon(JUDGMENT_RATES_HISTORY, [r.valid_from for r in JUDGMENT_RATES], on_date, "judgment interest rate")
raise ValueError("no judgment interest rate in force on %s" % (on_date,))
def _checked(a: int, b: int, c: int) -> int:
if a * b * c > _MAX_EXACT:
raise ValueError("the interest calculation exceeds the exact integer range")
return a * b * c
def statutory_interest(amount: Money, from_date: str, to_date: str, basis: InterestBasis) -> StatutoryInterest:
"""Statutory interest on a judgment debt or a late commercial debt."""
if amount.currency != "GBP":
raise ValueError("statutory interest is defined for GBP debts, received %s" % (amount.currency,))
if amount.minor <= 0:
raise ValueError("amount must be greater than zero")
zero = money(0, "GBP")
if basis == "late-payment":
claim = late_payment_interest(amount, from_date, to_date)
rate = claim.interest_rate_basis_points or 0
return StatutoryInterest(
days=claim.days_late,
rate_basis_points=rate,
daily_interest=money(round_div(amount.minor * rate, _YEAR, "half-up"), "GBP"),
interest=claim.interest,
compensation=claim.compensation,
total=claim.total,
)
if basis not in ("judgment-high-court", "judgment-county-court"):
raise ValueError(
'unknown interest basis "%s": expected judgment-high-court, judgment-county-court or late-payment' % (basis,)
)
days = days_between(from_date, to_date)
if days < 0:
raise ValueError("toDate %s is before fromDate %s" % (to_date, from_date))
if basis == "judgment-county-court":
threshold = next((r for r in COUNTY_COURT_THRESHOLDS if _in_force(from_date, r.valid_from, r.valid_to)), None)
if threshold is None:
_horizon(
COUNTY_COURT_THRESHOLDS_HISTORY,
[r.valid_from for r in COUNTY_COURT_THRESHOLDS],
from_date,
"County Court judgment interest rule",
)
raise ValueError("no County Court judgment interest rule in force on %s" % (from_date,))
if amount.minor < threshold.min_judgment_minor:
# Not a "relevant judgment": no statutory interest at all.
return StatutoryInterest(days=days, rate_basis_points=0, daily_interest=zero, interest=zero, compensation=zero, total=zero)
# Each day from the day after judgment to the payment date at the rate in force on it.
numerator = 0
covered = 0
first = add_days(from_date, 1)
for row in JUDGMENT_RATES:
if days == 0:
break
start = first if first > row.valid_from else row.valid_from
end = to_date if row.valid_to is None or to_date < row.valid_to else row.valid_to
if start > end:
continue
segment = days_between(start, end) + 1
covered += segment
numerator += _checked(amount.minor, row.basis_points, segment)
if covered != days:
_rate_on(first)
raise ValueError("no judgment interest rate for every day from %s to %s" % (first, to_date))
if numerator > _MAX_EXACT:
raise ValueError("the interest calculation exceeds the exact integer range")
rate = _rate_on(to_date)
interest = money(round_div(numerator, _YEAR, "half-up"), "GBP")
return StatutoryInterest(
days=days,
rate_basis_points=rate,
daily_interest=money(round_div(amount.minor * rate, _YEAR, "half-up"), "GBP"),
interest=interest,
compensation=zero,
total=interest,
)