Functional Weave
Code in Rust

payroll.minimum-wage-check@1.0.1

impl/rust.rs

4,012 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_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)),
    ])
}