1use chrono::{DateTime, NaiveDate, Utc};
11use rust_decimal::{Decimal, RoundingStrategy};
12use serde::{Deserialize, Serialize};
13use stateset_primitives::{CurrencyCode, ProductId};
14use strum::{Display, EnumString};
15use uuid::Uuid;
16
17#[derive(
23 Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, Serialize, Deserialize, Default,
24)]
25#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
26#[serde(rename_all = "snake_case")]
27#[non_exhaustive]
28pub enum TaxType {
29 #[default]
31 SalesTax,
32 Vat,
34 Gst,
36 Hst,
38 Pst,
40 Qst,
42 ConsumptionTax,
44 Custom,
46}
47
48impl TaxType {
49 #[must_use]
51 pub const fn as_str(&self) -> &'static str {
52 match self {
53 Self::SalesTax => "sales_tax",
54 Self::Vat => "vat",
55 Self::Gst => "gst",
56 Self::Hst => "hst",
57 Self::Pst => "pst",
58 Self::Qst => "qst",
59 Self::ConsumptionTax => "consumption_tax",
60 Self::Custom => "custom",
61 }
62 }
63}
64
65#[derive(
67 Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
68)]
69#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
70#[serde(rename_all = "snake_case")]
71#[non_exhaustive]
72pub enum TaxCalculationMethod {
73 #[default]
75 Exclusive,
76 Inclusive,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
82#[serde(rename_all = "snake_case")]
83#[non_exhaustive]
84pub enum TaxCompoundMethod {
85 #[default]
87 Combined,
88 Compound,
90 Separate,
92}
93
94impl std::fmt::Display for TaxCompoundMethod {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self {
97 Self::Combined => f.write_str("combined"),
98 Self::Compound => f.write_str("compound"),
99 Self::Separate => f.write_str("separate"),
100 }
101 }
102}
103
104impl std::str::FromStr for TaxCompoundMethod {
105 type Err = String;
106
107 fn from_str(s: &str) -> Result<Self, Self::Err> {
108 match s.trim().to_ascii_lowercase().as_str() {
109 "combined" => Ok(Self::Combined),
110 "compound" => Ok(Self::Compound),
111 "separate" => Ok(Self::Separate),
112 _ => Err(format!("Unknown tax compound method: {s}")),
113 }
114 }
115}
116
117#[derive(
119 Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize, Default,
120)]
121#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
122#[serde(rename_all = "snake_case")]
123#[non_exhaustive]
124pub enum ProductTaxCategory {
125 #[default]
127 Standard,
128 Reduced,
130 SuperReduced,
132 ZeroRated,
134 Exempt,
136 Digital,
138 Clothing,
140 Food,
142 PreparedFood,
144 Medical,
146 Educational,
148 Luxury,
150}
151
152impl ProductTaxCategory {
153 #[must_use]
155 pub const fn as_str(&self) -> &'static str {
156 match self {
157 Self::Standard => "standard",
158 Self::Reduced => "reduced",
159 Self::SuperReduced => "super_reduced",
160 Self::ZeroRated => "zero_rated",
161 Self::Exempt => "exempt",
162 Self::Digital => "digital",
163 Self::Clothing => "clothing",
164 Self::Food => "food",
165 Self::PreparedFood => "prepared_food",
166 Self::Medical => "medical",
167 Self::Educational => "educational",
168 Self::Luxury => "luxury",
169 }
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(rename_all = "snake_case")]
176#[non_exhaustive]
177pub enum ExemptionType {
178 Resale,
180 NonProfit,
182 Government,
184 Educational,
186 Religious,
188 Medical,
190 Manufacturing,
192 Agricultural,
194 Export,
196 Diplomatic,
198 Other,
200}
201
202impl std::fmt::Display for ExemptionType {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 match self {
205 Self::Resale => f.write_str("resale"),
206 Self::NonProfit => f.write_str("non_profit"),
207 Self::Government => f.write_str("government"),
208 Self::Educational => f.write_str("educational"),
209 Self::Religious => f.write_str("religious"),
210 Self::Medical => f.write_str("medical"),
211 Self::Manufacturing => f.write_str("manufacturing"),
212 Self::Agricultural => f.write_str("agricultural"),
213 Self::Export => f.write_str("export"),
214 Self::Diplomatic => f.write_str("diplomatic"),
215 Self::Other => f.write_str("other"),
216 }
217 }
218}
219
220impl std::str::FromStr for ExemptionType {
221 type Err = String;
222
223 fn from_str(s: &str) -> Result<Self, Self::Err> {
224 match s.trim().to_ascii_lowercase().as_str() {
225 "resale" => Ok(Self::Resale),
226 "non_profit" | "nonprofit" | "non-profit" => Ok(Self::NonProfit),
227 "government" => Ok(Self::Government),
228 "educational" => Ok(Self::Educational),
229 "religious" => Ok(Self::Religious),
230 "medical" => Ok(Self::Medical),
231 "manufacturing" => Ok(Self::Manufacturing),
232 "agricultural" => Ok(Self::Agricultural),
233 "export" => Ok(Self::Export),
234 "diplomatic" => Ok(Self::Diplomatic),
235 "other" => Ok(Self::Other),
236 _ => Err(format!("Unknown exemption type: {s}")),
237 }
238 }
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct TaxJurisdiction {
248 pub id: Uuid,
249 pub parent_id: Option<Uuid>,
251 pub name: String,
253 pub code: String,
255 pub level: JurisdictionLevel,
257 pub country_code: String,
259 pub state_code: Option<String>,
261 pub county: Option<String>,
263 pub city: Option<String>,
265 pub postal_codes: Vec<String>,
267 pub active: bool,
269 pub created_at: DateTime<Utc>,
270 pub updated_at: DateTime<Utc>,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
275#[serde(rename_all = "snake_case")]
276#[non_exhaustive]
277pub enum JurisdictionLevel {
278 #[default]
279 Country,
280 State,
281 County,
282 City,
283 District,
284 Special,
285}
286
287impl std::fmt::Display for JurisdictionLevel {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 match self {
290 Self::Country => f.write_str("country"),
291 Self::State => f.write_str("state"),
292 Self::County => f.write_str("county"),
293 Self::City => f.write_str("city"),
294 Self::District => f.write_str("district"),
295 Self::Special => f.write_str("special"),
296 }
297 }
298}
299
300impl std::str::FromStr for JurisdictionLevel {
301 type Err = String;
302
303 fn from_str(s: &str) -> Result<Self, Self::Err> {
304 match s.trim().to_ascii_lowercase().as_str() {
305 "country" => Ok(Self::Country),
306 "state" => Ok(Self::State),
307 "county" => Ok(Self::County),
308 "city" => Ok(Self::City),
309 "district" => Ok(Self::District),
310 "special" => Ok(Self::Special),
311 _ => Err(format!("Unknown jurisdiction level: {s}")),
312 }
313 }
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct TaxRate {
319 pub id: Uuid,
320 pub jurisdiction_id: Uuid,
322 pub tax_type: TaxType,
324 pub product_category: ProductTaxCategory,
326 pub rate: Decimal,
328 pub name: String,
330 pub description: Option<String>,
332 pub is_compound: bool,
334 pub priority: i32,
336 pub threshold_min: Option<Decimal>,
338 pub threshold_max: Option<Decimal>,
340 pub fixed_amount: Option<Decimal>,
342 pub effective_from: NaiveDate,
344 pub effective_to: Option<NaiveDate>,
346 pub active: bool,
348 pub created_at: DateTime<Utc>,
349 pub updated_at: DateTime<Utc>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct TaxExemption {
355 pub id: Uuid,
356 pub customer_id: Uuid,
358 pub exemption_type: ExemptionType,
360 pub certificate_number: Option<String>,
362 pub issuing_authority: Option<String>,
364 pub jurisdiction_ids: Vec<Uuid>,
366 pub exempt_categories: Vec<ProductTaxCategory>,
368 pub effective_from: NaiveDate,
370 pub expires_at: Option<NaiveDate>,
372 pub verified: bool,
374 pub verified_at: Option<DateTime<Utc>>,
376 pub notes: Option<String>,
378 pub active: bool,
380 pub created_at: DateTime<Utc>,
381 pub updated_at: DateTime<Utc>,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize, Default)]
390pub struct TaxCalculationRequest {
391 pub line_items: Vec<TaxLineItem>,
393 pub shipping_address: TaxAddress,
395 pub billing_address: Option<TaxAddress>,
397 pub customer_id: Option<Uuid>,
399 pub shipping_amount: Option<Decimal>,
401 #[serde(default = "default_currency")]
403 pub currency: CurrencyCode,
404 pub transaction_date: Option<NaiveDate>,
406 pub prices_include_tax: bool,
408}
409
410const fn default_currency() -> CurrencyCode {
411 CurrencyCode::USD
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct TaxLineItem {
417 pub id: String,
419 pub sku: Option<String>,
421 pub product_id: Option<ProductId>,
423 pub quantity: Decimal,
425 pub unit_price: Decimal,
427 pub discount_amount: Decimal,
429 pub tax_category: ProductTaxCategory,
431 pub tax_code: Option<String>,
433 pub description: Option<String>,
435}
436
437impl Default for TaxLineItem {
438 fn default() -> Self {
439 Self {
440 id: String::new(),
441 sku: None,
442 product_id: None,
443 quantity: Decimal::ONE,
444 unit_price: Decimal::ZERO,
445 discount_amount: Decimal::ZERO,
446 tax_category: ProductTaxCategory::Standard,
447 tax_code: None,
448 description: None,
449 }
450 }
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize, Default)]
455pub struct TaxAddress {
456 pub line1: Option<String>,
458 pub line2: Option<String>,
460 pub city: Option<String>,
462 pub state: Option<String>,
464 pub postal_code: Option<String>,
466 pub country: String,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct TaxCalculationResult {
473 pub id: Uuid,
475 pub total_tax: Decimal,
477 pub subtotal: Decimal,
479 pub total: Decimal,
481 pub shipping_tax: Decimal,
483 pub tax_breakdown: Vec<TaxBreakdown>,
485 pub line_item_taxes: Vec<LineItemTax>,
487 pub exemptions_applied: bool,
489 pub exemption_details: Option<ExemptionDetails>,
491 pub jurisdictions: Vec<JurisdictionSummary>,
493 pub calculated_at: DateTime<Utc>,
495 pub is_estimate: bool,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize)]
501pub struct TaxBreakdown {
502 pub jurisdiction_id: Uuid,
504 pub jurisdiction_name: String,
506 pub tax_type: TaxType,
508 pub rate_name: String,
510 pub rate: Decimal,
512 pub taxable_amount: Decimal,
514 pub tax_amount: Decimal,
516 pub is_compound: bool,
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct LineItemTax {
523 pub line_item_id: String,
525 pub taxable_amount: Decimal,
527 pub tax_amount: Decimal,
529 pub effective_rate: Decimal,
531 pub is_exempt: bool,
533 pub exemption_reason: Option<String>,
535 pub tax_details: Vec<TaxDetail>,
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct TaxDetail {
542 pub tax_type: TaxType,
543 pub jurisdiction_name: String,
544 pub rate: Decimal,
545 pub amount: Decimal,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct ExemptionDetails {
551 pub exemption_id: Uuid,
552 pub exemption_type: ExemptionType,
553 pub certificate_number: Option<String>,
554 pub amount_exempt: Decimal,
555 pub tax_saved: Decimal,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct JurisdictionSummary {
561 pub id: Uuid,
562 pub name: String,
563 pub code: String,
564 pub level: JurisdictionLevel,
565 pub total_rate: Decimal,
566 pub total_tax: Decimal,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize)]
575pub struct TaxSettings {
576 pub id: Uuid,
577 pub enabled: bool,
579 pub calculation_method: TaxCalculationMethod,
581 pub compound_method: TaxCompoundMethod,
583 pub tax_shipping: bool,
585 pub tax_handling: bool,
587 pub tax_gift_wrap: bool,
589 pub origin_address: Option<TaxAddress>,
591 pub default_product_category: ProductTaxCategory,
593 pub rounding_mode: String,
597 pub decimal_places: i32,
599 pub validate_addresses: bool,
601 pub tax_provider: Option<String>,
603 pub provider_credentials: Option<String>,
605 pub created_at: DateTime<Utc>,
606 pub updated_at: DateTime<Utc>,
607}
608
609impl Default for TaxSettings {
610 fn default() -> Self {
611 Self {
612 id: Uuid::new_v4(),
613 enabled: true,
614 calculation_method: TaxCalculationMethod::Exclusive,
615 compound_method: TaxCompoundMethod::Combined,
616 tax_shipping: true,
617 tax_handling: true,
618 tax_gift_wrap: true,
619 origin_address: None,
620 default_product_category: ProductTaxCategory::Standard,
621 rounding_mode: "half_up".to_string(),
622 decimal_places: 2,
623 validate_addresses: false,
624 tax_provider: None,
625 provider_credentials: None,
626 created_at: Utc::now(),
627 updated_at: Utc::now(),
628 }
629 }
630}
631
632impl TaxSettings {
633 #[must_use]
642 pub fn rounding_strategy(&self) -> RoundingStrategy {
643 match self.rounding_mode.trim().to_ascii_lowercase().as_str() {
644 "half_even" | "half_to_even" | "bankers" | "banker" => {
645 RoundingStrategy::MidpointNearestEven
646 }
647 "half_down" => RoundingStrategy::MidpointTowardZero,
648 "up" | "away_from_zero" => RoundingStrategy::AwayFromZero,
649 "down" | "truncate" | "toward_zero" => RoundingStrategy::ToZero,
650 "ceil" | "ceiling" => RoundingStrategy::ToPositiveInfinity,
651 "floor" => RoundingStrategy::ToNegativeInfinity,
652 _ => RoundingStrategy::MidpointAwayFromZero,
654 }
655 }
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize, Default)]
664pub struct CreateTaxJurisdiction {
665 pub parent_id: Option<Uuid>,
666 pub name: String,
667 pub code: String,
668 pub level: JurisdictionLevel,
669 pub country_code: String,
670 pub state_code: Option<String>,
671 pub county: Option<String>,
672 pub city: Option<String>,
673 pub postal_codes: Vec<String>,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize)]
678pub struct CreateTaxRate {
679 pub jurisdiction_id: Uuid,
680 pub tax_type: TaxType,
681 pub product_category: ProductTaxCategory,
682 pub rate: Decimal,
683 pub name: String,
684 pub description: Option<String>,
685 pub is_compound: bool,
686 pub priority: i32,
687 pub threshold_min: Option<Decimal>,
688 pub threshold_max: Option<Decimal>,
689 pub fixed_amount: Option<Decimal>,
690 pub effective_from: NaiveDate,
691 pub effective_to: Option<NaiveDate>,
692}
693
694impl Default for CreateTaxRate {
695 fn default() -> Self {
696 Self {
697 jurisdiction_id: Uuid::nil(),
698 tax_type: TaxType::SalesTax,
699 product_category: ProductTaxCategory::Standard,
700 rate: Decimal::ZERO,
701 name: String::new(),
702 description: None,
703 is_compound: false,
704 priority: 0,
705 threshold_min: None,
706 threshold_max: None,
707 fixed_amount: None,
708 effective_from: Utc::now().date_naive(),
709 effective_to: None,
710 }
711 }
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
716pub struct CreateTaxExemption {
717 pub customer_id: Uuid,
718 pub exemption_type: ExemptionType,
719 pub certificate_number: Option<String>,
720 pub issuing_authority: Option<String>,
721 pub jurisdiction_ids: Vec<Uuid>,
722 pub exempt_categories: Vec<ProductTaxCategory>,
723 pub effective_from: NaiveDate,
724 pub expires_at: Option<NaiveDate>,
725 pub notes: Option<String>,
726}
727
728#[derive(Debug, Clone, Default, Serialize, Deserialize)]
730pub struct TaxRateFilter {
731 pub jurisdiction_id: Option<Uuid>,
732 pub tax_type: Option<TaxType>,
733 pub product_category: Option<ProductTaxCategory>,
734 pub active_only: bool,
735 pub effective_date: Option<NaiveDate>,
736 pub limit: Option<u32>,
738 pub offset: Option<u32>,
740}
741
742#[derive(Debug, Clone, Default, Serialize, Deserialize)]
744pub struct TaxJurisdictionFilter {
745 pub country_code: Option<String>,
746 pub state_code: Option<String>,
747 pub level: Option<JurisdictionLevel>,
748 pub active_only: bool,
749 pub limit: Option<u32>,
751 pub offset: Option<u32>,
753}
754
755#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct UsStateTaxInfo {
762 pub state_code: String,
763 pub state_name: String,
764 pub state_rate: Decimal,
765 pub has_local_taxes: bool,
766 pub origin_based: bool,
767 pub tax_shipping: bool,
768 pub tax_clothing: bool,
769 pub tax_food: bool,
770 pub tax_digital: bool,
771}
772
773#[must_use]
775pub fn get_us_state_tax_info(state_code: &str) -> Option<UsStateTaxInfo> {
776 match state_code.to_uppercase().as_str() {
777 "AL" => Some(UsStateTaxInfo {
778 state_code: "AL".into(),
779 state_name: "Alabama".into(),
780 state_rate: Decimal::new(4, 2), has_local_taxes: true,
782 origin_based: false,
783 tax_shipping: true,
784 tax_clothing: true,
785 tax_food: true,
786 tax_digital: true,
787 }),
788 "AK" => Some(UsStateTaxInfo {
789 state_code: "AK".into(),
790 state_name: "Alaska".into(),
791 state_rate: Decimal::ZERO, has_local_taxes: true,
793 origin_based: false,
794 tax_shipping: false,
795 tax_clothing: false,
796 tax_food: false,
797 tax_digital: false,
798 }),
799 "AZ" => Some(UsStateTaxInfo {
800 state_code: "AZ".into(),
801 state_name: "Arizona".into(),
802 state_rate: Decimal::new(56, 3), has_local_taxes: true,
804 origin_based: true,
805 tax_shipping: true,
806 tax_clothing: true,
807 tax_food: false,
808 tax_digital: true,
809 }),
810 "CA" => Some(UsStateTaxInfo {
811 state_code: "CA".into(),
812 state_name: "California".into(),
813 state_rate: Decimal::new(725, 4), has_local_taxes: true,
815 origin_based: true,
816 tax_shipping: false,
817 tax_clothing: true,
818 tax_food: false,
819 tax_digital: false,
820 }),
821 "CO" => Some(UsStateTaxInfo {
822 state_code: "CO".into(),
823 state_name: "Colorado".into(),
824 state_rate: Decimal::new(29, 3), has_local_taxes: true,
826 origin_based: false,
827 tax_shipping: true,
828 tax_clothing: true,
829 tax_food: false,
830 tax_digital: true,
831 }),
832 "DE" => Some(UsStateTaxInfo {
833 state_code: "DE".into(),
834 state_name: "Delaware".into(),
835 state_rate: Decimal::ZERO, has_local_taxes: false,
837 origin_based: false,
838 tax_shipping: false,
839 tax_clothing: false,
840 tax_food: false,
841 tax_digital: false,
842 }),
843 "FL" => Some(UsStateTaxInfo {
844 state_code: "FL".into(),
845 state_name: "Florida".into(),
846 state_rate: Decimal::new(6, 2), has_local_taxes: true,
848 origin_based: false,
849 tax_shipping: true,
850 tax_clothing: true,
851 tax_food: false,
852 tax_digital: true,
853 }),
854 "MT" => Some(UsStateTaxInfo {
855 state_code: "MT".into(),
856 state_name: "Montana".into(),
857 state_rate: Decimal::ZERO, has_local_taxes: false,
859 origin_based: false,
860 tax_shipping: false,
861 tax_clothing: false,
862 tax_food: false,
863 tax_digital: false,
864 }),
865 "NH" => Some(UsStateTaxInfo {
866 state_code: "NH".into(),
867 state_name: "New Hampshire".into(),
868 state_rate: Decimal::ZERO, has_local_taxes: false,
870 origin_based: false,
871 tax_shipping: false,
872 tax_clothing: false,
873 tax_food: false,
874 tax_digital: false,
875 }),
876 "NY" => Some(UsStateTaxInfo {
877 state_code: "NY".into(),
878 state_name: "New York".into(),
879 state_rate: Decimal::new(4, 2), has_local_taxes: true,
881 origin_based: false,
882 tax_shipping: true,
883 tax_clothing: false, tax_food: false,
885 tax_digital: true,
886 }),
887 "OR" => Some(UsStateTaxInfo {
888 state_code: "OR".into(),
889 state_name: "Oregon".into(),
890 state_rate: Decimal::ZERO, has_local_taxes: false,
892 origin_based: false,
893 tax_shipping: false,
894 tax_clothing: false,
895 tax_food: false,
896 tax_digital: false,
897 }),
898 "TX" => Some(UsStateTaxInfo {
899 state_code: "TX".into(),
900 state_name: "Texas".into(),
901 state_rate: Decimal::new(625, 4), has_local_taxes: true,
903 origin_based: true,
904 tax_shipping: true,
905 tax_clothing: true,
906 tax_food: false,
907 tax_digital: true,
908 }),
909 "WA" => Some(UsStateTaxInfo {
910 state_code: "WA".into(),
911 state_name: "Washington".into(),
912 state_rate: Decimal::new(65, 3), has_local_taxes: true,
914 origin_based: false,
915 tax_shipping: true,
916 tax_clothing: true,
917 tax_food: false,
918 tax_digital: true,
919 }),
920 _ => None,
921 }
922}
923
924#[derive(Debug, Clone, Serialize, Deserialize)]
930pub struct EuVatInfo {
931 pub country_code: String,
932 pub country_name: String,
933 pub standard_rate: Decimal,
934 pub reduced_rate: Option<Decimal>,
935 pub super_reduced_rate: Option<Decimal>,
936 pub parking_rate: Option<Decimal>,
937}
938
939#[must_use]
941pub fn get_eu_vat_info(country_code: &str) -> Option<EuVatInfo> {
942 match country_code.to_uppercase().as_str() {
943 "AT" => Some(EuVatInfo {
944 country_code: "AT".into(),
945 country_name: "Austria".into(),
946 standard_rate: Decimal::new(20, 2),
947 reduced_rate: Some(Decimal::new(10, 2)),
948 super_reduced_rate: None,
949 parking_rate: Some(Decimal::new(13, 2)),
950 }),
951 "BE" => Some(EuVatInfo {
952 country_code: "BE".into(),
953 country_name: "Belgium".into(),
954 standard_rate: Decimal::new(21, 2),
955 reduced_rate: Some(Decimal::new(12, 2)),
956 super_reduced_rate: Some(Decimal::new(6, 2)),
957 parking_rate: Some(Decimal::new(12, 2)),
958 }),
959 "DE" => Some(EuVatInfo {
960 country_code: "DE".into(),
961 country_name: "Germany".into(),
962 standard_rate: Decimal::new(19, 2),
963 reduced_rate: Some(Decimal::new(7, 2)),
964 super_reduced_rate: None,
965 parking_rate: None,
966 }),
967 "ES" => Some(EuVatInfo {
968 country_code: "ES".into(),
969 country_name: "Spain".into(),
970 standard_rate: Decimal::new(21, 2),
971 reduced_rate: Some(Decimal::new(10, 2)),
972 super_reduced_rate: Some(Decimal::new(4, 2)),
973 parking_rate: None,
974 }),
975 "FR" => Some(EuVatInfo {
976 country_code: "FR".into(),
977 country_name: "France".into(),
978 standard_rate: Decimal::new(20, 2),
979 reduced_rate: Some(Decimal::new(10, 2)),
980 super_reduced_rate: Some(Decimal::new(55, 3)), parking_rate: None,
982 }),
983 "GB" => Some(EuVatInfo {
984 country_code: "GB".into(),
985 country_name: "United Kingdom".into(),
986 standard_rate: Decimal::new(20, 2),
987 reduced_rate: Some(Decimal::new(5, 2)),
988 super_reduced_rate: None,
989 parking_rate: None,
990 }),
991 "IE" => Some(EuVatInfo {
992 country_code: "IE".into(),
993 country_name: "Ireland".into(),
994 standard_rate: Decimal::new(23, 2),
995 reduced_rate: Some(Decimal::new(135, 3)), super_reduced_rate: Some(Decimal::new(48, 3)), parking_rate: Some(Decimal::new(135, 3)),
998 }),
999 "IT" => Some(EuVatInfo {
1000 country_code: "IT".into(),
1001 country_name: "Italy".into(),
1002 standard_rate: Decimal::new(22, 2),
1003 reduced_rate: Some(Decimal::new(10, 2)),
1004 super_reduced_rate: Some(Decimal::new(4, 2)),
1005 parking_rate: None,
1006 }),
1007 "NL" => Some(EuVatInfo {
1008 country_code: "NL".into(),
1009 country_name: "Netherlands".into(),
1010 standard_rate: Decimal::new(21, 2),
1011 reduced_rate: Some(Decimal::new(9, 2)),
1012 super_reduced_rate: None,
1013 parking_rate: None,
1014 }),
1015 "SE" => Some(EuVatInfo {
1016 country_code: "SE".into(),
1017 country_name: "Sweden".into(),
1018 standard_rate: Decimal::new(25, 2),
1019 reduced_rate: Some(Decimal::new(12, 2)),
1020 super_reduced_rate: Some(Decimal::new(6, 2)),
1021 parking_rate: None,
1022 }),
1023 _ => None,
1024 }
1025}
1026
1027pub const EU_MEMBER_STATES: &[&str] = &[
1029 "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV",
1030 "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE",
1031];
1032
1033#[must_use]
1035pub fn is_eu_member(country_code: &str) -> bool {
1036 EU_MEMBER_STATES.contains(&country_code.to_uppercase().as_str())
1037}
1038
1039#[derive(Debug, Clone, Serialize, Deserialize)]
1045pub struct CanadianTaxInfo {
1046 pub province_code: String,
1047 pub province_name: String,
1048 pub gst_rate: Decimal,
1049 pub pst_rate: Option<Decimal>,
1050 pub hst_rate: Option<Decimal>,
1051 pub qst_rate: Option<Decimal>,
1052 pub total_rate: Decimal,
1053}
1054
1055#[must_use]
1057pub fn get_canadian_tax_info(province_code: &str) -> Option<CanadianTaxInfo> {
1058 let gst = Decimal::new(5, 2); match province_code.to_uppercase().as_str() {
1061 "AB" => Some(CanadianTaxInfo {
1062 province_code: "AB".into(),
1063 province_name: "Alberta".into(),
1064 gst_rate: gst,
1065 pst_rate: None,
1066 hst_rate: None,
1067 qst_rate: None,
1068 total_rate: gst,
1069 }),
1070 "BC" => Some(CanadianTaxInfo {
1071 province_code: "BC".into(),
1072 province_name: "British Columbia".into(),
1073 gst_rate: gst,
1074 pst_rate: Some(Decimal::new(7, 2)),
1075 hst_rate: None,
1076 qst_rate: None,
1077 total_rate: Decimal::new(12, 2),
1078 }),
1079 "ON" => Some(CanadianTaxInfo {
1080 province_code: "ON".into(),
1081 province_name: "Ontario".into(),
1082 gst_rate: Decimal::ZERO, pst_rate: None,
1084 hst_rate: Some(Decimal::new(13, 2)),
1085 qst_rate: None,
1086 total_rate: Decimal::new(13, 2),
1087 }),
1088 "QC" => Some(CanadianTaxInfo {
1089 province_code: "QC".into(),
1090 province_name: "Quebec".into(),
1091 gst_rate: gst,
1092 pst_rate: None,
1093 hst_rate: None,
1094 qst_rate: Some(Decimal::new(9975, 4)), total_rate: Decimal::new(14975, 4),
1096 }),
1097 "SK" => Some(CanadianTaxInfo {
1098 province_code: "SK".into(),
1099 province_name: "Saskatchewan".into(),
1100 gst_rate: gst,
1101 pst_rate: Some(Decimal::new(6, 2)),
1102 hst_rate: None,
1103 qst_rate: None,
1104 total_rate: Decimal::new(11, 2),
1105 }),
1106 "MB" => Some(CanadianTaxInfo {
1107 province_code: "MB".into(),
1108 province_name: "Manitoba".into(),
1109 gst_rate: gst,
1110 pst_rate: Some(Decimal::new(7, 2)),
1111 hst_rate: None,
1112 qst_rate: None,
1113 total_rate: Decimal::new(12, 2),
1114 }),
1115 "NS" => Some(CanadianTaxInfo {
1116 province_code: "NS".into(),
1117 province_name: "Nova Scotia".into(),
1118 gst_rate: Decimal::ZERO,
1119 pst_rate: None,
1120 hst_rate: Some(Decimal::new(15, 2)),
1121 qst_rate: None,
1122 total_rate: Decimal::new(15, 2),
1123 }),
1124 "NB" => Some(CanadianTaxInfo {
1125 province_code: "NB".into(),
1126 province_name: "New Brunswick".into(),
1127 gst_rate: Decimal::ZERO,
1128 pst_rate: None,
1129 hst_rate: Some(Decimal::new(15, 2)),
1130 qst_rate: None,
1131 total_rate: Decimal::new(15, 2),
1132 }),
1133 _ => None,
1134 }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use super::*;
1140 use std::str::FromStr;
1141
1142 #[test]
1143 fn tax_type_from_str() {
1144 assert_eq!(TaxType::from_str("sales_tax").unwrap(), TaxType::SalesTax);
1145 assert!(TaxType::from_str("unknown").is_err());
1146 }
1147
1148 #[test]
1149 fn tax_calculation_method_from_str() {
1150 assert_eq!(
1151 TaxCalculationMethod::from_str("inclusive").unwrap(),
1152 TaxCalculationMethod::Inclusive
1153 );
1154 assert!(TaxCalculationMethod::from_str("other").is_err());
1155 }
1156
1157 #[test]
1158 fn tax_compound_method_from_str() {
1159 assert_eq!(TaxCompoundMethod::from_str("combined").unwrap(), TaxCompoundMethod::Combined);
1160 assert!(TaxCompoundMethod::from_str("other").is_err());
1161 }
1162
1163 #[test]
1164 fn product_tax_category_from_str() {
1165 assert_eq!(
1166 ProductTaxCategory::from_str("super_reduced").unwrap(),
1167 ProductTaxCategory::SuperReduced
1168 );
1169 assert!(ProductTaxCategory::from_str("other").is_err());
1170 }
1171
1172 #[test]
1173 fn exemption_type_from_str() {
1174 assert_eq!(ExemptionType::from_str("non_profit").unwrap(), ExemptionType::NonProfit);
1175 assert!(ExemptionType::from_str("unknown").is_err());
1176 }
1177
1178 #[test]
1179 fn jurisdiction_level_from_str() {
1180 assert_eq!(JurisdictionLevel::from_str("state").unwrap(), JurisdictionLevel::State);
1181 assert!(JurisdictionLevel::from_str("unknown").is_err());
1182 }
1183
1184 #[test]
1185 fn tax_settings_rounding_strategy_maps_modes() {
1186 use rust_decimal::RoundingStrategy;
1187 let strat = |mode: &str| {
1188 let s = TaxSettings { rounding_mode: mode.to_string(), ..Default::default() };
1189 s.rounding_strategy()
1190 };
1191 assert_eq!(strat("half_up"), RoundingStrategy::MidpointAwayFromZero);
1192 assert_eq!(strat("half_even"), RoundingStrategy::MidpointNearestEven);
1193 assert_eq!(strat("bankers"), RoundingStrategy::MidpointNearestEven);
1194 assert_eq!(strat("half_down"), RoundingStrategy::MidpointTowardZero);
1195 assert_eq!(strat("up"), RoundingStrategy::AwayFromZero);
1196 assert_eq!(strat("down"), RoundingStrategy::ToZero);
1197 assert_eq!(strat("truncate"), RoundingStrategy::ToZero);
1198 assert_eq!(strat("ceil"), RoundingStrategy::ToPositiveInfinity);
1199 assert_eq!(strat("floor"), RoundingStrategy::ToNegativeInfinity);
1200 assert_eq!(strat(" Half_Even "), RoundingStrategy::MidpointNearestEven);
1202 assert_eq!(strat("wat"), RoundingStrategy::MidpointAwayFromZero);
1204 assert_eq!(
1205 TaxSettings::default().rounding_strategy(),
1206 RoundingStrategy::MidpointAwayFromZero
1207 );
1208 }
1209}