construction.cis-deduction@1.0.2
impl/typescript.ts
2,950 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 RoundingMode } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { subtractMoney } from "./money_add.ts"; ← from money.add ^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 { applyRate } from "./money_apply_rate.ts"; ← from money.apply-rate ^1.0.0 · built alongside by fune
import { CIS_RATES, CIS_RATES_HISTORY } from "./construction_cis_deduction_data.ts"; ← this capability’s own data, compiled from data/cis-rates.json into the same file by fune build
import { type CisDeduction, type CisStatus } from "./construction_cis_deduction_types.ts";
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
function rateOn(status: string, onDate: string): number {
let best: { basisPoints: number; validFrom: string } | null = null;
let earliest: string | null = null;
for (const rule of CIS_RATES) {
if (rule.status !== status) 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.basisPoints;
// A history=current build has dropped the old rows; say so rather than
// answering an old payment at today's rate.
if (CIS_RATES_HISTORY !== "full" && earliest !== null && onDate < earliest) {
throw new RangeError(
`no CIS rate for ${status} on ${onDate}: this build was installed with history=${CIS_RATES_HISTORY}, ` +
`so it only carries rates from ${earliest}. Reinstall with history=full for older payments.`
);
}
throw new RangeError(`no CIS rate for ${status} on ${onDate}`);
}
/**
* The CIS deduction a contractor withholds from a subcontractor's payment.
*
* The rate applies only to the labour: VAT is left out by taking the payment
* net of VAT, and materials (with consumables, non-travel fuel, plant hire and
* prefabricated materials) are taken off before the rate is applied. Applying
* 20% to the whole invoice is the classic over-deduction.
*/
export function cisDeduction(payment: Money, materials: Money, status: CisStatus, onDate: string, mode: RoundingMode): CisDeduction {
if (status !== "gross" && status !== "standard" && status !== "higher") {
throw new RangeError(`unknown CIS status "${status}"`);
}
if (!ISO_DATE.test(onDate)) {
throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
}
if (payment.minor < 0) {
throw new RangeError(`payment must not be negative, received ${payment.minor}`);
}
if (materials.minor < 0) {
throw new RangeError(`materials must not be negative, received ${materials.minor}`);
}
const labour = subtractMoney(payment, materials);
if (labour.minor < 0) {
throw new RangeError(`materials (${materials.minor}) cannot exceed the payment (${payment.minor})`);
}
const basisPoints = rateOn(status, onDate);
const deduction = applyRate(labour, basisPoints, mode);
return {
payment,
materials,
labour,
basisPoints,
deduction,
netPayment: subtractMoney(payment, deduction),
};
}