use super::funejson::Value; use super::dates_add_days::parse_iso_date; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::payroll_holiday_pay_accrual_data::{HolidayPayRule, HOLIDAY_PAY_RULES, HOLIDAY_PAY_RULES_HISTORY}; 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 = 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 ¤cy { 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 = 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())) }