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"),
};
}