Functional Weave
Code in Rust

payroll.statutory-maternity-pay@1.0.0

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