use super::funejson::Value; use super::dates_add_months::add_months; use super::dates_age::age; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::payroll_minimum_wage_check_data::{NmwRate, NMW_RATES, NMW_RATES_HISTORY}; fn rates_in_force(period_start: &str) -> Vec<&'static NmwRate> { let rows: Vec<&'static NmwRate> = NMW_RATES .iter() .filter(|r| period_start >= r.valid_from && r.valid_to.map_or(true, |to| period_start <= to)) .collect(); if !rows.is_empty() { return rows; } if NMW_RATES_HISTORY != "full" { if let Some(earliest) = NMW_RATES.iter().map(|r| r.valid_from).min() { if period_start < earliest { panic!( "no minimum wage rate for {}: this build was installed with history={}, so it only carries rates from {}. Reinstall with history=full to check older pay.", period_start, NMW_RATES_HISTORY, earliest ); } } } panic!("no minimum wage rate for {}", period_start) } /// Minimum wage check for one pay reference period. The rate and the age are /// both taken on the first day of the period, never on the pay date. /// /// # Panics /// Panics on non-GBP pay, negative minutes, a worker under 16, an /// apprenticeship starting after the period, or a date with no rates. pub fn minimum_wage_check( pay: &Money, minutes_worked: i64, birth_date: &str, period_start: &str, apprenticeship_start: Option<&str>, ) -> MinimumWageCheck { if pay.currency != "GBP" { panic!("the minimum wage is in GBP, received {}", pay.currency); } if minutes_worked < 0 { panic!("minutesWorked must be a whole number of minutes, not negative, received {}", minutes_worked); } let years = age(birth_date, period_start); if years < 16 { panic!("a worker aged {} is below school leaving age: no minimum wage applies", years); } if let Some(start) = apprenticeship_start { if start > period_start { panic!("apprenticeshipStart {} is after periodStart {}", start, period_start); } } let rows = rates_in_force(period_start); let apprentice_rate = match apprenticeship_start { Some(start) => years < 19 || period_start < add_months(start, 12).as_str(), None => false, }; let mut chosen: Option<&NmwRate> = None; for row in rows { if apprentice_rate { if row.band == "apprentice" { chosen = Some(row); } } else if row.band != "apprentice" && row.min_age <= years && chosen.map_or(true, |c| row.min_age > c.min_age) { chosen = Some(row); } } let chosen = match chosen { Some(c) => c, None => panic!("no minimum wage rate for {}", period_start), }; let required = round_div(chosen.rate_minor * minutes_worked, 60, "up"); let shortfall = (required - pay.minor).max(0); MinimumWageCheck { band: chosen.band.to_string(), hourly_rate: money(chosen.rate_minor, "GBP"), required: money(required, "GBP"), shortfall: money(shortfall, "GBP"), compliant: shortfall == 0, } } pub fn fune_vector(args: &[Value]) -> Value { if let Value::Float(f) = &args[1] { panic!("minutesWorked must be a whole number of minutes, not negative, received {}", f); } let apprenticeship = if args[4].is_null() { None } else { Some(args[4].as_str()) }; let c = minimum_wage_check(&money_from_value(&args[0]), args[1].as_i64(), args[2].as_str(), args[3].as_str(), apprenticeship); Value::obj(vec![ ("band", Value::str(&c.band)), ("hourlyRate", money_to_value(&c.hourly_rate)), ("required", money_to_value(&c.required)), ("shortfall", money_to_value(&c.shortfall)), ("compliant", Value::Bool(c.compliant)), ]) }