Functional Weave
Code in Rust

payroll.holiday-pay-accrual@1.0.1

impl/typescript.ts

2,915 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 { parseIsoDate } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { HOLIDAY_PAY_RULES, HOLIDAY_PAY_RULES_HISTORY, type HolidayPayRule } from "./payroll_holiday_pay_accrual_data.ts";  ← this capability’s own data, compiled from data/holiday-pay-rules.json into the same file by fune build
import { type HolidayPay, type HolidayPayMethod } from "./payroll_holiday_pay_accrual_types.ts";

function ruleFor(method: string, onDate: string): HolidayPayRule {
  let best: HolidayPayRule | null = null;
  for (const rule of HOLIDAY_PAY_RULES) {
    if (rule.method !== method || onDate < rule.validFrom) continue;
    if (rule.validTo !== null && onDate > rule.validTo) continue;
    if (best === null || rule.validFrom > best.validFrom) best = rule;
  }
  if (best === null) {
    if (HOLIDAY_PAY_RULES_HISTORY !== "full") {
      throw new RangeError(`no ${method} holiday pay rule for ${onDate}: this build was installed with history=${HOLIDAY_PAY_RULES_HISTORY}. Reinstall with history=full for older dates.`);
    }
    throw new RangeError(`no ${method} holiday pay rule for ${onDate}`);
  }
  return best;
}

/**
 * Holiday pay under the Working Time Regulations 1998, by one of two methods.
 *
 * Rolled-up: a 12.07% uplift on pay for work done, paid with it, allowed only
 * for irregular-hours and part-year workers in leave years from 1 April 2024.
 * Average week: a week's pay as the average over the most recent 52 weeks in
 * which the worker was paid, skipping unpaid weeks but never looking back more
 * than 104 weeks.
 */
export function holidayPay(method: HolidayPayMethod, periodPay: Money, weeklyPay: readonly Money[], onDate: string): HolidayPay {
  parseIsoDate(onDate);
  if (method === "rolled-up") {
    const rule = ruleFor(method, onDate);
    if (periodPay.minor < 0) throw new RangeError("periodPay must not be negative");
    return { amount: money(roundDiv(periodPay.minor * rule.accrualBasisPoints, 10000, "half-up"), periodPay.currency), weeksCounted: 0 };
  }
  if (method !== "average-week") throw new RangeError(`unknown holiday pay method "${method}"`);

  const rule = ruleFor(method, onDate);
  let total = 0;
  let counted = 0;
  let currency: string | null = null;
  const window = Math.min(weeklyPay.length, rule.lookbackWeeks);
  for (let i = 0; i < window && counted < rule.referenceWeeks; i++) {
    const week = weeklyPay[i];
    if (currency === null) currency = week.currency;
    else if (week.currency !== currency) throw new RangeError(`currency mismatch: ${currency} and ${week.currency}`);
    if (week.minor < 0) throw new RangeError("weekly pay must not be negative");
    if (week.minor === 0) continue;
    total += week.minor;
    counted += 1;
  }
  if (counted === 0 || currency === null) throw new RangeError(`no week with pay in the last ${rule.lookbackWeeks} weeks`);
  return { amount: money(roundDiv(total, counted, "half-up"), currency), weeksCounted: counted };
}