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