from typing import List, Sequence from .dates_add_days import parse_iso_date from .education_fee_instalments_types import FeeInstalment from .money_allocate import allocate from .money_amount import Money, money 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