payroll.statutory-maternity-pay@1.0.1
impl/typescript.ts
3,452 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 { addDays } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { dayOfWeek } from "./dates_day_of_week.ts"; ← from dates.day-of-week ^1.0.0 · built alongside by fune
import { SMP_LOWER_EARNINGS_LIMITS, SMP_LOWER_EARNINGS_LIMITS_HISTORY, SMP_RATES, SMP_RATES_HISTORY } from "./payroll_statutory_maternity_pay_data.ts"; ← this capability’s own data, compiled from data/smp-rates.json into the same file by fune build
import { type MaternityPay, type MaternityPayWeek } from "./payroll_statutory_maternity_pay_types.ts";
const SMP_WEEKS = 39;
const EARNINGS_RELATED_WEEKS = 6;
/** 90% of average weekly earnings (SSCBA 1992 s.166(1)(a)). */
const EARNINGS_BASIS_POINTS = 9000;
interface Dated {
readonly validFrom: string;
readonly validTo: string | null;
}
function lookup<T extends Dated>(rows: readonly T[], history: string, date: string, what: string): T {
for (const row of rows) {
if (date >= row.validFrom && (row.validTo === null || date <= row.validTo)) return row;
}
if (history !== "full") {
const earliest = rows.reduce<string | null>((e, r) => (e === null || r.validFrom < e ? r.validFrom : e), null);
if (earliest !== null && date < earliest) {
throw new RangeError(`no ${what} for ${date}: this build was installed with history=${history}, so it only carries rules from ${earliest}. Reinstall with history=full for older cases.`);
}
}
throw new RangeError(`no ${what} for ${date}`);
}
/**
* Statutory Maternity Pay: whether it is due, and the 39 weekly amounts.
*
* Two dates decide the figures and are easy to confuse. Eligibility uses the
* lower earnings limit in force at the end of the qualifying week, which can
* be in an earlier tax year than the pay; each week's flat rate is the one in
* force on the day that SMP week starts.
*/
export function statutoryMaternityPay(averageWeeklyEarnings: Money, dueDate: string, payStart: string): MaternityPay {
if (averageWeeklyEarnings.currency !== "GBP") throw new RangeError(`SMP is paid in GBP, received ${averageWeeklyEarnings.currency}`);
if (averageWeeklyEarnings.minor < 0) throw new RangeError("averageWeeklyEarnings must not be negative");
// The expected week of childbirth runs Sunday to Saturday (SSCBA 1992 s.171(1)).
const expectedWeek = addDays(dueDate, -(dayOfWeek(dueDate) % 7));
const qualifyingWeekStart = addDays(expectedWeek, -15 * 7);
addDays(payStart, 0);
const lel = lookup(SMP_LOWER_EARNINGS_LIMITS, SMP_LOWER_EARNINGS_LIMITS_HISTORY, addDays(qualifyingWeekStart, 6), "lower earnings limit").weeklyLimit;
const weeks: MaternityPayWeek[] = [];
let total = 0;
if (averageWeeklyEarnings.minor >= lel) {
// Kept in hundredths of a penny: HMRC says not to round average weekly
// earnings, and reg 28 rounds only the payment, up.
const earningsRate = averageWeeklyEarnings.minor * EARNINGS_BASIS_POINTS;
for (let week = 1; week <= SMP_WEEKS; week++) {
const start = addDays(payStart, 7 * (week - 1));
let rate = earningsRate;
if (week > EARNINGS_RELATED_WEEKS) {
rate = Math.min(rate, lookup(SMP_RATES, SMP_RATES_HISTORY, start, "SMP rate").weeklyRate * 10000);
}
const amount = roundDiv(rate, 10000, "up");
total += amount;
weeks.push({ week, start, end: addDays(start, 6), amount: money(amount, "GBP") });
}
}
return {
eligible: averageWeeklyEarnings.minor >= lel,
qualifyingWeekStart,
lowerEarningsLimit: money(lel, "GBP"),
weeks,
total: money(total, "GBP"),
};
}