impl/typescript.ts
1,650 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 Retention, type RetentionStage } from "./construction_retention_types.ts";
/**
* Retention on a valuation, the way JCT-style contracts release it.
*
* Retention is always worked out afresh on the cumulative value, never
* accumulated valuation by valuation, so a later valuation corrects an earlier
* one. At practical completion half the percentage applies (not half of a
* figure rounded earlier), and after the defects are made good none does.
*/
export function retention(cumulativeValue: Money, basisPoints: number, stage: RetentionStage): Retention {
if (!Number.isInteger(basisPoints) || basisPoints < 0 || basisPoints > 10000) {
throw new RangeError(`basisPoints must be a whole number from 0 to 10000, received ${basisPoints}`);
}
if (cumulativeValue.minor < 0) {
throw new RangeError(`cumulativeValue must not be negative, received ${cumulativeValue.minor}`);
}
const product = cumulativeValue.minor * basisPoints;
if (!Number.isSafeInteger(product)) {
throw new RangeError("the retention calculation exceeds 2^53 - 1");
}
const full = roundDiv(product, 10000, "half-up");
let held: number;
if (stage === "interim") held = full;
else if (stage === "practical-completion") held = roundDiv(product, 20000, "half-up");
else if (stage === "final") held = 0;
else throw new RangeError(`unknown retention stage "${stage}"`);
const currency = cumulativeValue.currency;
return { full: money(full, currency), held: money(held, currency), released: money(full - held, currency) };
}