Functional Weave
Code in Rust

payroll.holiday-pay-accrual@1.0.1

impl/rust.rs

3,923 bytes · the Rust 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::parse_iso_date;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::payroll_holiday_pay_accrual_data::{HolidayPayRule, HOLIDAY_PAY_RULES, HOLIDAY_PAY_RULES_HISTORY};  ← this capability’s own data, compiled from data/holiday-pay-rules.json into the same file by fune build

fn rule_for(method: &str, on_date: &str) -> &'static HolidayPayRule {
    let mut best: Option<&'static HolidayPayRule> = None;
    for rule in HOLIDAY_PAY_RULES {
        if rule.method != method || on_date < rule.valid_from {
            continue;
        }
        if let Some(to) = rule.valid_to {
            if on_date > to {
                continue;
            }
        }
        if best.map_or(true, |b| rule.valid_from > b.valid_from) {
            best = Some(rule);
        }
    }
    match best {
        Some(rule) => rule,
        None => {
            if HOLIDAY_PAY_RULES_HISTORY != "full" {
                panic!(
                    "no {} holiday pay rule for {}: this build was installed with history={}. Reinstall with history=full for older dates.",
                    method, on_date, HOLIDAY_PAY_RULES_HISTORY
                );
            }
            panic!("no {} holiday pay rule for {}", method, on_date)
        }
    }
}

/// 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.
///
/// # Panics
/// Panics on negative pay, mixed currencies, no paid week, a malformed date,
/// an unknown method, or a date no rule covers.
pub fn holiday_pay(method: &str, period_pay: &Money, weekly_pay: &[Money], on_date: &str) -> HolidayPay {
    parse_iso_date(on_date);
    if method == "rolled-up" {
        let rule = rule_for(method, on_date);
        if period_pay.minor < 0 {
            panic!("periodPay must not be negative");
        }
        return HolidayPay {
            amount: money(round_div(period_pay.minor * rule.accrual_basis_points, 10000, "half-up"), &period_pay.currency),
            weeks_counted: 0,
        };
    }
    if method != "average-week" {
        panic!("unknown holiday pay method \"{}\"", method);
    }

    let rule = rule_for(method, on_date);
    let mut total = 0i64;
    let mut counted = 0i64;
    let mut currency: Option<String> = None;
    let window = weekly_pay.len().min(rule.lookback_weeks as usize);
    for week in &weekly_pay[..window] {
        if counted >= rule.reference_weeks {
            break;
        }
        match &currency {
            None => currency = Some(week.currency.clone()),
            Some(c) => {
                if *c != week.currency {
                    panic!("currency mismatch: {} and {}", c, week.currency);
                }
            }
        }
        if week.minor < 0 {
            panic!("weekly pay must not be negative");
        }
        if week.minor == 0 {
            continue;
        }
        total += week.minor;
        counted += 1;
    }
    match currency {
        Some(c) if counted > 0 => HolidayPay { amount: money(round_div(total, counted, "half-up"), &c), weeks_counted: counted },
        _ => panic!("no week with pay in the last {} weeks", rule.lookback_weeks),
    }
}

pub fn holiday_pay_to_value(pay: &HolidayPay) -> Value {
    Value::obj(vec![
        ("amount", money_to_value(&pay.amount)),
        ("weeksCounted", Value::Int(pay.weeks_counted)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let weeks: Vec<Money> = args[2].as_arr().iter().map(money_from_value).collect();
    holiday_pay_to_value(&holiday_pay(args[0].as_str(), &money_from_value(&args[1]), &weeks, args[3].as_str()))
}