from .finance_proration_types import ProrationSplit from .money_allocate import allocate from .money_amount import Money def prorate(amount: Money, total_days: int, used_days: int) -> ProrationSplit: """Split a period's charge into the part used and the part not used. The mid-cycle upgrade, downgrade and cancellation calculation. Written the obvious way - one rounding for the charge and another for the credit - the two halves do not reliably add back up: 9.99 over two days is 4.995 each way, and two half-up roundings produce 5.00 and 5.00, a penny invented on a line the customer can see. One call to ``money.allocate`` does the whole split instead, so ``used + unused`` is the original amount for every input, credits included. """ if isinstance(total_days, bool) or not isinstance(total_days, int) or total_days <= 0: raise ValueError("total days must be greater than zero, received %r" % (total_days,)) if isinstance(used_days, bool) or not isinstance(used_days, int) or used_days < 0: raise ValueError("used days must not be negative, received %r" % (used_days,)) if used_days > total_days: # Clamping here would turn a bug in the caller's period arithmetic into # a plausible invoice, which is far more expensive to find later. raise ValueError( "used days must not exceed total days, received %d of %d" % (used_days, total_days) ) # total_days > 0 guarantees the ratios do not sum to zero, so allocate is # safe even when one side is zero: [0, n] and [n, 0] are both well defined. used, unused = allocate(amount, [used_days, total_days - used_days]) return ProrationSplit(total=amount, used=used, unused=unused)