use super::funejson::Value; use super::finance_tax_vat_rate_data::{VAT_RULES, VAT_RULES_HISTORY}; 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()) } /// The VAT rate in force for a jurisdiction and category on a given date. /// /// The date matters. A credit note against a 2009 invoice has to use the rate /// that applied in 2009, not the one that applies today, and a system that /// hardcodes 0.20 cannot do that at all. /// /// ISO dates compare correctly as strings, so this needs no date crate. /// /// # Panics /// Panics if the date is malformed, or no rule covers the request. pub fn vat_rate(jurisdiction: &str, category: &str, on_date: &str) -> VatRate { if !is_iso_date(on_date) { panic!( "on_date must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date ); } let mut best: Option = None; for rule in VAT_RULES { if rule.jurisdiction != jurisdiction || rule.category != category { continue; } if on_date < rule.valid_from { continue; } if let Some(valid_to) = rule.valid_to { if on_date > valid_to { continue; } } let better = match &best { None => true, Some(current) => rule.valid_from > current.valid_from.as_str(), }; if better { best = Some(VatRate { jurisdiction: rule.jurisdiction.to_string(), category: rule.category.to_string(), basis_points: rule.basis_points, valid_from: rule.valid_from.to_string(), valid_to: rule.valid_to.map(str::to_string), }); } } match best { Some(rate) => rate, None => { // A build installed with history=current carries only the rules // still in force. Answering an older date with today's rate would // be the exact failure this capability exists to prevent, so say // what happened instead. The horizon that matters is this // jurisdiction and category's own earliest surviving rule, not the // registry-wide one. if VAT_RULES_HISTORY != "full" { let mut earliest: Option<&'static str> = None; for rule in VAT_RULES { if rule.jurisdiction != jurisdiction || rule.category != category { continue; } if earliest.is_none() || rule.valid_from < earliest.unwrap() { earliest = Some(rule.valid_from); } } if let Some(earliest) = earliest { if on_date < earliest { panic!( "no VAT rule for {}/{} on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full to reprint older invoices.", jurisdiction, category, on_date, VAT_RULES_HISTORY, earliest ); } } } panic!( "no VAT rule for {}/{} on {}", jurisdiction, category, on_date ) } } } /// Every category this registry knows a rate for, for a jurisdiction and date. pub fn vat_categories(jurisdiction: &str, on_date: &str) -> Vec<&'static str> { let mut seen: Vec<&'static str> = Vec::new(); for rule in VAT_RULES { if rule.jurisdiction != jurisdiction || on_date < rule.valid_from { continue; } if let Some(valid_to) = rule.valid_to { if on_date > valid_to { continue; } } if !seen.contains(&rule.category) { seen.push(rule.category); } } seen.sort_unstable(); seen } pub fn vat_rate_to_value(rate: &VatRate) -> Value { Value::obj(vec![ ("jurisdiction", Value::str(&rate.jurisdiction)), ("category", Value::str(&rate.category)), ("basisPoints", Value::Int(rate.basis_points)), ("validFrom", Value::str(&rate.valid_from)), ( "validTo", match &rate.valid_to { Some(date) => Value::str(date), None => Value::Null, }, ), ]) } pub fn fune_vector(args: &[Value]) -> Value { vat_rate_to_value(&vat_rate( args[0].as_str(), args[1].as_str(), args[2].as_str(), )) }