Functional Weave
Code in Rust

lending.ltv@1.0.1

impl/typescript.ts

1,992 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 } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type LtvBand, type LtvResult } from "./lending_ltv_types.ts";

/** Above this, loan × 10000 is no longer exact as a JavaScript number. */
const MAX_LOAN = 900719925474;

/**
 * Loan-to-value, and the band from the lender's table. The band is chosen on
 * the exact ratio, loan × 10000 <= ceiling × value, never on a rounded
 * percentage, so 75.004% is not "75%". The reported basis points are rounded
 * up, which agrees: a whole ceiling is met exactly when the rounded-up ratio
 * is within it.
 */
export function loanToValue(loan: Money, propertyValue: Money, bands: readonly LtvBand[]): LtvResult {
  if (loan.currency !== propertyValue.currency) {
    throw new RangeError(`currency mismatch: ${loan.currency} and ${propertyValue.currency}`);
  }
  if (!Number.isInteger(propertyValue.minor) || propertyValue.minor <= 0) {
    throw new RangeError(`propertyValue must be greater than zero, received ${propertyValue.minor}`);
  }
  if (!Number.isInteger(loan.minor) || loan.minor < 0) {
    throw new RangeError(`loan must not be negative, received ${loan.minor}`);
  }
  if (loan.minor > MAX_LOAN) {
    throw new RangeError(`loan must be at most ${MAX_LOAN} minor units`);
  }
  if (bands.length === 0) {
    throw new RangeError("bands must not be empty");
  }
  for (let i = 0; i < bands.length; i += 1) {
    const max = bands[i].maxBasisPoints;
    if (!Number.isInteger(max) || max < 1) {
      throw new RangeError(`band ceilings must be whole basis points of 1 or more, received ${max}`);
    }
    if (i > 0 && max <= bands[i - 1].maxBasisPoints) {
      throw new RangeError("band ceilings must be in strictly ascending order");
    }
  }
  const basisPoints = roundDiv(loan.minor * 10000, propertyValue.minor, "up");
  const found = bands.find((band) => basisPoints <= band.maxBasisPoints);
  return { basisPoints, band: found === undefined ? null : found.label };
}