use super::funejson::Value; use super::insurance_ipt_data::{IPT_RATES, IPT_RATES_HISTORY}; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; 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()) } /// Add UK Insurance Premium Tax to a net premium, or take it out of a gross /// one, at the rate in force on the date the insurer receives the premium. /// /// Only one figure is ever rounded - the tax when adding, the net when taking /// out - and the other is the difference, so net + tax is always the gross. /// /// # Panics /// Panics on an unknown band, a malformed date, or a date no rate covers. pub fn insurance_premium_tax(amount: &Money, includes_tax: bool, band: &str, on_date: &str, mode: &str) -> IptBreakdown { if band != "standard" && band != "higher" { panic!("unknown IPT band \"{}\": use standard or higher", band); } if !is_iso_date(on_date) { panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date); } let mut found: Option<(i64, &'static str)> = None; for r in IPT_RATES { if r.band != band || on_date < r.valid_from { continue; } if let Some(to) = r.valid_to { if on_date > to { continue; } } found = Some((r.basis_points, r.valid_from)); } let (bp, valid_from) = match found { Some(f) => f, None => { // A history=current build keeps only the rates in force; answering // an older date with today's rate would be exactly the wrong answer. if IPT_RATES_HISTORY != "full" { let mut earliest: Option<&'static str> = None; for r in IPT_RATES { if r.band == band && (earliest.is_none() || r.valid_from < earliest.unwrap()) { earliest = Some(r.valid_from); } } if let Some(earliest) = earliest { if on_date < earliest { panic!( "no IPT rule for {} on {}: this build was installed with history={}, so it only carries rates from {}. Reinstall with history=full for older premiums.", band, on_date, IPT_RATES_HISTORY, earliest ); } } } panic!("no IPT rule for {} on {}", band, on_date) } }; let (net, tax, gross) = if includes_tax { let net = money(round_div(amount.minor * 10000, 10000 + bp, mode), &amount.currency); let tax = money(amount.minor - net.minor, &amount.currency); (net, tax, amount.clone()) } else { let tax = money(round_div(amount.minor * bp, 10000, mode), &amount.currency); let gross = money(amount.minor + tax.minor, &amount.currency); (amount.clone(), tax, gross) }; IptBreakdown { net, tax, gross, basis_points: bp, band: band.to_string(), valid_from: valid_from.to_string(), } } pub fn ipt_breakdown_to_value(b: &IptBreakdown) -> Value { Value::obj(vec![ ("net", money_to_value(&b.net)), ("tax", money_to_value(&b.tax)), ("gross", money_to_value(&b.gross)), ("basisPoints", Value::Int(b.basis_points)), ("band", Value::str(&b.band)), ("validFrom", Value::str(&b.valid_from)), ]) } pub fn fune_vector(args: &[Value]) -> Value { ipt_breakdown_to_value(&insurance_premium_tax( &money_from_value(&args[0]), args[1].as_bool(), args[2].as_str(), args[3].as_str(), args[4].as_str(), )) }