Functional Weave
Code in Rust

property.deposit-cap@1.0.2

impl/typescript.ts

2,930 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 { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { DEPOSIT_CAPS, DEPOSIT_CAPS_HISTORY, type DepositCapRule } from "./property_deposit_cap_data.ts";  ← this capability’s own data, compiled from data/deposit-caps.json into the same file by fune build
import { type DepositCap, type RentPeriod } from "./property_deposit_cap_types.ts";

const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
// Keeps annual rent × 6 exact as a JavaScript number.
const MAX_MINOR = 1_000_000_000_000;

function findRule(jurisdiction: string, onDate: string): DepositCapRule {
  let best: DepositCapRule | null = null;
  let earliest: string | null = null;
  for (const rule of DEPOSIT_CAPS) {
    if (rule.jurisdiction !== jurisdiction) continue;
    if (earliest === null || rule.validFrom < earliest) earliest = rule.validFrom;
    if (onDate < rule.validFrom) continue;
    if (rule.validTo !== null && onDate > rule.validTo) continue;
    if (best === null || rule.validFrom > best.validFrom) best = rule;
  }
  if (best !== null) return best;
  // A build installed with a pruned history must not answer an old date with
  // today's rule, so it says what happened instead.
  if (DEPOSIT_CAPS_HISTORY !== "full" && earliest !== null && onDate < earliest) {
    throw new RangeError(
      `no deposit cap rule for ${jurisdiction} on ${onDate}: this build was installed with history=${DEPOSIT_CAPS_HISTORY}, so it only carries rules from ${earliest}`,
    );
  }
  throw new RangeError(`no deposit cap rule for ${jurisdiction} on ${onDate}`);
}

/**
 * The maximum tenancy deposit: five weeks' rent below the annual-rent
 * threshold, six at or above it. The threshold is tested on the exact annual
 * rent, and the cap is annual rent × weeks ÷ 52 rounded down once, so it can
 * never sit a fraction of a penny over the statutory limit.
 */
export function depositCap(rent: Money, rentPeriod: RentPeriod, jurisdiction: string, onDate: string): DepositCap {
  if (!ISO_DATE.test(onDate)) {
    throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
  }
  if (rent.currency !== "GBP") {
    throw new RangeError(`rent must be in GBP, received ${rent.currency}`);
  }
  if (!Number.isInteger(rent.minor) || rent.minor < 0) {
    throw new RangeError(`rent must not be negative, received ${rent.minor}`);
  }
  let annual: number;
  if (rentPeriod === "weekly") annual = rent.minor * 52;
  else if (rentPeriod === "monthly") annual = rent.minor * 12;
  else if (rentPeriod === "annual") annual = rent.minor;
  else throw new RangeError(`unknown rent period "${rentPeriod}"`);
  if (annual > MAX_MINOR) {
    throw new RangeError(`annual rent must be at most ${MAX_MINOR} minor units`);
  }
  const rule = findRule(jurisdiction, onDate);
  const weeks = annual < rule.annualRentThresholdMinor ? rule.weeksBelow : rule.weeksAtOrAbove;
  return {
    annualRent: money(annual, "GBP"),
    weeklyRent: money(Math.floor(annual / 52), "GBP"),
    weeks,
    maximum: money(Math.floor((annual * weeks) / 52), "GBP"),
  };
}