Functional Weave
Code in Rust

payroll.statutory-maternity-pay@1.0.1

impl/rust.rs

4,900 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::add_days;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_day_of_week::day_of_week;  ← from dates.day-of-week ^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_statutory_maternity_pay_data::{
    SMP_LOWER_EARNINGS_LIMITS, SMP_LOWER_EARNINGS_LIMITS_HISTORY, SMP_RATES, SMP_RATES_HISTORY,
};

const SMP_WEEKS: i64 = 39;
const EARNINGS_RELATED_WEEKS: i64 = 6;
/// 90% of average weekly earnings (SSCBA 1992 s.166(1)(a)).
const EARNINGS_BASIS_POINTS: i64 = 9000;

fn lookup(rows: &[(&'static str, Option<&'static str>, i64)], history: &str, date: &str, what: &str) -> i64 {
    for (from, to, value) in rows {
        if date >= *from && to.map_or(true, |t| date <= t) {
            return *value;
        }
    }
    if history != "full" {
        if let Some(earliest) = rows.iter().map(|r| r.0).min() {
            if date < earliest {
                panic!(
                    "no {} for {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for older cases.",
                    what, date, history, earliest
                );
            }
        }
    }
    panic!("no {} for {}", what, date)
}

/// Statutory Maternity Pay: whether it is due, and the 39 weekly amounts.
///
/// Two dates decide the figures and are easy to confuse. Eligibility uses the
/// lower earnings limit in force at the end of the qualifying week, which can
/// be in an earlier tax year than the pay; each week's flat rate is the one in
/// force on the day that SMP week starts.
///
/// # Panics
/// Panics on negative or non-GBP earnings, a malformed date, or a date no rule covers.
pub fn statutory_maternity_pay(average_weekly_earnings: &Money, due_date: &str, pay_start: &str) -> MaternityPay {
    if average_weekly_earnings.currency != "GBP" {
        panic!("SMP is paid in GBP, received {}", average_weekly_earnings.currency);
    }
    if average_weekly_earnings.minor < 0 {
        panic!("averageWeeklyEarnings must not be negative");
    }
    let rates: Vec<(&'static str, Option<&'static str>, i64)> =
        SMP_RATES.iter().map(|r| (r.valid_from, r.valid_to, r.weekly_rate)).collect();
    let limits: Vec<(&'static str, Option<&'static str>, i64)> =
        SMP_LOWER_EARNINGS_LIMITS.iter().map(|r| (r.valid_from, r.valid_to, r.weekly_limit)).collect();

    // The expected week of childbirth runs Sunday to Saturday (SSCBA 1992 s.171(1)).
    let expected_week = add_days(due_date, -(day_of_week(due_date) % 7));
    let qualifying_week_start = add_days(&expected_week, -15 * 7);
    add_days(pay_start, 0);
    let lel = lookup(&limits, SMP_LOWER_EARNINGS_LIMITS_HISTORY, &add_days(&qualifying_week_start, 6), "lower earnings limit");

    let eligible = average_weekly_earnings.minor >= lel;
    let mut weeks: Vec<MaternityPayWeek> = Vec::new();
    let mut total = 0i64;
    if eligible {
        // Kept in hundredths of a penny: HMRC says not to round average weekly
        // earnings, and reg 28 rounds only the payment, up.
        let earnings_rate = average_weekly_earnings.minor * EARNINGS_BASIS_POINTS;
        for week in 1..=SMP_WEEKS {
            let start = add_days(pay_start, 7 * (week - 1));
            let mut rate = earnings_rate;
            if week > EARNINGS_RELATED_WEEKS {
                rate = rate.min(lookup(&rates, SMP_RATES_HISTORY, &start, "SMP rate") * 10000);
            }
            let amount = round_div(rate, 10000, "up");
            total += amount;
            let end = add_days(&start, 6);
            weeks.push(MaternityPayWeek { week, start, end, amount: money(amount, "GBP") });
        }
    }
    MaternityPay {
        eligible,
        qualifying_week_start,
        lower_earnings_limit: money(lel, "GBP"),
        weeks,
        total: money(total, "GBP"),
    }
}

pub fn maternity_pay_to_value(pay: &MaternityPay) -> Value {
    Value::obj(vec![
        ("eligible", Value::Bool(pay.eligible)),
        ("qualifyingWeekStart", Value::str(&pay.qualifying_week_start)),
        ("lowerEarningsLimit", money_to_value(&pay.lower_earnings_limit)),
        (
            "weeks",
            Value::Arr(
                pay.weeks
                    .iter()
                    .map(|w| {
                        Value::obj(vec![
                            ("week", Value::Int(w.week)),
                            ("start", Value::str(&w.start)),
                            ("end", Value::str(&w.end)),
                            ("amount", money_to_value(&w.amount)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("total", money_to_value(&pay.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    maternity_pay_to_value(&statutory_maternity_pay(&money_from_value(&args[0]), args[1].as_str(), args[2].as_str()))
}