Functional Weave
Code in Python

payroll.minimum-wage-check@1.0.1

impl/typescript.ts

3,031 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 { addMonths } from "./dates_add_months.ts";  ← from dates.add-months ^1.0.0 · built alongside by fune
import { age } from "./dates_age.ts";  ← from dates.age ^1.0.0 · built alongside by fune
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 { NMW_RATES, NMW_RATES_HISTORY, type NmwRate } from "./payroll_minimum_wage_check_data.ts";  ← this capability’s own data, compiled from data/rates.json into the same file by fune build
import { type MinimumWageCheck, type WageBand } from "./payroll_minimum_wage_check_types.ts";

function ratesInForce(periodStart: string): NmwRate[] {
  const rows = NMW_RATES.filter((r) => periodStart >= r.validFrom && (r.validTo === null || periodStart <= r.validTo));
  if (rows.length > 0) return rows;
  if (NMW_RATES_HISTORY !== "full") {
    let earliest: string | null = null;
    for (const r of NMW_RATES) if (earliest === null || r.validFrom < earliest) earliest = r.validFrom;
    if (earliest !== null && periodStart < earliest) {
      throw new RangeError(
        `no minimum wage rate for ${periodStart}: this build was installed with history=${NMW_RATES_HISTORY}, ` +
          `so it only carries rates from ${earliest}. Reinstall with history=full to check older pay.`,
      );
    }
  }
  throw new RangeError(`no minimum wage rate for ${periodStart}`);
}

/**
 * Minimum wage check for one pay reference period. The rate and the age are
 * both taken on the first day of the period, never on the pay date.
 */
export function minimumWageCheck(
  pay: Money,
  minutesWorked: number,
  birthDate: string,
  periodStart: string,
  apprenticeshipStart: string | null,
): MinimumWageCheck {
  if (pay.currency !== "GBP") throw new RangeError(`the minimum wage is in GBP, received ${pay.currency}`);
  if (!Number.isInteger(minutesWorked) || minutesWorked < 0) {
    throw new RangeError(`minutesWorked must be a whole number of minutes, not negative, received ${minutesWorked}`);
  }
  const years = age(birthDate, periodStart);
  if (years < 16) throw new RangeError(`a worker aged ${years} is below school leaving age: no minimum wage applies`);
  if (apprenticeshipStart !== null && apprenticeshipStart > periodStart) {
    throw new RangeError(`apprenticeshipStart ${apprenticeshipStart} is after periodStart ${periodStart}`);
  }
  const rows = ratesInForce(periodStart);
  const apprenticeRate =
    apprenticeshipStart !== null && (years < 19 || periodStart < addMonths(apprenticeshipStart, 12));
  let chosen: NmwRate | null = null;
  for (const row of rows) {
    if (apprenticeRate) {
      if (row.band === "apprentice") chosen = row;
    } else if (row.band !== "apprentice" && row.minAge <= years && (chosen === null || row.minAge > chosen.minAge)) {
      chosen = row;
    }
  }
  if (chosen === null) throw new RangeError(`no minimum wage rate for ${periodStart}`);
  const required = roundDiv(chosen.rateMinor * minutesWorked, 60, "up");
  const shortfall = Math.max(0, required - pay.minor);
  return {
    band: chosen.band as WageBand,
    hourlyRate: money(chosen.rateMinor, "GBP"),
    required: money(required, "GBP"),
    shortfall: money(shortfall, "GBP"),
    compliant: shortfall === 0,
  };
}