use super::funejson::Value; use super::construction_cis_deduction_data::{CIS_RATES, CIS_RATES_HISTORY}; use super::money_add::subtract_money; use super::money_amount::{money_from_value, money_to_value, Money}; use super::money_apply_rate::apply_rate; fn is_iso_date(value: &str) -> bool { let bytes = value.as_bytes(); bytes.len() == 10 && bytes[4] == b'-' && bytes[7] == b'-' && bytes .iter() .enumerate() .all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit()) } fn rate_on(status: &str, on_date: &str) -> i64 { let mut best: Option<(i64, &'static str)> = None; let mut earliest: Option<&'static str> = None; for rule in CIS_RATES { if rule.status != status { continue; } if earliest.map_or(true, |e| rule.valid_from < e) { earliest = Some(rule.valid_from); } if on_date < rule.valid_from { continue; } if let Some(valid_to) = rule.valid_to { if on_date > valid_to { continue; } } if best.map_or(true, |(_, from)| rule.valid_from > from) { best = Some((rule.basis_points, rule.valid_from)); } } if let Some((basis_points, _)) = best { return basis_points; } // A history=current build has dropped the old rows; say so rather than // answering an old payment at today's rate. if CIS_RATES_HISTORY != "full" { if let Some(earliest) = earliest { if on_date < earliest { panic!( "no CIS rate for {} on {}: this build was installed with history={}, so it only carries rates from {}. Reinstall with history=full for older payments.", status, on_date, CIS_RATES_HISTORY, earliest ); } } } panic!("no CIS rate for {} on {}", status, on_date) } /// The CIS deduction a contractor withholds from a subcontractor's payment. /// /// The rate applies only to the labour: VAT is left out by taking the payment /// net of VAT, and materials (with consumables, non-travel fuel, plant hire and /// prefabricated materials) are taken off before the rate is applied. Applying /// 20% to the whole invoice is the classic over-deduction. /// /// # Panics /// Panics on an unknown status, a malformed date, a negative payment or /// materials, materials above the payment, mixed currencies, or a date no rate /// covers. pub fn cis_deduction(payment: &Money, materials: &Money, status: &str, on_date: &str, mode: &str) -> CisDeduction { if status != "gross" && status != "standard" && status != "higher" { panic!("unknown CIS status \"{}\"", status); } if !is_iso_date(on_date) { panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date); } if payment.minor < 0 { panic!("payment must not be negative, received {}", payment.minor); } if materials.minor < 0 { panic!("materials must not be negative, received {}", materials.minor); } let labour = subtract_money(payment, materials); if labour.minor < 0 { panic!( "materials ({}) cannot exceed the payment ({})", materials.minor, payment.minor ); } let basis_points = rate_on(status, on_date); let deduction = apply_rate(&labour, basis_points, mode); CisDeduction { payment: payment.clone(), materials: materials.clone(), labour, basis_points, net_payment: subtract_money(payment, &deduction), deduction, } } pub fn cis_deduction_to_value(d: &CisDeduction) -> Value { Value::obj(vec![ ("payment", money_to_value(&d.payment)), ("materials", money_to_value(&d.materials)), ("labour", money_to_value(&d.labour)), ("basisPoints", Value::Int(d.basis_points)), ("deduction", money_to_value(&d.deduction)), ("netPayment", money_to_value(&d.net_payment)), ]) } pub fn fune_vector(args: &[Value]) -> Value { cis_deduction_to_value(&cis_deduction( &money_from_value(&args[0]), &money_from_value(&args[1]), args[2].as_str(), args[3].as_str(), args[4].as_str(), )) }