Functional Weave
Code in TypeScript

property.rent-review@1.0.0

impl/python.py

2,804 bytes · the Python implementation · view raw

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 Optional

from .math_round_div import round_div  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune
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,
    )