Functional Weave
Code in TypeScript

construction.cis-deduction@1.0.0

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