from typing import Optional from .math_round_div import round_div from .money_amount import Money, money from .property_rent_review_types import RentReviewResult, RentReviewRule _MAX_RENT = 1_000_000_000 _MAX_INDEX = 1_000_000 def _is_int(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) def _check_basis_points(name: str, value: Optional[int]) -> None: if value is None: return if not _is_int(value) or value <= -10000 or value > 100000: raise ValueError("%s must be an integer from -9999 to 100000, received %r" % (name, value)) def rent_review( passing_rent: Money, base_index: int, review_index: int, collar_basis_points: Optional[int], cap_basis_points: Optional[int], upward_only: bool, ) -> RentReviewResult: """The rent after an index-linked review, held between an optional collar and cap, and not below the passing rent when upward-only. Every comparison is on exact fractions and the rent is rounded half-up once.""" if passing_rent.minor < 0 or passing_rent.minor > _MAX_RENT: raise ValueError( "passingRent must be between 0 and %d minor units, received %r" % (_MAX_RENT, passing_rent.minor) ) for name, value in (("baseIndex", base_index), ("reviewIndex", review_index)): if not _is_int(value) or value <= 0 or value > _MAX_INDEX: raise ValueError("%s must be an integer from 1 to %d, received %r" % (name, _MAX_INDEX, value)) _check_basis_points("collarBasisPoints", collar_basis_points) _check_basis_points("capBasisPoints", cap_basis_points) if collar_basis_points is not None and cap_basis_points is not None and collar_basis_points > cap_basis_points: raise ValueError("collar %d is above cap %d" % (collar_basis_points, cap_basis_points)) p = passing_rent.minor # The multiplier as a fraction num/den: the index ratio, or 1 + collar/cap. num, den = review_index, base_index rule: RentReviewRule = "index" if cap_basis_points is not None and review_index * 10000 > base_index * (10000 + cap_basis_points): num, den, rule = 10000 + cap_basis_points, 10000, "cap" elif collar_basis_points is not None and review_index * 10000 < base_index * (10000 + collar_basis_points): num, den, rule = 10000 + collar_basis_points, 10000, "collar" if upward_only and num < den: num, den, rule = 1, 1, "upward-only" return RentReviewResult( new_rent=money(round_div(p * num, den, "half-up"), passing_rent.currency), index_rent=money(round_div(p * review_index, base_index, "half-up"), passing_rent.currency), index_change_basis_points=round_div((review_index - base_index) * 10000, base_index, "half-up"), applied_by=rule, )