from .construction_retention_types import Retention, RetentionStage from .math_round_div import round_div from .money_amount import Money, money MAX_SAFE = 9007199254740991 def retention(cumulative_value: Money, basis_points: int, stage: RetentionStage) -> Retention: """Retention on a valuation, the way JCT-style contracts release it. Retention is always worked out afresh on the cumulative value, never accumulated valuation by valuation, so a later valuation corrects an earlier one. At practical completion half the percentage applies (not half of a figure rounded earlier), and after the defects are made good none does. """ if isinstance(basis_points, bool) or not isinstance(basis_points, int) or basis_points < 0 or basis_points > 10000: raise ValueError("basisPoints must be a whole number from 0 to 10000, received %r" % (basis_points,)) if cumulative_value.minor < 0: raise ValueError("cumulativeValue must not be negative, received %d" % (cumulative_value.minor,)) product = cumulative_value.minor * basis_points # Python would carry on, but TypeScript cannot, and all three must agree. if product > MAX_SAFE: raise ValueError("the retention calculation exceeds 2^53 - 1") full = round_div(product, 10000, "half-up") if stage == "interim": held = full elif stage == "practical-completion": held = round_div(product, 20000, "half-up") elif stage == "final": held = 0 else: raise ValueError('unknown retention stage "%s"' % (stage,)) currency = cumulative_value.currency return Retention(full=money(full, currency), held=money(held, currency), released=money(full - held, currency))