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 .dates_add_days import parse_iso_date ← from dates.add-days ^1.0.0 · built alongside by fune
from .education_fee_instalments_types import FeeInstalment
from .money_allocate import allocate ← from money.allocate ^1.0.0 · built alongside by fune
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
def _is_int(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def fee_instalments(total: Money, shares: Sequence[int], due_dates: Sequence[str]) -> List[FeeInstalment]:
"""Split a fee into dated instalments that add up to it exactly.
The split is money.allocate's: floors, then the leftover pennies to the
largest remainders, ties to the earlier instalment.
"""
if not _is_int(total.minor) or total.minor < 0:
raise ValueError("total must not be negative, received %s" % (total.minor,))
if len(shares) == 0:
raise ValueError("shares must name at least one instalment")
if len(shares) != len(due_dates):
raise ValueError(
"shares and dueDates must be the same length, received %s and %s" % (len(shares), len(due_dates))
)
for s in shares:
if not _is_int(s) or s < 0:
raise ValueError("each share must be a whole number of 0 or more, received %s" % (s,))
if sum(shares) == 0:
raise ValueError("shares must not all be zero")
for i, d in enumerate(due_dates):
parse_iso_date(d)
if i > 0 and d <= due_dates[i - 1]:
raise ValueError("dueDates must be in strictly ascending order: %s follows %s" % (d, due_dates[i - 1]))
amounts = allocate(total, shares)
paid = 0
out: List[FeeInstalment] = []
for i, amount in enumerate(amounts):
paid += amount.minor
out.append(
FeeInstalment(
number=i + 1,
due_date=due_dates[i],
amount=amount,
cumulative=money(paid, total.currency),
remaining=money(total.minor - paid, total.currency),
)
)
return out