1use crate::errors::InputValidationError;
9use log::debug;
10use serde::{Deserialize, Deserializer, Serialize};
11use std::collections::HashMap;
12use strum_macros::Display;
13use typeshare::typeshare;
14
15#[typeshare]
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum TaxSystemType {
20 Vat,
22 Gst,
24 Pst,
26 Hst,
28 Qst,
30 None,
32}
33
34#[typeshare]
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[serde(rename_all = "lowercase")]
38pub enum TransactionType {
39 B2B,
41 B2C,
43}
44
45#[typeshare]
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum TaxCalculationType {
50 Origin,
52 Destination,
54 ReverseCharge,
56 ZeroRated,
58 Exempt,
60 None,
62 ThresholdBased,
64}
65
66#[typeshare]
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69#[serde(tag = "type", content = "content")]
70#[serde(rename_all = "snake_case")]
71pub enum TaxType {
72 VAT(VatRate),
74 GST,
76 HST,
78 PST,
80 QST,
82 StateSalesTax,
84}
85
86#[typeshare]
88#[derive(Debug, Clone, Display, Serialize, Deserialize, PartialEq)]
89#[serde(rename_all = "snake_case")]
90pub enum VatRate {
91 Standard,
93 Reduced,
95 ReducedAlt,
97 SuperReduced,
99 Zero,
101 Exempt,
103 ReverseCharge,
105}
106
107#[typeshare]
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum TradeAgreementType {
112 CustomsUnion,
114 FederalState,
116}
117
118#[typeshare]
120#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(tag = "type", content = "content")]
122pub enum TradeAgreementOverride {
123 UseAgreement(String),
125 NoAgreement,
127}
128
129#[typeshare]
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct AppliesTo {
133 pub physical_goods: bool,
135 pub digital_goods: bool,
137 pub services: bool,
139}
140
141#[typeshare]
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct TradeAgreement {
145 pub name: String,
147 pub r#type: TradeAgreementType,
149 pub members: Vec<String>,
151 pub default_applicable: bool,
153 pub applies_to: AppliesTo,
155 pub tax_rules: TaxRules,
157}
158
159impl TradeAgreement {
160 pub fn is_federal(&self) -> bool {
162 self.r#type == TradeAgreementType::FederalState
163 }
164
165 pub fn is_international(&self) -> bool {
167 self.r#type == TradeAgreementType::CustomsUnion
168 }
169}
170
171#[typeshare]
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct TaxRuleConfig {
175 pub r#type: TaxCalculationType,
177 pub below_threshold: Option<TaxCalculationType>,
179 pub above_threshold: Option<TaxCalculationType>,
181 pub threshold: Option<u32>,
183 pub below_threshold_digital_products: Option<TaxCalculationType>,
185 pub above_threshold_digital_products: Option<TaxCalculationType>,
187 pub threshold_digital_products: Option<u32>,
189 pub requires_resale_certificate: Option<bool>,
191}
192
193impl TaxRuleConfig {
194 pub fn by_threshold(&self, amount: u32, ignore_threshold: bool) -> &TaxCalculationType {
200 let has_threshold = self.below_threshold.is_some()
201 && self.above_threshold.is_some()
202 && self.threshold.is_some();
203 if has_threshold {
204 let rule_threshold: u32 = self.threshold.unwrap();
205 if amount < rule_threshold && !ignore_threshold {
206 return self.below_threshold.as_ref().unwrap();
207 } else {
208 return self.above_threshold.as_ref().unwrap();
209 }
210 }
211 &self.r#type
212 }
213
214 pub fn by_digital_product_threshold(
216 &self,
217 amount: u32,
218 ignore_threshold: bool,
219 ) -> &TaxCalculationType {
220 let has_threshold = self.below_threshold_digital_products.is_some()
221 && self.above_threshold_digital_products.is_some()
222 && self.threshold_digital_products.is_some();
223 if has_threshold {
224 let rule_threshold: u32 = self.threshold_digital_products.unwrap();
225 if amount < rule_threshold && !ignore_threshold {
226 return self.below_threshold_digital_products.as_ref().unwrap();
227 } else {
228 return self.above_threshold_digital_products.as_ref().unwrap();
229 }
230 }
231 &self.r#type
232 }
233
234 pub fn by_threshold_or_digital_product_threshold(
236 &self,
237 amount: u32,
238 is_digital_product_or_service: bool,
239 ignore_threshold: bool,
240 ) -> &TaxCalculationType {
241 if is_digital_product_or_service {
242 return self.by_digital_product_threshold(amount, ignore_threshold);
243 }
244 self.by_threshold(amount, ignore_threshold)
245 }
246
247 pub fn is_reseller(&self, has_resale_certificate: bool) -> bool {
249 if self.requires_resale_certificate.is_some() {
250 return self.requires_resale_certificate.unwrap() && has_resale_certificate;
251 }
252 false
253 }
254}
255
256#[typeshare]
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct TaxRules {
260 pub internal_b2b: Option<TaxRuleConfig>,
262 pub internal_b2c: Option<TaxRuleConfig>,
264 pub external_export: TaxRuleConfig,
266}
267
268#[typeshare]
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct ProductRules {
272 pub default: String,
274 pub specific_products: HashMap<String, String>,
276}
277
278#[typeshare]
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct State {
282 pub standard_rate: f64,
284 #[serde(rename = "type")]
286 pub tax_type: TaxSystemType,
287}
288
289fn deserialize_rate<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
291where
292 D: Deserializer<'de>,
293{
294 #[derive(Deserialize)]
295 #[serde(untagged)]
296 enum RateValue {
297 Number(f64),
298 Boolean(bool),
299 }
300
301 match Option::<RateValue>::deserialize(deserializer)? {
302 Some(RateValue::Number(rate)) => Ok(Some(rate)),
303 Some(RateValue::Boolean(false)) => Ok(None),
304 Some(RateValue::Boolean(true)) => Ok(None),
305 None => Ok(None),
306 }
307}
308
309#[typeshare]
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct Country {
313 #[serde(rename = "type")]
315 pub tax_type: TaxSystemType,
316 pub currency: String,
318 pub standard_rate: f64,
320 #[serde(default, deserialize_with = "deserialize_rate")]
322 pub reduced_rate: Option<f64>,
323 #[serde(default, deserialize_with = "deserialize_rate")]
325 pub reduced_rate_alt: Option<f64>,
326 #[serde(default, deserialize_with = "deserialize_rate")]
328 pub super_reduced_rate: Option<f64>,
329 #[serde(default, deserialize_with = "deserialize_rate")]
331 pub parking_rate: Option<f64>,
332 pub vat_name: Option<String>,
334 pub vat_abbr: Option<String>,
336 pub states: Option<HashMap<String, State>>,
338}
339
340#[typeshare]
353#[derive(Debug, Clone)]
354pub struct Region {
355 pub country: String,
357 pub region: Option<String>,
359}
360
361impl Region {
362 pub fn new(country: String, region: Option<String>) -> Result<Self, InputValidationError> {
364 Self::validate(&country, ®ion)?;
365 Ok(Self { country, region })
366 }
367
368 fn validate(country: &str, region: &Option<String>) -> Result<(), InputValidationError> {
370 let country_info = rust_iso3166::from_alpha2(country)
371 .ok_or_else(|| InputValidationError::InvalidCountryCode(country.to_string()))?;
372
373 debug!("Found country: {}", country_info.name);
374
375 if let Some(region_code) = region {
376 let _ = country_info
377 .subdivisions()
378 .ok_or_else(|| InputValidationError::UnexpectedRegionCode(region_code.clone()))?;
379
380 let region_info = rust_iso3166::iso3166_2::from_code(region_code)
381 .ok_or_else(|| InputValidationError::InvalidRegionCode(region_code.clone()))?;
382
383 debug!("Found region: {}", region_info.name);
384 }
385
386 Ok(())
387 }
388}
389
390#[typeshare]
392#[derive(Debug, Clone)]
393pub struct TaxScenario {
394 pub source_region: Region,
396 pub destination_region: Region,
398 pub transaction_type: TransactionType,
400 pub trade_agreement_override: Option<TradeAgreementOverride>,
404 pub is_digital_product_or_service: bool,
406 pub has_resale_certificate: bool,
408 pub ignore_threshold: bool,
410 pub vat_rate: Option<VatRate>,
412}
413
414#[typeshare]
416#[derive(Debug, Serialize, Deserialize)]
417pub struct TaxRate {
418 pub rate: f64,
420 pub tax_type: TaxType,
422 pub compound: bool,
424}