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)]
884mod tests {
885 use super::*;
886 #[test]
887 fn test_generate_default_countries() {
888 let mut gen = TaxCodeGenerator::new(42);
889 let (jurisdictions, codes) = gen.generate();
890
891 let countries: Vec<&str> = jurisdictions
893 .iter()
894 .map(|j| j.country_code.as_str())
895 .collect();
896 assert!(countries.contains(&"US"), "Should contain US");
897 assert!(countries.contains(&"DE"), "Should contain DE");
898 assert!(countries.contains(&"GB"), "Should contain GB");
899
900 assert!(
902 jurisdictions
903 .iter()
904 .any(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::Federal),
905 "US should have a federal jurisdiction"
906 );
907 assert!(
908 jurisdictions
909 .iter()
910 .any(|j| j.country_code == "DE" && j.jurisdiction_type == JurisdictionType::Federal),
911 "DE should have a federal jurisdiction"
912 );
913 assert!(
914 jurisdictions
915 .iter()
916 .any(|j| j.country_code == "GB" && j.jurisdiction_type == JurisdictionType::Federal),
917 "GB should have a federal jurisdiction"
918 );
919
920 assert!(!codes.is_empty(), "Should produce tax codes");
922 }
923
924 #[test]
925 fn test_generate_specific_countries() {
926 let mut config = TaxConfig::default();
927 config.jurisdictions.countries = vec!["SG".into(), "JP".into()];
928
929 let mut gen = TaxCodeGenerator::with_config(42, config);
930 let (jurisdictions, codes) = gen.generate();
931
932 let country_codes: Vec<&str> = jurisdictions
933 .iter()
934 .map(|j| j.country_code.as_str())
935 .collect();
936
937 assert!(country_codes.contains(&"SG"), "Should contain SG");
938 assert!(country_codes.contains(&"JP"), "Should contain JP");
939 assert!(!country_codes.contains(&"US"), "Should NOT contain US");
940 assert!(!country_codes.contains(&"DE"), "Should NOT contain DE");
941
942 let sg_codes: Vec<&TaxCode> = codes
944 .iter()
945 .filter(|c| c.jurisdiction_id == "JUR-SG")
946 .collect();
947 assert!(!sg_codes.is_empty(), "SG should have tax codes");
948 assert!(
949 sg_codes.iter().any(|c| c.tax_type == TaxType::Gst),
950 "SG codes should be GST type"
951 );
952
953 let jp_codes: Vec<&TaxCode> = codes
955 .iter()
956 .filter(|c| c.jurisdiction_id == "JUR-JP")
957 .collect();
958 let jp_rates: Vec<Decimal> = jp_codes
959 .iter()
960 .filter(|c| !c.is_exempt)
961 .map(|c| c.rate)
962 .collect();
963 assert!(
964 jp_rates.contains(&dec!(0.10)),
965 "JP should have standard rate 10%"
966 );
967 assert!(
968 jp_rates.contains(&dec!(0.08)),
969 "JP should have reduced rate 8%"
970 );
971 }
972
973 #[test]
974 fn test_us_sales_tax_codes() {
975 let mut config = TaxConfig::default();
976 config.jurisdictions.countries = vec!["US".into()];
977 config.jurisdictions.include_subnational = true;
978
979 let mut gen = TaxCodeGenerator::with_config(42, config);
980 let (jurisdictions, codes) = gen.generate();
981
982 let federal = jurisdictions
984 .iter()
985 .find(|j| j.id == "JUR-US")
986 .expect("US federal jurisdiction");
987 assert_eq!(federal.jurisdiction_type, JurisdictionType::Federal);
988
989 let state_jurs: Vec<&TaxJurisdiction> = jurisdictions
990 .iter()
991 .filter(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::State)
992 .collect();
993 assert_eq!(
994 state_jurs.len(),
995 10,
996 "Should have 10 US state jurisdictions"
997 );
998
999 let ca_code = codes
1001 .iter()
1002 .find(|c| c.code == "ST-CA")
1003 .expect("California sales tax code");
1004 assert_eq!(ca_code.rate, dec!(0.0725));
1005 assert_eq!(ca_code.tax_type, TaxType::SalesTax);
1006
1007 let ny_code = codes
1008 .iter()
1009 .find(|c| c.code == "ST-NY")
1010 .expect("New York sales tax code");
1011 assert_eq!(ny_code.rate, dec!(0.08));
1012
1013 let tx_code = codes
1014 .iter()
1015 .find(|c| c.code == "ST-TX")
1016 .expect("Texas sales tax code");
1017 assert_eq!(tx_code.rate, dec!(0.0625));
1018 }
1019
1020 #[test]
1021 fn test_eu_vat_codes() {
1022 let mut config = TaxConfig::default();
1023 config.jurisdictions.countries = vec!["DE".into(), "GB".into(), "FR".into()];
1024
1025 let mut gen = TaxCodeGenerator::with_config(42, config);
1026 let (_jurisdictions, codes) = gen.generate();
1027
1028 let de_std = codes
1030 .iter()
1031 .find(|c| c.code == "VAT-STD-DE")
1032 .expect("DE standard VAT code");
1033 assert_eq!(de_std.rate, dec!(0.19));
1034 assert_eq!(de_std.tax_type, TaxType::Vat);
1035 assert!(de_std.is_reverse_charge, "DE should have reverse charge");
1036
1037 let de_red = codes
1038 .iter()
1039 .find(|c| c.code == "VAT-RED-DE")
1040 .expect("DE reduced VAT code");
1041 assert_eq!(de_red.rate, dec!(0.07));
1042
1043 let gb_std = codes
1045 .iter()
1046 .find(|c| c.code == "VAT-STD-GB")
1047 .expect("GB standard VAT code");
1048 assert_eq!(gb_std.rate, dec!(0.20));
1049 assert!(
1050 !gb_std.is_reverse_charge,
1051 "GB should NOT have reverse charge (not EU)"
1052 );
1053
1054 let gb_red = codes
1055 .iter()
1056 .find(|c| c.code == "VAT-RED-GB")
1057 .expect("GB reduced VAT code");
1058 assert_eq!(gb_red.rate, dec!(0.05));
1059
1060 let gb_zero = codes
1061 .iter()
1062 .find(|c| c.code == "VAT-ZERO-GB")
1063 .expect("GB zero-rate VAT code");
1064 assert_eq!(gb_zero.rate, dec!(0));
1065
1066 let fr_std = codes
1068 .iter()
1069 .find(|c| c.code == "VAT-STD-FR")
1070 .expect("FR standard VAT code");
1071 assert_eq!(fr_std.rate, dec!(0.20));
1072 assert!(fr_std.is_reverse_charge, "FR should have reverse charge");
1073
1074 let fr_red = codes
1075 .iter()
1076 .find(|c| c.code == "VAT-RED-FR")
1077 .expect("FR reduced VAT code");
1078 assert_eq!(fr_red.rate, dec!(0.055));
1079 }
1080
1081 #[test]
1082 fn test_deterministic() {
1083 let mut gen1 = TaxCodeGenerator::new(12345);
1084 let (jur1, codes1) = gen1.generate();
1085
1086 let mut gen2 = TaxCodeGenerator::new(12345);
1087 let (jur2, codes2) = gen2.generate();
1088
1089 assert_eq!(jur1.len(), jur2.len(), "Same number of jurisdictions");
1090 assert_eq!(codes1.len(), codes2.len(), "Same number of codes");
1091
1092 for (j1, j2) in jur1.iter().zip(jur2.iter()) {
1093 assert_eq!(j1.id, j2.id);
1094 assert_eq!(j1.name, j2.name);
1095 assert_eq!(j1.country_code, j2.country_code);
1096 assert_eq!(j1.jurisdiction_type, j2.jurisdiction_type);
1097 assert_eq!(j1.vat_registered, j2.vat_registered);
1098 }
1099
1100 for (c1, c2) in codes1.iter().zip(codes2.iter()) {
1101 assert_eq!(c1.id, c2.id);
1102 assert_eq!(c1.code, c2.code);
1103 assert_eq!(c1.rate, c2.rate);
1104 assert_eq!(c1.tax_type, c2.tax_type);
1105 }
1106 }
1107
1108 #[test]
1109 fn test_config_rate_override() {
1110 let mut config = TaxConfig::default();
1111 config.jurisdictions.countries = vec!["DE".into()];
1112 config.vat_gst.standard_rates.insert("DE".into(), 0.25);
1113
1114 let mut gen = TaxCodeGenerator::with_config(42, config);
1115 let (_jurisdictions, codes) = gen.generate();
1116
1117 let de_std = codes
1118 .iter()
1119 .find(|c| c.code == "VAT-STD-DE")
1120 .expect("DE standard VAT code");
1121 assert_eq!(
1122 de_std.rate,
1123 dec!(0.25),
1124 "Config override should replace built-in rate"
1125 );
1126 }
1127
1128 #[test]
1129 fn test_subnational_generation() {
1130 let mut config = TaxConfig::default();
1131 config.jurisdictions.countries = vec!["US".into(), "IN".into(), "CA".into()];
1132 config.jurisdictions.include_subnational = true;
1133
1134 let mut gen = TaxCodeGenerator::with_config(42, config);
1135 let (jurisdictions, codes) = gen.generate();
1136
1137 let us_jurs: Vec<&TaxJurisdiction> = jurisdictions
1139 .iter()
1140 .filter(|j| j.country_code == "US")
1141 .collect();
1142 assert_eq!(us_jurs.len(), 11, "US: 1 federal + 10 states");
1143
1144 let in_jurs: Vec<&TaxJurisdiction> = jurisdictions
1146 .iter()
1147 .filter(|j| j.country_code == "IN")
1148 .collect();
1149 assert_eq!(in_jurs.len(), 11, "IN: 1 federal + 10 states");
1150
1151 let in_states: Vec<&TaxJurisdiction> = in_jurs
1153 .iter()
1154 .filter(|j| j.jurisdiction_type == JurisdictionType::State)
1155 .copied()
1156 .collect();
1157 assert!(
1158 in_states.iter().all(|j| j.vat_registered),
1159 "IN states should be VAT-registered"
1160 );
1161
1162 let in_slab_codes: Vec<&TaxCode> = codes
1164 .iter()
1165 .filter(|c| c.code.starts_with("GST-SLAB-"))
1166 .collect();
1167 assert_eq!(in_slab_codes.len(), 4, "India should have 4 GST slab codes");
1168
1169 let ca_jurs: Vec<&TaxJurisdiction> = jurisdictions
1171 .iter()
1172 .filter(|j| j.country_code == "CA")
1173 .collect();
1174 assert_eq!(ca_jurs.len(), 11, "CA: 1 federal + 10 provinces");
1175
1176 let ca_hst_codes: Vec<&TaxCode> = codes
1178 .iter()
1179 .filter(|c| c.code.starts_with("HST-"))
1180 .collect();
1181 assert_eq!(
1182 ca_hst_codes.len(),
1183 10,
1184 "CA should have 10 provincial HST codes"
1185 );
1186
1187 let on_code = ca_hst_codes
1189 .iter()
1190 .find(|c| c.code == "HST-ON")
1191 .expect("Ontario HST code");
1192 assert_eq!(on_code.rate, dec!(0.13));
1193 }
1194
1195 #[test]
1196 fn test_nexus_states_filter() {
1197 let mut config = TaxConfig::default();
1198 config.jurisdictions.countries = vec!["US".into()];
1199 config.jurisdictions.include_subnational = true;
1200 config.sales_tax.nexus_states = vec!["CA".into(), "NY".into()];
1201
1202 let mut gen = TaxCodeGenerator::with_config(42, config);
1203 let (jurisdictions, codes) = gen.generate();
1204
1205 let state_jurs: Vec<&TaxJurisdiction> = jurisdictions
1206 .iter()
1207 .filter(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::State)
1208 .collect();
1209 assert_eq!(state_jurs.len(), 2, "Should only generate nexus states");
1210
1211 let state_codes: Vec<String> = state_jurs
1212 .iter()
1213 .filter_map(|j| j.region_code.clone())
1214 .collect();
1215 assert!(state_codes.contains(&"CA".to_string()));
1216 assert!(state_codes.contains(&"NY".to_string()));
1217
1218 let sales_codes: Vec<&TaxCode> = codes
1220 .iter()
1221 .filter(|c| c.tax_type == TaxType::SalesTax)
1222 .collect();
1223 assert_eq!(sales_codes.len(), 2);
1224 }
1225
1226 #[test]
1227 fn test_vat_registered_flag() {
1228 let mut config = TaxConfig::default();
1229 config.jurisdictions.countries = vec!["DE".into(), "SG".into(), "US".into()];
1230
1231 let mut gen = TaxCodeGenerator::with_config(42, config);
1232 let (jurisdictions, _codes) = gen.generate();
1233
1234 let de_federal = jurisdictions
1235 .iter()
1236 .find(|j| j.id == "JUR-DE")
1237 .expect("DE federal");
1238 assert!(de_federal.vat_registered, "DE should be VAT-registered");
1239
1240 let sg_federal = jurisdictions
1241 .iter()
1242 .find(|j| j.id == "JUR-SG")
1243 .expect("SG federal");
1244 assert!(
1245 sg_federal.vat_registered,
1246 "SG should be VAT-registered (GST)"
1247 );
1248
1249 let us_federal = jurisdictions
1250 .iter()
1251 .find(|j| j.id == "JUR-US")
1252 .expect("US federal");
1253 assert!(
1254 !us_federal.vat_registered,
1255 "US should NOT be VAT-registered (sales tax)"
1256 );
1257 }
1258
1259 #[test]
1260 fn test_exempt_codes_generated() {
1261 let mut config = TaxConfig::default();
1262 config.jurisdictions.countries = vec!["DE".into()];
1263
1264 let mut gen = TaxCodeGenerator::with_config(42, config);
1265 let (_jurisdictions, codes) = gen.generate();
1266
1267 let exempt = codes
1268 .iter()
1269 .find(|c| c.code == "VAT-EX-DE")
1270 .expect("DE exempt code");
1271 assert!(exempt.is_exempt);
1272 assert_eq!(exempt.rate, dec!(0));
1273 assert_eq!(exempt.tax_amount(dec!(10000)), dec!(0));
1274 }
1275
1276 #[test]
1277 fn test_effective_dates() {
1278 let mut gen = TaxCodeGenerator::new(42);
1279 let (_jurisdictions, codes) = gen.generate();
1280
1281 let expected_date = NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
1282 for code in &codes {
1283 assert_eq!(
1284 code.effective_date, expected_date,
1285 "All codes should have effective date 2020-01-01, got {} for {}",
1286 code.effective_date, code.code
1287 );
1288 assert!(
1289 code.expiry_date.is_none(),
1290 "Codes should not have an expiry date"
1291 );
1292 }
1293 }
1294
1295 #[test]
1296 fn test_reduced_rate_override() {
1297 let mut config = TaxConfig::default();
1298 config.jurisdictions.countries = vec!["JP".into()];
1299 config.vat_gst.reduced_rates.insert("JP".into(), 0.03);
1300
1301 let mut gen = TaxCodeGenerator::with_config(42, config);
1302 let (_jurisdictions, codes) = gen.generate();
1303
1304 let jp_red = codes
1305 .iter()
1306 .find(|c| c.code == "GST-RED-JP")
1307 .expect("JP reduced GST code");
1308 assert_eq!(
1309 jp_red.rate,
1310 dec!(0.03),
1311 "Reduced rate override should apply"
1312 );
1313 }
1314
1315 #[test]
1316 fn test_germany_subnational() {
1317 let mut config = TaxConfig::default();
1318 config.jurisdictions.countries = vec!["DE".into()];
1319 config.jurisdictions.include_subnational = true;
1320
1321 let mut gen = TaxCodeGenerator::with_config(42, config);
1322 let (jurisdictions, _codes) = gen.generate();
1323
1324 let de_states: Vec<&TaxJurisdiction> = jurisdictions
1325 .iter()
1326 .filter(|j| j.country_code == "DE" && j.jurisdiction_type == JurisdictionType::State)
1327 .collect();
1328 assert_eq!(de_states.len(), 16, "Germany should have 16 Bundeslaender");
1329
1330 for state in &de_states {
1332 assert_eq!(
1333 state.parent_jurisdiction_id,
1334 Some("JUR-DE".to_string()),
1335 "State {} should have federal parent",
1336 state.name
1337 );
1338 assert!(state.vat_registered);
1339 }
1340 }
1341
1342 #[test]
1343 fn test_format_rate_pct() {
1344 assert_eq!(format_rate_pct(dec!(0.19)), "19%");
1345 assert_eq!(format_rate_pct(dec!(0.055)), "5.5%");
1346 assert_eq!(format_rate_pct(dec!(0.0725)), "7.25%");
1347 assert_eq!(format_rate_pct(dec!(0)), "0%");
1348 }
1349}