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_months::add_months; ← from dates.add-months ^1.0.0 · built alongside by fune
use super::dates_age::age; ← from dates.age ^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_minimum_wage_check_data::{NmwRate, NMW_RATES, NMW_RATES_HISTORY}; ← this capability’s own data, compiled from data/rates.json into the same file by fune build
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)),
])
}