1use chrono::NaiveDate;
8use rand::prelude::*;
9use rand_chacha::ChaCha8Rng;
10use rust_decimal::Decimal;
11use rust_decimal_macros::dec;
12
13use datasynth_config::schema::TaxConfig;
14use datasynth_core::models::{JurisdictionType, TaxCode, TaxJurisdiction, TaxType};
15
16const US_STATE_RATES: &[(&str, &str, &str)] = &[
22 ("CA", "California", "0.0725"),
23 ("NY", "New York", "0.08"),
24 ("TX", "Texas", "0.0625"),
25 ("FL", "Florida", "0.06"),
26 ("WA", "Washington", "0.065"),
27 ("IL", "Illinois", "0.0625"),
28 ("PA", "Pennsylvania", "0.06"),
29 ("OH", "Ohio", "0.0575"),
30 ("NJ", "New Jersey", "0.06625"),
31 ("GA", "Georgia", "0.04"),
32];
33
34const COUNTRY_RATES: &[(&str, &str, &str, &str, Option<&str>)] = &[
38 ("DE", "Germany", "vat", "0.19", Some("0.07")),
39 ("GB", "United Kingdom", "vat", "0.20", Some("0.05")),
40 ("FR", "France", "vat", "0.20", Some("0.055")),
41 ("IT", "Italy", "vat", "0.22", Some("0.10")),
42 ("ES", "Spain", "vat", "0.21", Some("0.10")),
43 ("NL", "Netherlands", "vat", "0.21", Some("0.09")),
44 ("SG", "Singapore", "gst", "0.09", None),
45 ("AU", "Australia", "gst", "0.10", None),
46 ("JP", "Japan", "gst", "0.10", Some("0.08")),
47 ("IN", "India", "gst", "0.18", Some("0.05")),
48 ("BR", "Brazil", "vat", "0.17", None),
49 ("CA", "Canada", "gst", "0.05", None),
50];
51
52const INDIA_STATES: &[(&str, &str)] = &[
54 ("MH", "Maharashtra"),
55 ("DL", "Delhi"),
56 ("KA", "Karnataka"),
57 ("TN", "Tamil Nadu"),
58 ("GJ", "Gujarat"),
59 ("UP", "Uttar Pradesh"),
60 ("WB", "West Bengal"),
61 ("RJ", "Rajasthan"),
62 ("TG", "Telangana"),
63 ("KL", "Kerala"),
64];
65
66const GERMANY_STATES: &[(&str, &str)] = &[
68 ("BW", "Baden-Wuerttemberg"),
69 ("BY", "Bavaria"),
70 ("BE", "Berlin"),
71 ("BB", "Brandenburg"),
72 ("HB", "Bremen"),
73 ("HH", "Hamburg"),
74 ("HE", "Hesse"),
75 ("MV", "Mecklenburg-Vorpommern"),
76 ("NI", "Lower Saxony"),
77 ("NW", "North Rhine-Westphalia"),
78 ("RP", "Rhineland-Palatinate"),
79 ("SL", "Saarland"),
80 ("SN", "Saxony"),
81 ("ST", "Saxony-Anhalt"),
82 ("SH", "Schleswig-Holstein"),
83 ("TH", "Thuringia"),
84];
85
86const CANADA_PROVINCES: &[(&str, &str, &str)] = &[
88 ("ON", "Ontario", "0.13"),
89 ("BC", "British Columbia", "0.12"),
90 ("QC", "Quebec", "0.14975"),
91 ("AB", "Alberta", "0.05"),
92 ("NS", "Nova Scotia", "0.15"),
93 ("NB", "New Brunswick", "0.15"),
94 ("MB", "Manitoba", "0.12"),
95 ("SK", "Saskatchewan", "0.11"),
96 ("NL", "Newfoundland and Labrador", "0.15"),
97 ("PE", "Prince Edward Island", "0.15"),
98];
99
100const INDIA_GST_SLABS: &[(&str, &str)] = &[
102 ("0.05", "GST 5% slab"),
103 ("0.12", "GST 12% slab"),
104 ("0.18", "GST 18% slab"),
105 ("0.28", "GST 28% slab"),
106];
107
108fn default_effective_date() -> NaiveDate {
110 NaiveDate::from_ymd_opt(2020, 1, 1).expect("valid date")
111}
112
113pub struct TaxCodeGenerator {
135 rng: ChaCha8Rng,
136 config: TaxConfig,
137}
138
139impl TaxCodeGenerator {
140 pub fn new(seed: u64) -> Self {
144 Self {
145 rng: ChaCha8Rng::seed_from_u64(seed),
146 config: TaxConfig::default(),
147 }
148 }
149
150 pub fn with_config(seed: u64, config: TaxConfig) -> Self {
152 Self {
153 rng: ChaCha8Rng::seed_from_u64(seed),
154 config,
155 }
156 }
157
158 pub fn generate(&mut self) -> (Vec<TaxJurisdiction>, Vec<TaxCode>) {
162 let countries = self.resolve_countries();
163 let include_subnational = self.config.jurisdictions.include_subnational;
164
165 let mut jurisdictions = Vec::new();
166 let mut codes = Vec::new();
167 let mut code_counter: u32 = 1;
168
169 for country in &countries {
170 let cc = country.as_str();
171 match cc {
172 "US" => self.generate_us(
173 include_subnational,
174 &mut jurisdictions,
175 &mut codes,
176 &mut code_counter,
177 ),
178 _ => self.generate_country(
179 cc,
180 include_subnational,
181 &mut jurisdictions,
182 &mut codes,
183 &mut code_counter,
184 ),
185 }
186 }
187
188 (jurisdictions, codes)
189 }
190
191 fn resolve_countries(&self) -> Vec<String> {
197 if self.config.jurisdictions.countries.is_empty() {
198 vec!["US".into(), "DE".into(), "GB".into()]
199 } else {
200 self.config.jurisdictions.countries.clone()
201 }
202 }
203
204 fn generate_us(
206 &mut self,
207 include_subnational: bool,
208 jurisdictions: &mut Vec<TaxJurisdiction>,
209 codes: &mut Vec<TaxCode>,
210 counter: &mut u32,
211 ) {
212 let federal_id = "JUR-US".to_string();
213
214 jurisdictions.push(TaxJurisdiction::new(
216 &federal_id,
217 "United States - Federal",
218 "US",
219 JurisdictionType::Federal,
220 ));
221
222 if !include_subnational {
223 return;
224 }
225
226 let nexus_states = &self.config.sales_tax.nexus_states;
228
229 for &(state_code, state_name, rate_str) in US_STATE_RATES {
230 if !nexus_states.is_empty()
232 && !nexus_states
233 .iter()
234 .any(|s| s.eq_ignore_ascii_case(state_code))
235 {
236 continue;
237 }
238
239 let jur_id = format!("JUR-US-{state_code}");
240
241 jurisdictions.push(
242 TaxJurisdiction::new(&jur_id, state_name, "US", JurisdictionType::State)
243 .with_region_code(state_code)
244 .with_parent_jurisdiction_id(&federal_id),
245 );
246
247 let rate: Decimal = rate_str.parse().expect("valid decimal");
248 let code_id = format!("TC-{counter:04}");
249 let code_mnemonic = format!("ST-{state_code}");
250 let description = format!("{state_name} Sales Tax {}", format_rate_pct(rate));
251
252 codes.push(TaxCode::new(
253 code_id,
254 code_mnemonic,
255 description,
256 TaxType::SalesTax,
257 rate,
258 &jur_id,
259 default_effective_date(),
260 ));
261 *counter += 1;
262 }
263 }
264
265 fn generate_country(
268 &mut self,
269 country_code: &str,
270 include_subnational: bool,
271 jurisdictions: &mut Vec<TaxJurisdiction>,
272 codes: &mut Vec<TaxCode>,
273 counter: &mut u32,
274 ) {
275 let entry = COUNTRY_RATES
277 .iter()
278 .find(|(cc, _, _, _, _)| *cc == country_code);
279
280 let (country_name, tax_type_str, default_std_rate_str, default_reduced_str) = match entry {
281 Some((_, name, tt, std_rate, reduced)) => (*name, *tt, *std_rate, *reduced),
282 None => {
283 return;
286 }
287 };
288
289 let tax_type = match tax_type_str {
290 "gst" => TaxType::Gst,
291 _ => TaxType::Vat,
292 };
293
294 let is_vat_gst = matches!(tax_type, TaxType::Vat | TaxType::Gst);
295
296 let federal_id = format!("JUR-{country_code}");
297
298 jurisdictions.push(
300 TaxJurisdiction::new(
301 &federal_id,
302 format!("{country_name} - Federal"),
303 country_code,
304 JurisdictionType::Federal,
305 )
306 .with_vat_registered(is_vat_gst),
307 );
308
309 let std_rate = self.resolve_standard_rate(country_code, default_std_rate_str);
311 let reduced_rate = self.resolve_reduced_rate(country_code, default_reduced_str);
312
313 let std_code_id = format!("TC-{counter:04}");
315 let std_mnemonic = format!(
316 "{}-STD-{}",
317 if tax_type == TaxType::Gst {
318 "GST"
319 } else {
320 "VAT"
321 },
322 country_code
323 );
324 let std_desc = format!(
325 "{country_name} {} Standard {}",
326 if tax_type == TaxType::Gst {
327 "GST"
328 } else {
329 "VAT"
330 },
331 format_rate_pct(std_rate)
332 );
333
334 let mut std_code = TaxCode::new(
335 std_code_id,
336 std_mnemonic,
337 std_desc,
338 tax_type,
339 std_rate,
340 &federal_id,
341 default_effective_date(),
342 );
343
344 if is_eu_country(country_code) && self.config.vat_gst.reverse_charge {
346 std_code = std_code.with_reverse_charge(true);
347 }
348
349 codes.push(std_code);
350 *counter += 1;
351
352 if let Some(red_rate) = reduced_rate {
354 let red_code_id = format!("TC-{counter:04}");
355 let red_mnemonic = format!(
356 "{}-RED-{}",
357 if tax_type == TaxType::Gst {
358 "GST"
359 } else {
360 "VAT"
361 },
362 country_code
363 );
364 let red_desc = format!(
365 "{country_name} {} Reduced {}",
366 if tax_type == TaxType::Gst {
367 "GST"
368 } else {
369 "VAT"
370 },
371 format_rate_pct(red_rate)
372 );
373
374 codes.push(TaxCode::new(
375 red_code_id,
376 red_mnemonic,
377 red_desc,
378 tax_type,
379 red_rate,
380 &federal_id,
381 default_effective_date(),
382 ));
383 *counter += 1;
384 }
385
386 if country_code == "GB" {
388 let zero_code_id = format!("TC-{counter:04}");
389 codes.push(TaxCode::new(
390 zero_code_id,
391 format!("VAT-ZERO-{country_code}"),
392 format!("{country_name} VAT Zero Rate"),
393 TaxType::Vat,
394 dec!(0),
395 &federal_id,
396 default_effective_date(),
397 ));
398 *counter += 1;
399 }
400
401 let exempt_code_id = format!("TC-{counter:04}");
403 let exempt_mnemonic = format!(
404 "{}-EX-{}",
405 if tax_type == TaxType::Gst {
406 "GST"
407 } else {
408 "VAT"
409 },
410 country_code
411 );
412 codes.push(
413 TaxCode::new(
414 exempt_code_id,
415 exempt_mnemonic,
416 format!("{country_name} Tax Exempt"),
417 tax_type,
418 dec!(0),
419 &federal_id,
420 default_effective_date(),
421 )
422 .with_exempt(true),
423 );
424 *counter += 1;
425
426 if include_subnational {
428 self.generate_subnational(
429 country_code,
430 &federal_id,
431 tax_type,
432 jurisdictions,
433 codes,
434 counter,
435 );
436 }
437 }
438
439 fn generate_subnational(
441 &mut self,
442 country_code: &str,
443 federal_id: &str,
444 _tax_type: TaxType,
445 jurisdictions: &mut Vec<TaxJurisdiction>,
446 codes: &mut Vec<TaxCode>,
447 counter: &mut u32,
448 ) {
449 match country_code {
450 "IN" => {
451 for &(state_code, state_name) in INDIA_STATES {
453 let jur_id = format!("JUR-IN-{state_code}");
454 jurisdictions.push(
455 TaxJurisdiction::new(&jur_id, state_name, "IN", JurisdictionType::State)
456 .with_region_code(state_code)
457 .with_parent_jurisdiction_id(federal_id)
458 .with_vat_registered(true),
459 );
460 }
461
462 for &(rate_str, label) in INDIA_GST_SLABS {
464 let rate: Decimal = rate_str.parse().expect("valid decimal");
465 let code_id = format!("TC-{counter:04}");
466 let pct = format_rate_pct(rate);
467 codes.push(TaxCode::new(
468 code_id,
469 format!("GST-SLAB-{pct}"),
470 label,
471 TaxType::Gst,
472 rate,
473 federal_id,
474 default_effective_date(),
475 ));
476 *counter += 1;
477 }
478 }
479 "DE" => {
480 for &(state_code, state_name) in GERMANY_STATES {
482 let jur_id = format!("JUR-DE-{state_code}");
483 jurisdictions.push(
484 TaxJurisdiction::new(&jur_id, state_name, "DE", JurisdictionType::State)
485 .with_region_code(state_code)
486 .with_parent_jurisdiction_id(federal_id)
487 .with_vat_registered(true),
488 );
489 }
490 }
491 "CA" => {
492 for &(prov_code, prov_name, combined_rate_str) in CANADA_PROVINCES {
494 let jur_id = format!("JUR-CA-{prov_code}");
495 jurisdictions.push(
496 TaxJurisdiction::new(&jur_id, prov_name, "CA", JurisdictionType::State)
497 .with_region_code(prov_code)
498 .with_parent_jurisdiction_id(federal_id)
499 .with_vat_registered(true),
500 );
501
502 let combined_rate: Decimal = combined_rate_str.parse().expect("valid decimal");
503 let code_id = format!("TC-{counter:04}");
504 codes.push(TaxCode::new(
505 code_id,
506 format!("HST-{prov_code}"),
507 format!("{prov_name} HST/GST+PST {}", format_rate_pct(combined_rate)),
508 TaxType::Gst,
509 combined_rate,
510 &jur_id,
511 default_effective_date(),
512 ));
513 *counter += 1;
514 }
515 }
516 _ => {
517 }
519 }
520 }
521
522 fn resolve_standard_rate(&self, country_code: &str, default_str: &str) -> Decimal {
524 if let Some(&override_rate) = self.config.vat_gst.standard_rates.get(country_code) {
525 Decimal::try_from(override_rate)
526 .unwrap_or_else(|_| default_str.parse().expect("valid decimal"))
527 } else {
528 default_str.parse().expect("valid decimal")
529 }
530 }
531
532 fn resolve_reduced_rate(
534 &self,
535 country_code: &str,
536 default_opt: Option<&str>,
537 ) -> Option<Decimal> {
538 if let Some(&override_rate) = self.config.vat_gst.reduced_rates.get(country_code) {
539 Some(Decimal::try_from(override_rate).unwrap_or_else(|_| {
540 default_opt
541 .map(|s| s.parse().expect("valid decimal"))
542 .unwrap_or(dec!(0))
543 }))
544 } else {
545 default_opt.map(|s| s.parse().expect("valid decimal"))
546 }
547 }
548}
549
550fn is_eu_country(cc: &str) -> bool {
556 matches!(
557 cc,
558 "DE" | "FR"
559 | "IT"
560 | "ES"
561 | "NL"
562 | "BE"
563 | "AT"
564 | "PT"
565 | "IE"
566 | "FI"
567 | "SE"
568 | "DK"
569 | "PL"
570 | "CZ"
571 | "RO"
572 | "HU"
573 | "BG"
574 | "HR"
575 | "SK"
576 | "SI"
577 | "LT"
578 | "LV"
579 | "EE"
580 | "CY"
581 | "LU"
582 | "MT"
583 | "EL"
584 | "GR"
585 )
586}
587
588fn format_rate_pct(rate: Decimal) -> String {
590 let pct = rate * dec!(100);
591 let s = pct.normalize().to_string();
593 format!("{s}%")
594}
595
596#[cfg(test)]
601#[allow(clippy::unwrap_used)]
602mod tests {
603 use super::*;
604 #[test]
605 fn test_generate_default_countries() {
606 let mut gen = TaxCodeGenerator::new(42);
607 let (jurisdictions, codes) = gen.generate();
608
609 let countries: Vec<&str> = jurisdictions
611 .iter()
612 .map(|j| j.country_code.as_str())
613 .collect();
614 assert!(countries.contains(&"US"), "Should contain US");
615 assert!(countries.contains(&"DE"), "Should contain DE");
616 assert!(countries.contains(&"GB"), "Should contain GB");
617
618 assert!(
620 jurisdictions
621 .iter()
622 .any(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::Federal),
623 "US should have a federal jurisdiction"
624 );
625 assert!(
626 jurisdictions
627 .iter()
628 .any(|j| j.country_code == "DE" && j.jurisdiction_type == JurisdictionType::Federal),
629 "DE should have a federal jurisdiction"
630 );
631 assert!(
632 jurisdictions
633 .iter()
634 .any(|j| j.country_code == "GB" && j.jurisdiction_type == JurisdictionType::Federal),
635 "GB should have a federal jurisdiction"
636 );
637
638 assert!(!codes.is_empty(), "Should produce tax codes");
640 }
641
642 #[test]
643 fn test_generate_specific_countries() {
644 let mut config = TaxConfig::default();
645 config.jurisdictions.countries = vec!["SG".into(), "JP".into()];
646
647 let mut gen = TaxCodeGenerator::with_config(42, config);
648 let (jurisdictions, codes) = gen.generate();
649
650 let country_codes: Vec<&str> = jurisdictions
651 .iter()
652 .map(|j| j.country_code.as_str())
653 .collect();
654
655 assert!(country_codes.contains(&"SG"), "Should contain SG");
656 assert!(country_codes.contains(&"JP"), "Should contain JP");
657 assert!(!country_codes.contains(&"US"), "Should NOT contain US");
658 assert!(!country_codes.contains(&"DE"), "Should NOT contain DE");
659
660 let sg_codes: Vec<&TaxCode> = codes
662 .iter()
663 .filter(|c| c.jurisdiction_id == "JUR-SG")
664 .collect();
665 assert!(!sg_codes.is_empty(), "SG should have tax codes");
666 assert!(
667 sg_codes.iter().any(|c| c.tax_type == TaxType::Gst),
668 "SG codes should be GST type"
669 );
670
671 let jp_codes: Vec<&TaxCode> = codes
673 .iter()
674 .filter(|c| c.jurisdiction_id == "JUR-JP")
675 .collect();
676 let jp_rates: Vec<Decimal> = jp_codes
677 .iter()
678 .filter(|c| !c.is_exempt)
679 .map(|c| c.rate)
680 .collect();
681 assert!(
682 jp_rates.contains(&dec!(0.10)),
683 "JP should have standard rate 10%"
684 );
685 assert!(
686 jp_rates.contains(&dec!(0.08)),
687 "JP should have reduced rate 8%"
688 );
689 }
690
691 #[test]
692 fn test_us_sales_tax_codes() {
693 let mut config = TaxConfig::default();
694 config.jurisdictions.countries = vec!["US".into()];
695 config.jurisdictions.include_subnational = true;
696
697 let mut gen = TaxCodeGenerator::with_config(42, config);
698 let (jurisdictions, codes) = gen.generate();
699
700 let federal = jurisdictions
702 .iter()
703 .find(|j| j.id == "JUR-US")
704 .expect("US federal jurisdiction");
705 assert_eq!(federal.jurisdiction_type, JurisdictionType::Federal);
706
707 let state_jurs: Vec<&TaxJurisdiction> = jurisdictions
708 .iter()
709 .filter(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::State)
710 .collect();
711 assert_eq!(
712 state_jurs.len(),
713 10,
714 "Should have 10 US state jurisdictions"
715 );
716
717 let ca_code = codes
719 .iter()
720 .find(|c| c.code == "ST-CA")
721 .expect("California sales tax code");
722 assert_eq!(ca_code.rate, dec!(0.0725));
723 assert_eq!(ca_code.tax_type, TaxType::SalesTax);
724
725 let ny_code = codes
726 .iter()
727 .find(|c| c.code == "ST-NY")
728 .expect("New York sales tax code");
729 assert_eq!(ny_code.rate, dec!(0.08));
730
731 let tx_code = codes
732 .iter()
733 .find(|c| c.code == "ST-TX")
734 .expect("Texas sales tax code");
735 assert_eq!(tx_code.rate, dec!(0.0625));
736 }
737
738 #[test]
739 fn test_eu_vat_codes() {
740 let mut config = TaxConfig::default();
741 config.jurisdictions.countries = vec!["DE".into(), "GB".into(), "FR".into()];
742
743 let mut gen = TaxCodeGenerator::with_config(42, config);
744 let (_jurisdictions, codes) = gen.generate();
745
746 let de_std = codes
748 .iter()
749 .find(|c| c.code == "VAT-STD-DE")
750 .expect("DE standard VAT code");
751 assert_eq!(de_std.rate, dec!(0.19));
752 assert_eq!(de_std.tax_type, TaxType::Vat);
753 assert!(de_std.is_reverse_charge, "DE should have reverse charge");
754
755 let de_red = codes
756 .iter()
757 .find(|c| c.code == "VAT-RED-DE")
758 .expect("DE reduced VAT code");
759 assert_eq!(de_red.rate, dec!(0.07));
760
761 let gb_std = codes
763 .iter()
764 .find(|c| c.code == "VAT-STD-GB")
765 .expect("GB standard VAT code");
766 assert_eq!(gb_std.rate, dec!(0.20));
767 assert!(
768 !gb_std.is_reverse_charge,
769 "GB should NOT have reverse charge (not EU)"
770 );
771
772 let gb_red = codes
773 .iter()
774 .find(|c| c.code == "VAT-RED-GB")
775 .expect("GB reduced VAT code");
776 assert_eq!(gb_red.rate, dec!(0.05));
777
778 let gb_zero = codes
779 .iter()
780 .find(|c| c.code == "VAT-ZERO-GB")
781 .expect("GB zero-rate VAT code");
782 assert_eq!(gb_zero.rate, dec!(0));
783
784 let fr_std = codes
786 .iter()
787 .find(|c| c.code == "VAT-STD-FR")
788 .expect("FR standard VAT code");
789 assert_eq!(fr_std.rate, dec!(0.20));
790 assert!(fr_std.is_reverse_charge, "FR should have reverse charge");
791
792 let fr_red = codes
793 .iter()
794 .find(|c| c.code == "VAT-RED-FR")
795 .expect("FR reduced VAT code");
796 assert_eq!(fr_red.rate, dec!(0.055));
797 }
798
799 #[test]
800 fn test_deterministic() {
801 let mut gen1 = TaxCodeGenerator::new(12345);
802 let (jur1, codes1) = gen1.generate();
803
804 let mut gen2 = TaxCodeGenerator::new(12345);
805 let (jur2, codes2) = gen2.generate();
806
807 assert_eq!(jur1.len(), jur2.len(), "Same number of jurisdictions");
808 assert_eq!(codes1.len(), codes2.len(), "Same number of codes");
809
810 for (j1, j2) in jur1.iter().zip(jur2.iter()) {
811 assert_eq!(j1.id, j2.id);
812 assert_eq!(j1.name, j2.name);
813 assert_eq!(j1.country_code, j2.country_code);
814 assert_eq!(j1.jurisdiction_type, j2.jurisdiction_type);
815 assert_eq!(j1.vat_registered, j2.vat_registered);
816 }
817
818 for (c1, c2) in codes1.iter().zip(codes2.iter()) {
819 assert_eq!(c1.id, c2.id);
820 assert_eq!(c1.code, c2.code);
821 assert_eq!(c1.rate, c2.rate);
822 assert_eq!(c1.tax_type, c2.tax_type);
823 }
824 }
825
826 #[test]
827 fn test_config_rate_override() {
828 let mut config = TaxConfig::default();
829 config.jurisdictions.countries = vec!["DE".into()];
830 config.vat_gst.standard_rates.insert("DE".into(), 0.25);
831
832 let mut gen = TaxCodeGenerator::with_config(42, config);
833 let (_jurisdictions, codes) = gen.generate();
834
835 let de_std = codes
836 .iter()
837 .find(|c| c.code == "VAT-STD-DE")
838 .expect("DE standard VAT code");
839 assert_eq!(
840 de_std.rate,
841 dec!(0.25),
842 "Config override should replace built-in rate"
843 );
844 }
845
846 #[test]
847 fn test_subnational_generation() {
848 let mut config = TaxConfig::default();
849 config.jurisdictions.countries = vec!["US".into(), "IN".into(), "CA".into()];
850 config.jurisdictions.include_subnational = true;
851
852 let mut gen = TaxCodeGenerator::with_config(42, config);
853 let (jurisdictions, codes) = gen.generate();
854
855 let us_jurs: Vec<&TaxJurisdiction> = jurisdictions
857 .iter()
858 .filter(|j| j.country_code == "US")
859 .collect();
860 assert_eq!(us_jurs.len(), 11, "US: 1 federal + 10 states");
861
862 let in_jurs: Vec<&TaxJurisdiction> = jurisdictions
864 .iter()
865 .filter(|j| j.country_code == "IN")
866 .collect();
867 assert_eq!(in_jurs.len(), 11, "IN: 1 federal + 10 states");
868
869 let in_states: Vec<&TaxJurisdiction> = in_jurs
871 .iter()
872 .filter(|j| j.jurisdiction_type == JurisdictionType::State)
873 .copied()
874 .collect();
875 assert!(
876 in_states.iter().all(|j| j.vat_registered),
877 "IN states should be VAT-registered"
878 );
879
880 let in_slab_codes: Vec<&TaxCode> = codes
882 .iter()
883 .filter(|c| c.code.starts_with("GST-SLAB-"))
884 .collect();
885 assert_eq!(in_slab_codes.len(), 4, "India should have 4 GST slab codes");
886
887 let ca_jurs: Vec<&TaxJurisdiction> = jurisdictions
889 .iter()
890 .filter(|j| j.country_code == "CA")
891 .collect();
892 assert_eq!(ca_jurs.len(), 11, "CA: 1 federal + 10 provinces");
893
894 let ca_hst_codes: Vec<&TaxCode> = codes
896 .iter()
897 .filter(|c| c.code.starts_with("HST-"))
898 .collect();
899 assert_eq!(
900 ca_hst_codes.len(),
901 10,
902 "CA should have 10 provincial HST codes"
903 );
904
905 let on_code = ca_hst_codes
907 .iter()
908 .find(|c| c.code == "HST-ON")
909 .expect("Ontario HST code");
910 assert_eq!(on_code.rate, dec!(0.13));
911 }
912
913 #[test]
914 fn test_nexus_states_filter() {
915 let mut config = TaxConfig::default();
916 config.jurisdictions.countries = vec!["US".into()];
917 config.jurisdictions.include_subnational = true;
918 config.sales_tax.nexus_states = vec!["CA".into(), "NY".into()];
919
920 let mut gen = TaxCodeGenerator::with_config(42, config);
921 let (jurisdictions, codes) = gen.generate();
922
923 let state_jurs: Vec<&TaxJurisdiction> = jurisdictions
924 .iter()
925 .filter(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::State)
926 .collect();
927 assert_eq!(state_jurs.len(), 2, "Should only generate nexus states");
928
929 let state_codes: Vec<String> = state_jurs
930 .iter()
931 .filter_map(|j| j.region_code.clone())
932 .collect();
933 assert!(state_codes.contains(&"CA".to_string()));
934 assert!(state_codes.contains(&"NY".to_string()));
935
936 let sales_codes: Vec<&TaxCode> = codes
938 .iter()
939 .filter(|c| c.tax_type == TaxType::SalesTax)
940 .collect();
941 assert_eq!(sales_codes.len(), 2);
942 }
943
944 #[test]
945 fn test_vat_registered_flag() {
946 let mut config = TaxConfig::default();
947 config.jurisdictions.countries = vec!["DE".into(), "SG".into(), "US".into()];
948
949 let mut gen = TaxCodeGenerator::with_config(42, config);
950 let (jurisdictions, _codes) = gen.generate();
951
952 let de_federal = jurisdictions
953 .iter()
954 .find(|j| j.id == "JUR-DE")
955 .expect("DE federal");
956 assert!(de_federal.vat_registered, "DE should be VAT-registered");
957
958 let sg_federal = jurisdictions
959 .iter()
960 .find(|j| j.id == "JUR-SG")
961 .expect("SG federal");
962 assert!(
963 sg_federal.vat_registered,
964 "SG should be VAT-registered (GST)"
965 );
966
967 let us_federal = jurisdictions
968 .iter()
969 .find(|j| j.id == "JUR-US")
970 .expect("US federal");
971 assert!(
972 !us_federal.vat_registered,
973 "US should NOT be VAT-registered (sales tax)"
974 );
975 }
976
977 #[test]
978 fn test_exempt_codes_generated() {
979 let mut config = TaxConfig::default();
980 config.jurisdictions.countries = vec!["DE".into()];
981
982 let mut gen = TaxCodeGenerator::with_config(42, config);
983 let (_jurisdictions, codes) = gen.generate();
984
985 let exempt = codes
986 .iter()
987 .find(|c| c.code == "VAT-EX-DE")
988 .expect("DE exempt code");
989 assert!(exempt.is_exempt);
990 assert_eq!(exempt.rate, dec!(0));
991 assert_eq!(exempt.tax_amount(dec!(10000)), dec!(0));
992 }
993
994 #[test]
995 fn test_effective_dates() {
996 let mut gen = TaxCodeGenerator::new(42);
997 let (_jurisdictions, codes) = gen.generate();
998
999 let expected_date = NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
1000 for code in &codes {
1001 assert_eq!(
1002 code.effective_date, expected_date,
1003 "All codes should have effective date 2020-01-01, got {} for {}",
1004 code.effective_date, code.code
1005 );
1006 assert!(
1007 code.expiry_date.is_none(),
1008 "Codes should not have an expiry date"
1009 );
1010 }
1011 }
1012
1013 #[test]
1014 fn test_reduced_rate_override() {
1015 let mut config = TaxConfig::default();
1016 config.jurisdictions.countries = vec!["JP".into()];
1017 config.vat_gst.reduced_rates.insert("JP".into(), 0.03);
1018
1019 let mut gen = TaxCodeGenerator::with_config(42, config);
1020 let (_jurisdictions, codes) = gen.generate();
1021
1022 let jp_red = codes
1023 .iter()
1024 .find(|c| c.code == "GST-RED-JP")
1025 .expect("JP reduced GST code");
1026 assert_eq!(
1027 jp_red.rate,
1028 dec!(0.03),
1029 "Reduced rate override should apply"
1030 );
1031 }
1032
1033 #[test]
1034 fn test_germany_subnational() {
1035 let mut config = TaxConfig::default();
1036 config.jurisdictions.countries = vec!["DE".into()];
1037 config.jurisdictions.include_subnational = true;
1038
1039 let mut gen = TaxCodeGenerator::with_config(42, config);
1040 let (jurisdictions, _codes) = gen.generate();
1041
1042 let de_states: Vec<&TaxJurisdiction> = jurisdictions
1043 .iter()
1044 .filter(|j| j.country_code == "DE" && j.jurisdiction_type == JurisdictionType::State)
1045 .collect();
1046 assert_eq!(de_states.len(), 16, "Germany should have 16 Bundeslaender");
1047
1048 for state in &de_states {
1050 assert_eq!(
1051 state.parent_jurisdiction_id,
1052 Some("JUR-DE".to_string()),
1053 "State {} should have federal parent",
1054 state.name
1055 );
1056 assert!(state.vat_registered);
1057 }
1058 }
1059
1060 #[test]
1061 fn test_format_rate_pct() {
1062 assert_eq!(format_rate_pct(dec!(0.19)), "19%");
1063 assert_eq!(format_rate_pct(dec!(0.055)), "5.5%");
1064 assert_eq!(format_rate_pct(dec!(0.0725)), "7.25%");
1065 assert_eq!(format_rate_pct(dec!(0)), "0%");
1066 }
1067}