use super::funejson::Value; use super::dates_add_days::add_days; use super::dates_day_of_week::day_of_week; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; 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 = 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())) }