Functional Weave
Code in Rust

property.rent-review@1.0.0

impl/typescript.ts

2,717 bytes · the TypeScript 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.

import { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type RentReviewResult, type RentReviewRule } from "./property_rent_review_types.ts";

const MAX_RENT = 1_000_000_000;
const MAX_INDEX = 1_000_000;

function checkBasisPoints(name: string, value: number | null): void {
  if (value === null) return;
  if (!Number.isInteger(value) || value <= -10000 || value > 100000) {
    throw new RangeError(`${name} must be an integer from -9999 to 100000, received ${value}`);
  }
}

/**
 * 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.
 */
export function rentReview(
  passingRent: Money,
  baseIndex: number,
  reviewIndex: number,
  collarBasisPoints: number | null,
  capBasisPoints: number | null,
  upwardOnly: boolean,
): RentReviewResult {
  if (!Number.isInteger(passingRent.minor) || passingRent.minor < 0 || passingRent.minor > MAX_RENT) {
    throw new RangeError(`passingRent must be between 0 and ${MAX_RENT} minor units, received ${passingRent.minor}`);
  }
  for (const [name, value] of [["baseIndex", baseIndex], ["reviewIndex", reviewIndex]] as const) {
    if (!Number.isInteger(value) || value <= 0 || value > MAX_INDEX) {
      throw new RangeError(`${name} must be an integer from 1 to ${MAX_INDEX}, received ${value}`);
    }
  }
  checkBasisPoints("collarBasisPoints", collarBasisPoints);
  checkBasisPoints("capBasisPoints", capBasisPoints);
  if (collarBasisPoints !== null && capBasisPoints !== null && collarBasisPoints > capBasisPoints) {
    throw new RangeError(`collar ${collarBasisPoints} is above cap ${capBasisPoints}`);
  }
  const p = passingRent.minor;
  // The multiplier as a fraction num/den: the index ratio, or 1 + collar/cap.
  let num = reviewIndex;
  let den = baseIndex;
  let rule: RentReviewRule = "index";
  if (capBasisPoints !== null && reviewIndex * 10000 > baseIndex * (10000 + capBasisPoints)) {
    num = 10000 + capBasisPoints;
    den = 10000;
    rule = "cap";
  } else if (collarBasisPoints !== null && reviewIndex * 10000 < baseIndex * (10000 + collarBasisPoints)) {
    num = 10000 + collarBasisPoints;
    den = 10000;
    rule = "collar";
  }
  if (upwardOnly && num < den) {
    num = 1;
    den = 1;
    rule = "upward-only";
  }
  return {
    newRent: money(roundDiv(p * num, den, "half-up"), passingRent.currency),
    indexRent: money(roundDiv(p * reviewIndex, baseIndex, "half-up"), passingRent.currency),
    indexChangeBasisPoints: roundDiv((reviewIndex - baseIndex) * 10000, baseIndex, "half-up"),
    appliedBy: rule,
  };
}