1use log::debug;
9use serde::{Deserialize, Deserializer, Serialize};
10use strum_macros::Display;
11use std::collections::HashMap;
12use crate::errors::InputValidationError;
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum TaxSystemType {
18 Vat,
20 Gst,
22 Pst,
24 Hst,
26 Qst,
28 None,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34#[serde(rename_all = "lowercase")]
35pub enum TransactionType {
36 B2B,
38 B2C,
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum TaxCalculationType {
46 Origin,
48 Destination,
50 ReverseCharge,
52 ZeroRated,
54 Exempt,
56 None,
58 ThresholdBased
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize ,PartialEq)]
64#[serde(rename_all = "snake_case")]
65pub enum TaxType {
66 VAT(VatRate),
68 GST,
70 HST,
72 PST,
74 QST,
76 StateSalesTax,
78}
79
80#[derive(Debug, Clone, Display, Serialize, Deserialize, PartialEq)]
82#[serde(rename_all = "snake_case")]
83pub enum VatRate {
84 Standard,
86 Reduced,
88 ReducedAlt,
90 SuperReduced,
92 Zero,
94 Exempt,
96 ReverseCharge
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum TradeAgreementType {
104 CustomsUnion,
106 FederalState
108}
109
110#[derive(Debug, Clone)]
112pub enum TradeAgreementOverride {
113 UseAgreement(String),
115 NoAgreement,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct AppliesTo {
122 pub physical_goods: bool,
124 pub digital_goods: bool,
126 pub services: bool,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct TradeAgreement {
133 pub name: String,
135 pub r#type: TradeAgreementType,
137 pub members: Vec<String>,
139 pub default_applicable: bool,
141 pub applies_to: AppliesTo,
143 pub tax_rules: TaxRules,
145}
146
147impl TradeAgreement {
148 pub fn is_federal(&self) -> bool {
150 self.r#type == TradeAgreementType::FederalState
151 }
152
153 pub fn is_international(&self) -> bool {
155 self.r#type == TradeAgreementType::CustomsUnion
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct TaxRuleConfig {
162 pub r#type: TaxCalculationType,
164 pub below_threshold: Option<TaxCalculationType>,
166 pub above_threshold: Option<TaxCalculationType>,
168 pub threshold: Option<u32>,
170 pub below_threshold_digital_products: Option<TaxCalculationType>,
172 pub above_threshold_digital_products: Option<TaxCalculationType>,
174 pub threshold_digital_products: Option<u32>,
176 pub requires_resale_certificate: Option<bool>,
178}
179
180impl TaxRuleConfig {
181 pub fn by_threshold(&self, amount: u32, ignore_threshold: bool) -> &TaxCalculationType {
187 let has_threshold = self.below_threshold.is_some() && self.above_threshold.is_some() && self.threshold.is_some();
188 if has_threshold {
189 let rule_threshold: u32 = self.threshold.unwrap();
190 if amount < rule_threshold && !ignore_threshold {
191 return self.below_threshold.as_ref().unwrap();
192 } else {
193 return self.above_threshold.as_ref().unwrap();
194 }
195 }
196 &self.r#type
197 }
198
199 pub fn by_digital_product_threshold(&self, amount: u32, ignore_threshold: bool) -> &TaxCalculationType {
201 let has_threshold = self.below_threshold_digital_products.is_some()
202 && self.above_threshold_digital_products.is_some()
203 && self.threshold_digital_products.is_some();
204 if has_threshold {
205 let rule_threshold: u32 = self.threshold_digital_products.unwrap();
206 if amount < rule_threshold && !ignore_threshold {
207 return self.below_threshold_digital_products.as_ref().unwrap();
208 } else {
209 return self.above_threshold_digital_products.as_ref().unwrap();
210 }
211 }
212 &self.r#type
213 }
214
215 pub fn by_threshold_or_digital_product_threshold(
217 &self,
218 amount: u32,
219 is_digital_product_or_service: bool,
220 ignore_threshold: bool
221 ) -> &TaxCalculationType {
222 if is_digital_product_or_service {
223 return self.by_digital_product_threshold(amount, ignore_threshold);
224 }
225 self.by_threshold(amount, ignore_threshold)
226 }
227
228 pub fn is_reseller(&self, has_resale_certificate: bool) -> bool {
230 if self.requires_resale_certificate.is_some() {
231 return self.requires_resale_certificate.unwrap() && has_resale_certificate;
232 }
233 false
234 }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct TaxRules {
240 pub internal_b2b: Option<TaxRuleConfig>,
242 pub internal_b2c: Option<TaxRuleConfig>,
244 pub external_export: TaxRuleConfig,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ProductRules {
251 pub default: String,
253 pub specific_products: HashMap<String, String>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct State {
260 pub standard_rate: f64,
262 #[serde(rename = "type")]
264 pub tax_type: TaxSystemType,
265}
266
267fn deserialize_rate<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
269where
270 D: Deserializer<'de>,
271{
272 #[derive(Deserialize)]
273 #[serde(untagged)]
274 enum RateValue {
275 Number(f64),
276 Boolean(bool),
277 }
278
279 match Option::<RateValue>::deserialize(deserializer)? {
280 Some(RateValue::Number(rate)) => Ok(Some(rate)),
281 Some(RateValue::Boolean(false)) => Ok(None),
282 Some(RateValue::Boolean(true)) => Ok(None),
283 None => Ok(None),
284 }
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct Country {
290 #[serde(rename = "type")]
292 pub tax_type: TaxSystemType,
293 pub currency: String,
295 pub standard_rate: f64,
297 #[serde(default, deserialize_with = "deserialize_rate")]
299 pub reduced_rate: Option<f64>,
300 #[serde(default, deserialize_with = "deserialize_rate")]
302 pub reduced_rate_alt: Option<f64>,
303 #[serde(default, deserialize_with = "deserialize_rate")]
305 pub super_reduced_rate: Option<f64>,
306 #[serde(default, deserialize_with = "deserialize_rate")]
308 pub parking_rate: Option<f64>,
309 pub vat_name: Option<String>,
311 pub vat_abbr: Option<String>,
313 pub states: Option<HashMap<String, State>>,
315}
316
317#[derive(Debug, Clone)]
330pub struct Region {
331 pub country: String,
333 pub region: Option<String>,
335}
336
337impl Region {
338 pub fn new(country: String, region: Option<String>) -> Result<Self, InputValidationError> {
340 Self::validate(&country, ®ion)?;
341 Ok(Self { country, region })
342 }
343
344 fn validate(country: &str, region: &Option<String>) -> Result<(), InputValidationError> {
346 let country_info = rust_iso3166::from_alpha2(country)
347 .ok_or_else(|| InputValidationError::InvalidCountryCode(country.to_string()))?;
348
349 debug!("Found country: {}", country_info.name);
350
351 if let Some(region_code) = region {
352 let _ = country_info.subdivisions()
353 .ok_or_else(|| InputValidationError::UnexpectedRegionCode(region_code.clone()))?;
354
355 let region_info = rust_iso3166::iso3166_2::from_code(region_code)
356 .ok_or_else(|| InputValidationError::InvalidRegionCode(region_code.clone()))?;
357
358 debug!("Found region: {}", region_info.name);
359 }
360
361 Ok(())
362 }
363}
364
365#[derive(Debug, Clone)]
367pub struct TaxScenario {
368 pub source_region: Region,
370 pub destination_region: Region,
372 pub transaction_type: TransactionType,
374 pub trade_agreement_override: Option<TradeAgreementOverride>,
378 pub is_digital_product_or_service: bool,
380 pub has_resale_certificate: bool,
382 pub ignore_threshold: bool,
384 pub vat_rate: Option<VatRate>,
386}
387
388#[derive(Debug, Serialize, Deserialize)]
390pub struct TaxRate {
391 pub rate: f64,
393 pub tax_type: TaxType,
395 pub compound: bool,
397}