1use chrono::NaiveDate;
8use datasynth_core::utils::seeded_rng;
9use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11use rust_decimal::Decimal;
12use rust_decimal_macros::dec;
13
14use datasynth_config::schema::TaxConfig;
15use datasynth_core::models::{JurisdictionType, TaxCode, TaxJurisdiction, TaxType};
16
17const US_STATE_RATES: &[(&str, &str, &str)] = &[
23 ("CA", "California", "0.0725"),
24 ("NY", "New York", "0.08"),
25 ("TX", "Texas", "0.0625"),
26 ("FL", "Florida", "0.06"),
27 ("WA", "Washington", "0.065"),
28 ("IL", "Illinois", "0.0625"),
29 ("PA", "Pennsylvania", "0.06"),
30 ("OH", "Ohio", "0.0575"),
31 ("NJ", "New Jersey", "0.06625"),
32 ("GA", "Georgia", "0.04"),
33];
34
35const COUNTRY_RATES: &[(&str, &str, &str, &str, Option<&str>)] = &[
39 ("DE", "Germany", "vat", "0.19", Some("0.07")),
40 ("GB", "United Kingdom", "vat", "0.20", Some("0.05")),
41 ("FR", "France", "vat", "0.20", Some("0.055")),
42 ("IT", "Italy", "vat", "0.22", Some("0.10")),
43 ("ES", "Spain", "vat", "0.21", Some("0.10")),
44 ("NL", "Netherlands", "vat", "0.21", Some("0.09")),
45 ("SG", "Singapore", "gst", "0.09", None),
46 ("AU", "Australia", "gst", "0.10", None),
47 ("JP", "Japan", "gst", "0.10", Some("0.08")),
48 ("IN", "India", "gst", "0.18", Some("0.05")),
49 ("BR", "Brazil", "vat", "0.17", None),
50 ("CA", "Canada", "gst", "0.05", None),
51];
52
53const INDIA_STATES: &[(&str, &str)] = &[
55 ("MH", "Maharashtra"),
56 ("DL", "Delhi"),
57 ("KA", "Karnataka"),
58 ("TN", "Tamil Nadu"),
59 ("GJ", "Gujarat"),
60 ("UP", "Uttar Pradesh"),
61 ("WB", "West Bengal"),
62 ("RJ", "Rajasthan"),
63 ("TG", "Telangana"),
64 ("KL", "Kerala"),
65];
66
67const GERMANY_STATES: &[(&str, &str)] = &[
69 ("BW", "Baden-Wuerttemberg"),
70 ("BY", "Bavaria"),
71 ("BE", "Berlin"),
72 ("BB", "Brandenburg"),
73 ("HB", "Bremen"),
74 ("HH", "Hamburg"),
75 ("HE", "Hesse"),
76 ("MV", "Mecklenburg-Vorpommern"),
77 ("NI", "Lower Saxony"),
78 ("NW", "North Rhine-Westphalia"),
79 ("RP", "Rhineland-Palatinate"),
80 ("SL", "Saarland"),
81 ("SN", "Saxony"),
82 ("ST", "Saxony-Anhalt"),
83 ("SH", "Schleswig-Holstein"),
84 ("TH", "Thuringia"),
85];
86
87const CANADA_PROVINCES: &[(&str, &str, &str)] = &[
89 ("ON", "Ontario", "0.13"),
90 ("BC", "British Columbia", "0.12"),
91 ("QC", "Quebec", "0.14975"),
92 ("AB", "Alberta", "0.05"),
93 ("NS", "Nova Scotia", "0.15"),
94 ("NB", "New Brunswick", "0.15"),
95 ("MB", "Manitoba", "0.12"),
96 ("SK", "Saskatchewan", "0.11"),
97 ("NL", "Newfoundland and Labrador", "0.15"),
98 ("PE", "Prince Edward Island", "0.15"),
99];
100
101const INDIA_GST_SLABS: &[(&str, &str)] = &[
103 ("0.05", "GST 5% slab"),
104 ("0.12", "GST 12% slab"),
105 ("0.18", "GST 18% slab"),
106 ("0.28", "GST 28% slab"),
107];
108
109fn default_effective_date() -> NaiveDate {
111 NaiveDate::from_ymd_opt(2020, 1, 1).expect("valid date")
112}
113
114pub struct TaxCodeGenerator {
136 rng: ChaCha8Rng,
137 config: TaxConfig,
138}
139
140impl TaxCodeGenerator {
141 pub fn new(seed: u64) -> Self {
145 Self {
146 rng: seeded_rng(seed, 0),
147 config: TaxConfig::default(),
148 }
149 }
150
151 pub fn with_config(seed: u64, config: TaxConfig) -> Self {
153 Self {
154 rng: seeded_rng(seed, 0),
155 config,
156 }
157 }
158
159 pub fn generate(&mut self) -> (Vec<TaxJurisdiction>, Vec<TaxCode>) {
163 let countries = self.resolve_countries();
164 let include_subnational = self.config.jurisdictions.include_subnational;
165
166 let mut jurisdictions = Vec::new();
167 let mut codes = Vec::new();
168 let mut code_counter: u32 = 1;
169
170 for country in &countries {
171 let cc = country.as_str();
172 match cc {
173 "US" => self.generate_us(
174 include_subnational,
175 &mut jurisdictions,
176 &mut codes,
177 &mut code_counter,
178 ),
179 _ => self.generate_country(
180 cc,
181 include_subnational,
182 &mut jurisdictions,
183 &mut codes,
184 &mut code_counter,
185 ),
186 }
187 }
188
189 (jurisdictions, codes)
190 }
191
192 fn resolve_countries(&self) -> Vec<String> {
198 if self.config.jurisdictions.countries.is_empty() {
199 vec!["US".into(), "DE".into(), "GB".into()]
200 } else {
201 self.config.jurisdictions.countries.clone()
202 }
203 }
204
205 fn generate_us(
207 &mut self,
208 include_subnational: bool,
209 jurisdictions: &mut Vec<TaxJurisdiction>,
210 codes: &mut Vec<TaxCode>,
211 counter: &mut u32,
212 ) {
213 let federal_id = "JUR-US".to_string();
214
215 jurisdictions.push(TaxJurisdiction::new(
217 &federal_id,
218 "United States - Federal",
219 "US",
220 JurisdictionType::Federal,
221 ));
222
223 if !include_subnational {
224 return;
225 }
226
227 let nexus_states = &self.config.sales_tax.nexus_states;
229
230 for &(state_code, state_name, rate_str) in US_STATE_RATES {
231 if !nexus_states.is_empty()
233 && !nexus_states
234 .iter()
235 .any(|s| s.eq_ignore_ascii_case(state_code))
236 {
237 continue;
238 }
239
240 let jur_id = format!("JUR-US-{state_code}");
241
242 jurisdictions.push(
243 TaxJurisdiction::new(&jur_id, state_name, "US", JurisdictionType::State)
244 .with_region_code(state_code)
245 .with_parent_jurisdiction_id(&federal_id),
246 );
247
248 let rate: Decimal = rate_str.parse().expect("valid decimal");
249 let code_id = format!("TC-{counter:04}");
250 let code_mnemonic = format!("ST-{state_code}");
251 let description = format!("{state_name} Sales Tax {}", format_rate_pct(rate));
252
253 codes.push(TaxCode::new(
254 code_id,
255 code_mnemonic,
256 description,
257 TaxType::SalesTax,
258 rate,
259 &jur_id,
260 default_effective_date(),
261 ));
262 *counter += 1;
263 }
264 }
265
266 fn generate_country(
269 &mut self,
270 country_code: &str,
271 include_subnational: bool,
272 jurisdictions: &mut Vec<TaxJurisdiction>,
273 codes: &mut Vec<TaxCode>,
274 counter: &mut u32,
275 ) {
276 let entry = COUNTRY_RATES
278 .iter()
279 .find(|(cc, _, _, _, _)| *cc == country_code);
280
281 let (country_name, tax_type_str, default_std_rate_str, default_reduced_str) = match entry {
282 Some((_, name, tt, std_rate, reduced)) => (*name, *tt, *std_rate, *reduced),
283 None => {
284 return;
287 }
288 };
289
290 let tax_type = match tax_type_str {
291 "gst" => TaxType::Gst,
292 _ => TaxType::Vat,
293 };
294
295 let is_vat_gst = matches!(tax_type, TaxType::Vat | TaxType::Gst);
296
297 let federal_id = format!("JUR-{country_code}");
298
299 jurisdictions.push(
301 TaxJurisdiction::new(
302 &federal_id,
303 format!("{country_name} - Federal"),
304 country_code,
305 JurisdictionType::Federal,
306 )
307 .with_vat_registered(is_vat_gst),
308 );
309
310 let std_rate = self.resolve_standard_rate(country_code, default_std_rate_str);
312 let reduced_rate = self.resolve_reduced_rate(country_code, default_reduced_str);
313
314 let std_code_id = format!("TC-{counter:04}");
316 let std_mnemonic = format!(
317 "{}-STD-{}",
318 if tax_type == TaxType::Gst {
319 "GST"
320 } else {
321 "VAT"
322 },
323 country_code
324 );
325 let std_desc = format!(
326 "{country_name} {} Standard {}",
327 if tax_type == TaxType::Gst {
328 "GST"
329 } else {
330 "VAT"
331 },
332 format_rate_pct(std_rate)
333 );
334
335 let mut std_code = TaxCode::new(
336 std_code_id,
337 std_mnemonic,
338 std_desc,
339 tax_type,
340 std_rate,
341 &federal_id,
342 default_effective_date(),
343 );
344
345 if is_eu_country(country_code) && self.config.vat_gst.reverse_charge {
347 std_code = std_code.with_reverse_charge(true);
348 }
349
350 codes.push(std_code);
351 *counter += 1;
352
353 if let Some(red_rate) = reduced_rate {
355 let red_code_id = format!("TC-{counter:04}");
356 let red_mnemonic = format!(
357 "{}-RED-{}",
358 if tax_type == TaxType::Gst {
359 "GST"
360 } else {
361 "VAT"
362 },
363 country_code
364 );
365 let red_desc = format!(
366 "{country_name} {} Reduced {}",
367 if tax_type == TaxType::Gst {
368 "GST"
369 } else {
370 "VAT"
371 },
372 format_rate_pct(red_rate)
373 );
374
375 codes.push(TaxCode::new(
376 red_code_id,
377 red_mnemonic,
378 red_desc,
379 tax_type,
380 red_rate,
381 &federal_id,
382 default_effective_date(),
383 ));
384 *counter += 1;
385 }
386
387 if country_code == "GB" {
389 let zero_code_id = format!("TC-{counter:04}");
390 codes.push(TaxCode::new(
391 zero_code_id,
392 format!("VAT-ZERO-{country_code}"),
393 format!("{country_name} VAT Zero Rate"),
394 TaxType::Vat,
395 dec!(0),
396 &federal_id,
397 default_effective_date(),
398 ));
399 *counter += 1;
400 }
401
402 let exempt_code_id = format!("TC-{counter:04}");
404 let exempt_mnemonic = format!(
405 "{}-EX-{}",
406 if tax_type == TaxType::Gst {
407 "GST"
408 } else {
409 "VAT"
410 },
411 country_code
412 );
413 codes.push(
414 TaxCode::new(
415 exempt_code_id,
416 exempt_mnemonic,
417 format!("{country_name} Tax Exempt"),
418 tax_type,
419 dec!(0),
420 &federal_id,
421 default_effective_date(),
422 )
423 .with_exempt(true),
424 );
425 *counter += 1;
426
427 if include_subnational {
429 self.generate_subnational(
430 country_code,
431 &federal_id,
432 tax_type,
433 jurisdictions,
434 codes,
435 counter,
436 );
437 }
438 }
439
440 fn generate_subnational(
442 &mut self,
443 country_code: &str,
444 federal_id: &str,
445 _tax_type: TaxType,
446 jurisdictions: &mut Vec<TaxJurisdiction>,
447 codes: &mut Vec<TaxCode>,
448 counter: &mut u32,
449 ) {
450 match country_code {
451 "IN" => {
452 for &(state_code, state_name) in INDIA_STATES {
454 let jur_id = format!("JUR-IN-{state_code}");
455 jurisdictions.push(
456 TaxJurisdiction::new(&jur_id, state_name, "IN", JurisdictionType::State)
457 .with_region_code(state_code)
458 .with_parent_jurisdiction_id(federal_id)
459 .with_vat_registered(true),
460 );
461 }
462
463 for &(rate_str, label) in INDIA_GST_SLABS {
465 let rate: Decimal = rate_str.parse().expect("valid decimal");
466 let code_id = format!("TC-{counter:04}");
467 let pct = format_rate_pct(rate);
468 codes.push(TaxCode::new(
469 code_id,
470 format!("GST-SLAB-{pct}"),
471 label,
472 TaxType::Gst,
473 rate,
474 federal_id,
475 default_effective_date(),
476 ));
477 *counter += 1;
478 }
479 }
480 "DE" => {
481 for &(state_code, state_name) in GERMANY_STATES {
483 let jur_id = format!("JUR-DE-{state_code}");
484 jurisdictions.push(
485 TaxJurisdiction::new(&jur_id, state_name, "DE", JurisdictionType::State)
486 .with_region_code(state_code)
487 .with_parent_jurisdiction_id(federal_id)
488 .with_vat_registered(true),
489 );
490 }
491 }
492 "CA" => {
493 for &(prov_code, prov_name, combined_rate_str) in CANADA_PROVINCES {
495 let jur_id = format!("JUR-CA-{prov_code}");
496 jurisdictions.push(
497 TaxJurisdiction::new(&jur_id, prov_name, "CA", JurisdictionType::State)
498 .with_region_code(prov_code)
499 .with_parent_jurisdiction_id(federal_id)
500 .with_vat_registered(true),
501 );
502
503 let combined_rate: Decimal = combined_rate_str.parse().expect("valid decimal");
504 let code_id = format!("TC-{counter:04}");
505 codes.push(TaxCode::new(
506 code_id,
507 format!("HST-{prov_code}"),
508 format!("{prov_name} HST/GST+PST {}", format_rate_pct(combined_rate)),
509 TaxType::Gst,
510 combined_rate,
511 &jur_id,
512 default_effective_date(),
513 ));
514 *counter += 1;
515 }
516 }
517 _ => {
518 }
520 }
521 }
522
523 pub fn generate_from_country_pack(
542 &mut self,
543 pack: &datasynth_core::CountryPack,
544 company_code: &str,
545 fiscal_year: i32,
546 ) -> (Vec<TaxJurisdiction>, Vec<TaxCode>) {
547 let tax = &pack.tax;
548 let country_code = pack.country_code.as_str();
549 let country_name = if pack.country_name.is_empty() {
550 country_code
551 } else {
552 pack.country_name.as_str()
553 };
554
555 let has_vat = tax.vat.standard_rate > 0.0;
557 let has_cit = tax.corporate_income_tax.standard_rate > 0.0;
558 let has_subnational = !tax.subnational.is_empty();
559
560 if !has_vat && !has_cit && !has_subnational {
561 return (Vec::new(), Vec::new());
562 }
563
564 let effective_date =
565 NaiveDate::from_ymd_opt(fiscal_year, 1, 1).unwrap_or_else(default_effective_date);
566
567 let mut jurisdictions = Vec::new();
568 let mut codes = Vec::new();
569 let mut counter: u32 = 1;
570
571 let federal_id = format!("JUR-{company_code}-{country_code}");
575
576 jurisdictions.push(
577 TaxJurisdiction::new(
578 &federal_id,
579 format!("{country_name} - Federal"),
580 country_code,
581 JurisdictionType::Federal,
582 )
583 .with_vat_registered(has_vat),
584 );
585
586 if has_vat {
590 let std_rate = Decimal::try_from(tax.vat.standard_rate).unwrap_or_else(|_| dec!(0));
591
592 let tax_type = if is_gst_country(country_code) {
595 TaxType::Gst
596 } else {
597 TaxType::Vat
598 };
599
600 let type_label = if tax_type == TaxType::Gst {
601 "GST"
602 } else {
603 "VAT"
604 };
605
606 let std_code_id = format!("TC-{company_code}-{counter:04}");
608 let std_mnemonic = format!("{type_label}-STD-{country_code}");
609 let std_desc = format!(
610 "{country_name} {type_label} Standard {}",
611 format_rate_pct(std_rate)
612 );
613
614 let mut std_code = TaxCode::new(
615 std_code_id,
616 std_mnemonic,
617 std_desc,
618 tax_type,
619 std_rate,
620 &federal_id,
621 effective_date,
622 );
623
624 if tax.vat.reverse_charge_applicable {
625 std_code = std_code.with_reverse_charge(true);
626 }
627
628 codes.push(std_code);
629 counter += 1;
630
631 for reduced in &tax.vat.reduced_rates {
633 if reduced.rate <= 0.0 {
634 continue;
635 }
636 let red_rate = Decimal::try_from(reduced.rate).unwrap_or_else(|_| dec!(0));
637
638 let label_suffix = if reduced.label.is_empty() {
639 format_rate_pct(red_rate)
640 } else {
641 reduced.label.clone()
642 };
643
644 let red_code_id = format!("TC-{company_code}-{counter:04}");
645 let red_mnemonic = format!("{type_label}-RED-{country_code}-{counter}");
646 let red_desc = format!(
647 "{country_name} {type_label} Reduced {label_suffix} {}",
648 format_rate_pct(red_rate)
649 );
650
651 codes.push(TaxCode::new(
652 red_code_id,
653 red_mnemonic,
654 red_desc,
655 tax_type,
656 red_rate,
657 &federal_id,
658 effective_date,
659 ));
660 counter += 1;
661 }
662
663 if !tax.vat.zero_rated.is_empty() {
665 let zero_code_id = format!("TC-{company_code}-{counter:04}");
666 codes.push(TaxCode::new(
667 zero_code_id,
668 format!("{type_label}-ZERO-{country_code}"),
669 format!("{country_name} {type_label} Zero Rate"),
670 tax_type,
671 dec!(0),
672 &federal_id,
673 effective_date,
674 ));
675 counter += 1;
676 }
677
678 if !tax.vat.exempt.is_empty() {
680 let exempt_code_id = format!("TC-{company_code}-{counter:04}");
681 codes.push(
682 TaxCode::new(
683 exempt_code_id,
684 format!("{type_label}-EX-{country_code}"),
685 format!("{country_name} Tax Exempt"),
686 tax_type,
687 dec!(0),
688 &federal_id,
689 effective_date,
690 )
691 .with_exempt(true),
692 );
693 counter += 1;
694 }
695 }
696
697 if has_cit {
701 let cit_rate = Decimal::try_from(tax.corporate_income_tax.standard_rate)
702 .unwrap_or_else(|_| dec!(0));
703
704 let cit_code_id = format!("TC-{company_code}-{counter:04}");
705 codes.push(TaxCode::new(
706 cit_code_id,
707 format!("CIT-{country_code}"),
708 format!(
709 "{country_name} Corporate Income Tax {}",
710 format_rate_pct(cit_rate)
711 ),
712 TaxType::IncomeTax,
713 cit_rate,
714 &federal_id,
715 effective_date,
716 ));
717 counter += 1;
718 }
719
720 for sub in &tax.subnational {
724 if sub.code.is_empty() {
725 continue;
726 }
727
728 let jur_id = format!("JUR-{company_code}-{country_code}-{}", sub.code);
729
730 let sub_name = if sub.name.is_empty() {
731 &sub.code
732 } else {
733 &sub.name
734 };
735
736 jurisdictions.push(
737 TaxJurisdiction::new(&jur_id, sub_name, country_code, JurisdictionType::State)
738 .with_region_code(&sub.code)
739 .with_parent_jurisdiction_id(&federal_id)
740 .with_vat_registered(has_vat),
741 );
742
743 if sub.rate > 0.0 {
745 let sub_rate = Decimal::try_from(sub.rate).unwrap_or_else(|_| dec!(0));
746
747 let sub_tax_type = match sub.tax_type.as_str() {
748 "sales_tax" | "SalesTax" => TaxType::SalesTax,
749 "gst" | "Gst" | "GST" => TaxType::Gst,
750 "vat" | "Vat" | "VAT" => TaxType::Vat,
751 "income_tax" | "IncomeTax" => TaxType::IncomeTax,
752 _ => {
753 if country_code == "US" {
755 TaxType::SalesTax
756 } else if is_gst_country(country_code) {
757 TaxType::Gst
758 } else {
759 TaxType::Vat
760 }
761 }
762 };
763
764 let type_label = match sub_tax_type {
765 TaxType::SalesTax => "ST",
766 TaxType::Gst => "GST",
767 TaxType::Vat => "VAT",
768 TaxType::IncomeTax => "CIT",
769 _ => "TAX",
770 };
771
772 let sub_code_id = format!("TC-{company_code}-{counter:04}");
773 let sub_mnemonic = format!("{type_label}-{}", sub.code);
774 let sub_desc = format!("{sub_name} {} {}", type_label, format_rate_pct(sub_rate));
775
776 codes.push(TaxCode::new(
777 sub_code_id,
778 sub_mnemonic,
779 sub_desc,
780 sub_tax_type,
781 sub_rate,
782 &jur_id,
783 effective_date,
784 ));
785 counter += 1;
786 }
787 }
788
789 let _ = self.rng.random::<u32>();
792
793 (jurisdictions, codes)
794 }
795
796 fn resolve_standard_rate(&self, country_code: &str, default_str: &str) -> Decimal {
802 if let Some(&override_rate) = self.config.vat_gst.standard_rates.get(country_code) {
803 Decimal::try_from(override_rate)
804 .unwrap_or_else(|_| default_str.parse().expect("valid decimal"))
805 } else {
806 default_str.parse().expect("valid decimal")
807 }
808 }
809
810 fn resolve_reduced_rate(
812 &self,
813 country_code: &str,
814 default_opt: Option<&str>,
815 ) -> Option<Decimal> {
816 if let Some(&override_rate) = self.config.vat_gst.reduced_rates.get(country_code) {
817 Some(Decimal::try_from(override_rate).unwrap_or_else(|_| {
818 default_opt
819 .map(|s| s.parse().expect("valid decimal"))
820 .unwrap_or(dec!(0))
821 }))
822 } else {
823 default_opt.map(|s| s.parse().expect("valid decimal"))
824 }
825 }
826}
827
828fn is_eu_country(cc: &str) -> bool {
834 matches!(
835 cc,
836 "DE" | "FR"
837 | "IT"
838 | "ES"
839 | "NL"
840 | "BE"
841 | "AT"
842 | "PT"
843 | "IE"
844 | "FI"
845 | "SE"
846 | "DK"
847 | "PL"
848 | "CZ"
849 | "RO"
850 | "HU"
851 | "BG"
852 | "HR"
853 | "SK"
854 | "SI"
855 | "LT"
856 | "LV"
857 | "EE"
858 | "CY"
859 | "LU"
860 | "MT"
861 | "EL"
862 | "GR"
863 )
864}
865
866fn is_gst_country(cc: &str) -> bool {
868 matches!(cc, "SG" | "AU" | "NZ" | "IN" | "CA" | "MY" | "JP")
869}
870
871fn format_rate_pct(rate: Decimal) -> String {
873 let pct = rate * dec!(100);
874 let s = pct.normalize().to_string();
876 format!("{s}%")
877}
878
879#[cfg(test)]
884#[allow(clippy::unwrap_used)]
885mod tests {
886 use super::*;
887 #[test]
888 fn test_generate_default_countries() {
889 let mut gen = TaxCodeGenerator::new(42);
890 let (jurisdictions, codes) = gen.generate();
891
892 let countries: Vec<&str> = jurisdictions
894 .iter()
895 .map(|j| j.country_code.as_str())
896 .collect();
897 assert!(countries.contains(&"US"), "Should contain US");
898 assert!(countries.contains(&"DE"), "Should contain DE");
899 assert!(countries.contains(&"GB"), "Should contain GB");
900
901 assert!(
903 jurisdictions
904 .iter()
905 .any(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::Federal),
906 "US should have a federal jurisdiction"
907 );
908 assert!(
909 jurisdictions
910 .iter()
911 .any(|j| j.country_code == "DE" && j.jurisdiction_type == JurisdictionType::Federal),
912 "DE should have a federal jurisdiction"
913 );
914 assert!(
915 jurisdictions
916 .iter()
917 .any(|j| j.country_code == "GB" && j.jurisdiction_type == JurisdictionType::Federal),
918 "GB should have a federal jurisdiction"
919 );
920
921 assert!(!codes.is_empty(), "Should produce tax codes");
923 }
924
925 #[test]
926 fn test_generate_specific_countries() {
927 let mut config = TaxConfig::default();
928 config.jurisdictions.countries = vec!["SG".into(), "JP".into()];
929
930 let mut gen = TaxCodeGenerator::with_config(42, config);
931 let (jurisdictions, codes) = gen.generate();
932
933 let country_codes: Vec<&str> = jurisdictions
934 .iter()
935 .map(|j| j.country_code.as_str())
936 .collect();
937
938 assert!(country_codes.contains(&"SG"), "Should contain SG");
939 assert!(country_codes.contains(&"JP"), "Should contain JP");
940 assert!(!country_codes.contains(&"US"), "Should NOT contain US");
941 assert!(!country_codes.contains(&"DE"), "Should NOT contain DE");
942
943 let sg_codes: Vec<&TaxCode> = codes
945 .iter()
946 .filter(|c| c.jurisdiction_id == "JUR-SG")
947 .collect();
948 assert!(!sg_codes.is_empty(), "SG should have tax codes");
949 assert!(
950 sg_codes.iter().any(|c| c.tax_type == TaxType::Gst),
951 "SG codes should be GST type"
952 );
953
954 let jp_codes: Vec<&TaxCode> = codes
956 .iter()
957 .filter(|c| c.jurisdiction_id == "JUR-JP")
958 .collect();
959 let jp_rates: Vec<Decimal> = jp_codes
960 .iter()
961 .filter(|c| !c.is_exempt)
962 .map(|c| c.rate)
963 .collect();
964 assert!(
965 jp_rates.contains(&dec!(0.10)),
966 "JP should have standard rate 10%"
967 );
968 assert!(
969 jp_rates.contains(&dec!(0.08)),
970 "JP should have reduced rate 8%"
971 );
972 }
973
974 #[test]
975 fn test_us_sales_tax_codes() {
976 let mut config = TaxConfig::default();
977 config.jurisdictions.countries = vec!["US".into()];
978 config.jurisdictions.include_subnational = true;
979
980 let mut gen = TaxCodeGenerator::with_config(42, config);
981 let (jurisdictions, codes) = gen.generate();
982
983 let federal = jurisdictions
985 .iter()
986 .find(|j| j.id == "JUR-US")
987 .expect("US federal jurisdiction");
988 assert_eq!(federal.jurisdiction_type, JurisdictionType::Federal);
989
990 let state_jurs: Vec<&TaxJurisdiction> = jurisdictions
991 .iter()
992 .filter(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::State)
993 .collect();
994 assert_eq!(
995 state_jurs.len(),
996 10,
997 "Should have 10 US state jurisdictions"
998 );
999
1000 let ca_code = codes
1002 .iter()
1003 .find(|c| c.code == "ST-CA")
1004 .expect("California sales tax code");
1005 assert_eq!(ca_code.rate, dec!(0.0725));
1006 assert_eq!(ca_code.tax_type, TaxType::SalesTax);
1007
1008 let ny_code = codes
1009 .iter()
1010 .find(|c| c.code == "ST-NY")
1011 .expect("New York sales tax code");
1012 assert_eq!(ny_code.rate, dec!(0.08));
1013
1014 let tx_code = codes
1015 .iter()
1016 .find(|c| c.code == "ST-TX")
1017 .expect("Texas sales tax code");
1018 assert_eq!(tx_code.rate, dec!(0.0625));
1019 }
1020
1021 #[test]
1022 fn test_eu_vat_codes() {
1023 let mut config = TaxConfig::default();
1024 config.jurisdictions.countries = vec!["DE".into(), "GB".into(), "FR".into()];
1025
1026 let mut gen = TaxCodeGenerator::with_config(42, config);
1027 let (_jurisdictions, codes) = gen.generate();
1028
1029 let de_std = codes
1031 .iter()
1032 .find(|c| c.code == "VAT-STD-DE")
1033 .expect("DE standard VAT code");
1034 assert_eq!(de_std.rate, dec!(0.19));
1035 assert_eq!(de_std.tax_type, TaxType::Vat);
1036 assert!(de_std.is_reverse_charge, "DE should have reverse charge");
1037
1038 let de_red = codes
1039 .iter()
1040 .find(|c| c.code == "VAT-RED-DE")
1041 .expect("DE reduced VAT code");
1042 assert_eq!(de_red.rate, dec!(0.07));
1043
1044 let gb_std = codes
1046 .iter()
1047 .find(|c| c.code == "VAT-STD-GB")
1048 .expect("GB standard VAT code");
1049 assert_eq!(gb_std.rate, dec!(0.20));
1050 assert!(
1051 !gb_std.is_reverse_charge,
1052 "GB should NOT have reverse charge (not EU)"
1053 );
1054
1055 let gb_red = codes
1056 .iter()
1057 .find(|c| c.code == "VAT-RED-GB")
1058 .expect("GB reduced VAT code");
1059 assert_eq!(gb_red.rate, dec!(0.05));
1060
1061 let gb_zero = codes
1062 .iter()
1063 .find(|c| c.code == "VAT-ZERO-GB")
1064 .expect("GB zero-rate VAT code");
1065 assert_eq!(gb_zero.rate, dec!(0));
1066
1067 let fr_std = codes
1069 .iter()
1070 .find(|c| c.code == "VAT-STD-FR")
1071 .expect("FR standard VAT code");
1072 assert_eq!(fr_std.rate, dec!(0.20));
1073 assert!(fr_std.is_reverse_charge, "FR should have reverse charge");
1074
1075 let fr_red = codes
1076 .iter()
1077 .find(|c| c.code == "VAT-RED-FR")
1078 .expect("FR reduced VAT code");
1079 assert_eq!(fr_red.rate, dec!(0.055));
1080 }
1081
1082 #[test]
1083 fn test_deterministic() {
1084 let mut gen1 = TaxCodeGenerator::new(12345);
1085 let (jur1, codes1) = gen1.generate();
1086
1087 let mut gen2 = TaxCodeGenerator::new(12345);
1088 let (jur2, codes2) = gen2.generate();
1089
1090 assert_eq!(jur1.len(), jur2.len(), "Same number of jurisdictions");
1091 assert_eq!(codes1.len(), codes2.len(), "Same number of codes");
1092
1093 for (j1, j2) in jur1.iter().zip(jur2.iter()) {
1094 assert_eq!(j1.id, j2.id);
1095 assert_eq!(j1.name, j2.name);
1096 assert_eq!(j1.country_code, j2.country_code);
1097 assert_eq!(j1.jurisdiction_type, j2.jurisdiction_type);
1098 assert_eq!(j1.vat_registered, j2.vat_registered);
1099 }
1100
1101 for (c1, c2) in codes1.iter().zip(codes2.iter()) {
1102 assert_eq!(c1.id, c2.id);
1103 assert_eq!(c1.code, c2.code);
1104 assert_eq!(c1.rate, c2.rate);
1105 assert_eq!(c1.tax_type, c2.tax_type);
1106 }
1107 }
1108
1109 #[test]
1110 fn test_config_rate_override() {
1111 let mut config = TaxConfig::default();
1112 config.jurisdictions.countries = vec!["DE".into()];
1113 config.vat_gst.standard_rates.insert("DE".into(), 0.25);
1114
1115 let mut gen = TaxCodeGenerator::with_config(42, config);
1116 let (_jurisdictions, codes) = gen.generate();
1117
1118 let de_std = codes
1119 .iter()
1120 .find(|c| c.code == "VAT-STD-DE")
1121 .expect("DE standard VAT code");
1122 assert_eq!(
1123 de_std.rate,
1124 dec!(0.25),
1125 "Config override should replace built-in rate"
1126 );
1127 }
1128
1129 #[test]
1130 fn test_subnational_generation() {
1131 let mut config = TaxConfig::default();
1132 config.jurisdictions.countries = vec!["US".into(), "IN".into(), "CA".into()];
1133 config.jurisdictions.include_subnational = true;
1134
1135 let mut gen = TaxCodeGenerator::with_config(42, config);
1136 let (jurisdictions, codes) = gen.generate();
1137
1138 let us_jurs: Vec<&TaxJurisdiction> = jurisdictions
1140 .iter()
1141 .filter(|j| j.country_code == "US")
1142 .collect();
1143 assert_eq!(us_jurs.len(), 11, "US: 1 federal + 10 states");
1144
1145 let in_jurs: Vec<&TaxJurisdiction> = jurisdictions
1147 .iter()
1148 .filter(|j| j.country_code == "IN")
1149 .collect();
1150 assert_eq!(in_jurs.len(), 11, "IN: 1 federal + 10 states");
1151
1152 let in_states: Vec<&TaxJurisdiction> = in_jurs
1154 .iter()
1155 .filter(|j| j.jurisdiction_type == JurisdictionType::State)
1156 .copied()
1157 .collect();
1158 assert!(
1159 in_states.iter().all(|j| j.vat_registered),
1160 "IN states should be VAT-registered"
1161 );
1162
1163 let in_slab_codes: Vec<&TaxCode> = codes
1165 .iter()
1166 .filter(|c| c.code.starts_with("GST-SLAB-"))
1167 .collect();
1168 assert_eq!(in_slab_codes.len(), 4, "India should have 4 GST slab codes");
1169
1170 let ca_jurs: Vec<&TaxJurisdiction> = jurisdictions
1172 .iter()
1173 .filter(|j| j.country_code == "CA")
1174 .collect();
1175 assert_eq!(ca_jurs.len(), 11, "CA: 1 federal + 10 provinces");
1176
1177 let ca_hst_codes: Vec<&TaxCode> = codes
1179 .iter()
1180 .filter(|c| c.code.starts_with("HST-"))
1181 .collect();
1182 assert_eq!(
1183 ca_hst_codes.len(),
1184 10,
1185 "CA should have 10 provincial HST codes"
1186 );
1187
1188 let on_code = ca_hst_codes
1190 .iter()
1191 .find(|c| c.code == "HST-ON")
1192 .expect("Ontario HST code");
1193 assert_eq!(on_code.rate, dec!(0.13));
1194 }
1195
1196 #[test]
1197 fn test_nexus_states_filter() {
1198 let mut config = TaxConfig::default();
1199 config.jurisdictions.countries = vec!["US".into()];
1200 config.jurisdictions.include_subnational = true;
1201 config.sales_tax.nexus_states = vec!["CA".into(), "NY".into()];
1202
1203 let mut gen = TaxCodeGenerator::with_config(42, config);
1204 let (jurisdictions, codes) = gen.generate();
1205
1206 let state_jurs: Vec<&TaxJurisdiction> = jurisdictions
1207 .iter()
1208 .filter(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::State)
1209 .collect();
1210 assert_eq!(state_jurs.len(), 2, "Should only generate nexus states");
1211
1212 let state_codes: Vec<String> = state_jurs
1213 .iter()
1214 .filter_map(|j| j.region_code.clone())
1215 .collect();
1216 assert!(state_codes.contains(&"CA".to_string()));
1217 assert!(state_codes.contains(&"NY".to_string()));
1218
1219 let sales_codes: Vec<&TaxCode> = codes
1221 .iter()
1222 .filter(|c| c.tax_type == TaxType::SalesTax)
1223 .collect();
1224 assert_eq!(sales_codes.len(), 2);
1225 }
1226
1227 #[test]
1228 fn test_vat_registered_flag() {
1229 let mut config = TaxConfig::default();
1230 config.jurisdictions.countries = vec!["DE".into(), "SG".into(), "US".into()];
1231
1232 let mut gen = TaxCodeGenerator::with_config(42, config);
1233 let (jurisdictions, _codes) = gen.generate();
1234
1235 let de_federal = jurisdictions
1236 .iter()
1237 .find(|j| j.id == "JUR-DE")
1238 .expect("DE federal");
1239 assert!(de_federal.vat_registered, "DE should be VAT-registered");
1240
1241 let sg_federal = jurisdictions
1242 .iter()
1243 .find(|j| j.id == "JUR-SG")
1244 .expect("SG federal");
1245 assert!(
1246 sg_federal.vat_registered,
1247 "SG should be VAT-registered (GST)"
1248 );
1249
1250 let us_federal = jurisdictions
1251 .iter()
1252 .find(|j| j.id == "JUR-US")
1253 .expect("US federal");
1254 assert!(
1255 !us_federal.vat_registered,
1256 "US should NOT be VAT-registered (sales tax)"
1257 );
1258 }
1259
1260 #[test]
1261 fn test_exempt_codes_generated() {
1262 let mut config = TaxConfig::default();
1263 config.jurisdictions.countries = vec!["DE".into()];
1264
1265 let mut gen = TaxCodeGenerator::with_config(42, config);
1266 let (_jurisdictions, codes) = gen.generate();
1267
1268 let exempt = codes
1269 .iter()
1270 .find(|c| c.code == "VAT-EX-DE")
1271 .expect("DE exempt code");
1272 assert!(exempt.is_exempt);
1273 assert_eq!(exempt.rate, dec!(0));
1274 assert_eq!(exempt.tax_amount(dec!(10000)), dec!(0));
1275 }
1276
1277 #[test]
1278 fn test_effective_dates() {
1279 let mut gen = TaxCodeGenerator::new(42);
1280 let (_jurisdictions, codes) = gen.generate();
1281
1282 let expected_date = NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
1283 for code in &codes {
1284 assert_eq!(
1285 code.effective_date, expected_date,
1286 "All codes should have effective date 2020-01-01, got {} for {}",
1287 code.effective_date, code.code
1288 );
1289 assert!(
1290 code.expiry_date.is_none(),
1291 "Codes should not have an expiry date"
1292 );
1293 }
1294 }
1295
1296 #[test]
1297 fn test_reduced_rate_override() {
1298 let mut config = TaxConfig::default();
1299 config.jurisdictions.countries = vec!["JP".into()];
1300 config.vat_gst.reduced_rates.insert("JP".into(), 0.03);
1301
1302 let mut gen = TaxCodeGenerator::with_config(42, config);
1303 let (_jurisdictions, codes) = gen.generate();
1304
1305 let jp_red = codes
1306 .iter()
1307 .find(|c| c.code == "GST-RED-JP")
1308 .expect("JP reduced GST code");
1309 assert_eq!(
1310 jp_red.rate,
1311 dec!(0.03),
1312 "Reduced rate override should apply"
1313 );
1314 }
1315
1316 #[test]
1317 fn test_germany_subnational() {
1318 let mut config = TaxConfig::default();
1319 config.jurisdictions.countries = vec!["DE".into()];
1320 config.jurisdictions.include_subnational = true;
1321
1322 let mut gen = TaxCodeGenerator::with_config(42, config);
1323 let (jurisdictions, _codes) = gen.generate();
1324
1325 let de_states: Vec<&TaxJurisdiction> = jurisdictions
1326 .iter()
1327 .filter(|j| j.country_code == "DE" && j.jurisdiction_type == JurisdictionType::State)
1328 .collect();
1329 assert_eq!(de_states.len(), 16, "Germany should have 16 Bundeslaender");
1330
1331 for state in &de_states {
1333 assert_eq!(
1334 state.parent_jurisdiction_id,
1335 Some("JUR-DE".to_string()),
1336 "State {} should have federal parent",
1337 state.name
1338 );
1339 assert!(state.vat_registered);
1340 }
1341 }
1342
1343 #[test]
1344 fn test_format_rate_pct() {
1345 assert_eq!(format_rate_pct(dec!(0.19)), "19%");
1346 assert_eq!(format_rate_pct(dec!(0.055)), "5.5%");
1347 assert_eq!(format_rate_pct(dec!(0.0725)), "7.25%");
1348 assert_eq!(format_rate_pct(dec!(0)), "0%");
1349 }
1350}