use super::funejson::Value; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::retail_sales_tax_us_data::{STATE_RATES, STATE_RATES_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()) } fn state_rate(state: &str, on_date: &str) -> i64 { if !STATE_RATES.iter().any(|r| r.state == state) { panic!("unknown US state \"{}\"", state); } let mut best: Option<(&str, i64)> = None; for r in STATE_RATES { if r.state != state || on_date < r.valid_from { continue; } if let Some(to) = r.valid_to { if on_date > to { continue; } } if best.map_or(true, |(from, _)| r.valid_from > from) { best = Some((r.valid_from, r.rate_per_million)); } } if let Some((_, rate)) = best { return rate; } if STATE_RATES_HISTORY != "full" { let mut earliest: Option<&str> = None; for r in STATE_RATES { if r.state == state && earliest.map_or(true, |e| r.valid_from < e) { earliest = Some(r.valid_from); } } if let Some(earliest) = earliest { if on_date < earliest { panic!( "no sales tax rate for {} on {}: this build was installed with history={}, so it only carries rates from {}. Reinstall with history=full for older sales.", state, on_date, STATE_RATES_HISTORY, earliest ); } } } panic!("no sales tax rate for {} on {}", state, on_date) } /// US sales tax: the state's base rate on the date of sale plus the caller's /// local rate, applied once and rounded half up to the cent. /// /// # Panics /// Panics on a malformed date, a non-USD amount, an unknown state, a local /// rate outside 0..1000000, or a date no rate covers. pub fn sales_tax_us(amount: &Money, state: &str, local_rate_per_million: i64, on_date: &str) -> UsSalesTax { if !is_iso_date(on_date) { panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date); } if amount.currency != "USD" { panic!("US sales tax is charged in USD, received {}", amount.currency); } if !(0..1000000).contains(&local_rate_per_million) { panic!( "localRatePerMillion must be from 0 to 999999, received {}", local_rate_per_million ); } let state_rate_per_million = state_rate(state, on_date); let combined = state_rate_per_million + local_rate_per_million; let tax = money(round_div(amount.minor * combined, 1000000, "half-up"), "USD"); UsSalesTax { state: state.to_string(), state_rate_per_million, local_rate_per_million, combined_rate_per_million: combined, taxable: amount.clone(), total: money(amount.minor + tax.minor, "USD"), tax, } } pub fn us_sales_tax_to_value(t: &UsSalesTax) -> Value { Value::obj(vec![ ("state", Value::str(&t.state)), ("stateRatePerMillion", Value::Int(t.state_rate_per_million)), ("localRatePerMillion", Value::Int(t.local_rate_per_million)), ("combinedRatePerMillion", Value::Int(t.combined_rate_per_million)), ("taxable", money_to_value(&t.taxable)), ("tax", money_to_value(&t.tax)), ("total", money_to_value(&t.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { us_sales_tax_to_value(&sales_tax_us( &money_from_value(&args[0]), args[1].as_str(), args[2].as_i64(), args[3].as_str(), )) }