Skip to main content

datasynth_generators/tax/
tax_code_generator.rs

1//! Tax Code Generator.
2//!
3//! Generates tax jurisdictions and tax codes with built-in rate tables
4//! for common countries. Supports VAT/GST (EU, UK, SG, AU, JP, IN, BR, CA),
5//! sales tax (US states), and config-driven rate overrides.
6
7use 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
16// ---------------------------------------------------------------------------
17// Built-in rate tables
18// ---------------------------------------------------------------------------
19
20/// US state sales tax rates (top 10 nexus states).
21const 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
34/// Country-level VAT/GST rate table.
35///
36/// Tuple: (country_code, country_name, tax_type, standard_rate, reduced_rate_or_none)
37const 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
52/// Indian state names for GST sub-jurisdictions.
53const 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
66/// German Bundeslaender (states).
67const 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
86/// Canadian provinces for GST/HST sub-jurisdictions.
87const 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
100/// India GST slabs beyond the standard 18%.
101const 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
108/// Default effective date for all generated tax codes.
109fn default_effective_date() -> NaiveDate {
110    NaiveDate::from_ymd_opt(2020, 1, 1).expect("valid date")
111}
112
113// ---------------------------------------------------------------------------
114// Generator
115// ---------------------------------------------------------------------------
116
117/// Generates tax jurisdictions and tax codes from built-in rate tables.
118///
119/// The generator reads `TaxConfig` to determine which countries to produce
120/// jurisdictions for, whether to include sub-national jurisdictions (US states,
121/// Canadian provinces, etc.), and whether config-provided rate overrides should
122/// replace the built-in defaults.
123///
124/// # Examples
125///
126/// ```
127/// use datasynth_generators::tax::TaxCodeGenerator;
128///
129/// let mut gen = TaxCodeGenerator::new(42);
130/// let (jurisdictions, codes) = gen.generate();
131/// assert!(!jurisdictions.is_empty());
132/// assert!(!codes.is_empty());
133/// ```
134pub struct TaxCodeGenerator {
135    rng: ChaCha8Rng,
136    config: TaxConfig,
137}
138
139impl TaxCodeGenerator {
140    /// Creates a new generator with default configuration.
141    ///
142    /// Default config generates jurisdictions for US, DE, and GB.
143    pub fn new(seed: u64) -> Self {
144        Self {
145            rng: ChaCha8Rng::seed_from_u64(seed),
146            config: TaxConfig::default(),
147        }
148    }
149
150    /// Creates a new generator with custom configuration.
151    pub fn with_config(seed: u64, config: TaxConfig) -> Self {
152        Self {
153            rng: ChaCha8Rng::seed_from_u64(seed),
154            config,
155        }
156    }
157
158    /// Generates tax jurisdictions and tax codes.
159    ///
160    /// Returns a tuple of `(Vec<TaxJurisdiction>, Vec<TaxCode>)`.
161    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    // -----------------------------------------------------------------------
192    // Internal helpers
193    // -----------------------------------------------------------------------
194
195    /// Resolves the list of country codes to generate.
196    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    /// Generates US federal + state jurisdictions and sales tax codes.
205    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        // Federal jurisdiction (no federal sales tax, but anchor the hierarchy)
215        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        // Determine which states to generate
227        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 non-empty, only generate those states
231            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    /// Generates a non-US country's federal jurisdiction, tax codes, and
266    /// optionally sub-national jurisdictions.
267    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        // Look up country in the built-in rate table
276        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                // Unknown country - skip silently (config may list countries
284                // for which we don't have built-in rates yet)
285                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        // Federal jurisdiction
299        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        // Resolve standard rate (config override > built-in)
310        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        // Standard-rate code
314        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        // For EU countries, enable reverse charge on the standard code
345        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        // Reduced-rate code (if applicable)
353        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        // Zero-rate code for GB (food, children's clothing)
387        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        // Exempt code
402        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        // Sub-national jurisdictions
427        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    /// Generates sub-national jurisdictions for countries that have them.
440    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                // India: state-level GST jurisdictions + slab codes
452                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                // India GST slab codes (attached to federal jurisdiction)
463                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                // Germany: Bundeslaender (no separate tax rates, but jurisdiction hierarchy)
481                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                // Canada: provincial HST/PST combined rates
493                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                // No sub-national jurisdictions for other countries
518            }
519        }
520    }
521
522    /// Resolves the standard rate for a country, applying config overrides.
523    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    /// Resolves the reduced rate for a country, applying config overrides.
533    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
550// ---------------------------------------------------------------------------
551// Utility functions
552// ---------------------------------------------------------------------------
553
554/// Returns `true` for EU member state country codes.
555fn 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
588/// Formats a decimal rate as a percentage string (e.g., 0.19 -> "19%").
589fn format_rate_pct(rate: Decimal) -> String {
590    let pct = rate * dec!(100);
591    // Strip trailing zeros for cleaner display
592    let s = pct.normalize().to_string();
593    format!("{s}%")
594}
595
596// ---------------------------------------------------------------------------
597// Tests
598// ---------------------------------------------------------------------------
599
600#[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        // Default countries: US, DE, GB
610        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        // Should have at least one jurisdiction per country
619        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        // Should have tax codes generated
639        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        // SG should have GST codes
661        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        // JP should have standard and reduced rates
672        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        // Should have federal + state jurisdictions
701        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        // Check specific state rates
718        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        // DE: standard 19%, reduced 7%
747        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        // GB: standard 20%, reduced 5%, zero rate
762        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        // FR: standard 20%, reduced 5.5%
785        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        // US: 1 federal + 10 states
856        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        // IN: 1 federal + 10 states
863        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        // IN state jurisdictions should be VAT-registered
870        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        // India should have GST slab codes
881        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        // CA: 1 federal + 10 provinces
888        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        // CA should have HST codes per province
895        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        // Ontario HST should be 13%
906        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        // Sales tax codes should only be for CA and NY
937        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        // All states should reference the federal parent
1049        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}