Functional Weave
Code in Rust

banking.overdraft-interest@1.0.0

impl/python.py

2,553 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, Sequence

from .banking_overdraft_interest_types import OverdraftInterest
from .dates_day_count_fraction import DayCountConvention  ← from dates.day-count-fraction ^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_daily_interest import DatedBalance, daily_interest  ← from lending.daily-interest ^1.0.0 · built alongside by fune
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 overdraft_interest(
    balances: Sequence[DatedBalance],
    from_iso: str,
    to_iso: str,
    annual_rate_basis_points: int,
    interest_free_buffer: Money,
    convention: DayCountConvention,
    mode: RoundingMode,
) -> OverdraftInterest:
    """Overdraft interest over a statement period.

    Each day the account is overdrawn, the part of the overdrawn amount above
    the interest-free buffer accrues interest; credit balances earn nothing
    here. The accrual itself is lending.daily-interest on those chargeable
    amounts, so it is summed exactly and rounded once for the period.
    """
    b = annual_rate_basis_points
    if isinstance(b, bool) or not isinstance(b, int) or b < 0:
        raise ValueError("annualRateBasisPoints must not be negative, received %r" % (b,))
    if interest_free_buffer.minor < 0:
        raise ValueError("interestFreeBuffer must not be negative, received %r" % (interest_free_buffer.minor,))
    for entry in balances:
        if entry.balance.currency != interest_free_buffer.currency:
            raise ValueError("currency mismatch: %s and %s" % (entry.balance.currency, interest_free_buffer.currency))
    chargeable: List[DatedBalance] = [
        DatedBalance(
            date=entry.date,
            balance=money(max(0, -entry.balance.minor - interest_free_buffer.minor), entry.balance.currency),
        )
        for entry in balances
    ]
    # Validates the dates, their order and the period before anything is counted.
    accrued = daily_interest(chargeable, from_iso, to_iso, b, convention, mode)
    days_overdrawn = 0
    days_charged = 0
    for i, entry in enumerate(balances):
        start = entry.date if entry.date > from_iso else from_iso
        following = balances[i + 1].date if i + 1 < len(balances) else to_iso
        end = following if following < to_iso else to_iso
        if start >= end:
            continue
        days = days_between(start, end)
        if entry.balance.minor < 0:
            days_overdrawn += days
        if chargeable[i].balance.minor > 0:
            days_charged += days
    return OverdraftInterest(interest=accrued.interest, days_overdrawn=days_overdrawn, days_charged=days_charged)