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 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
17// ---------------------------------------------------------------------------
18// Built-in rate tables
19// ---------------------------------------------------------------------------
20
21/// US state sales tax rates (top 10 nexus states).
22const 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
35/// Country-level VAT/GST rate table.
36///
37/// Tuple: (country_code, country_name, tax_type, standard_rate, reduced_rate_or_none)
38const 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
53/// Indian state names for GST sub-jurisdictions.
54const 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
67/// German Bundeslaender (states).
68const 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
87/// Canadian provinces for GST/HST sub-jurisdictions.
88const 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
101/// India GST slabs beyond the standard 18%.
102const 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
109/// Default effective date for all generated tax codes.
110fn default_effective_date() -> NaiveDate {
111    NaiveDate::from_ymd_opt(2020, 1, 1).expect("valid date")
112}
113
114// ---------------------------------------------------------------------------
115// Generator
116// ---------------------------------------------------------------------------
117
118/// Generates tax jurisdictions and tax codes from built-in rate tables.
119///
120/// The generator reads `TaxConfig` to determine which countries to produce
121/// jurisdictions for, whether to include sub-national jurisdictions (US states,
122/// Canadian provinces, etc.), and whether config-provided rate overrides should
123/// replace the built-in defaults.
124///
125/// # Examples
126///
127/// ```
128/// use datasynth_generators::tax::TaxCodeGenerator;
129///
130/// let mut gen = TaxCodeGenerator::new(42);
131/// let (jurisdictions, codes) = gen.generate();
132/// assert!(!jurisdictions.is_empty());
133/// assert!(!codes.is_empty());
134/// ```
135pub struct TaxCodeGenerator {
136    rng: ChaCha8Rng,
137    config: TaxConfig,
138}
139
140impl TaxCodeGenerator {
141    /// Creates a new generator with default configuration.
142    ///
143    /// Default config generates jurisdictions for US, DE, and GB.
144    pub fn new(seed: u64) -> Self {
145        Self {
146            rng: seeded_rng(seed, 0),
147            config: TaxConfig::default(),
148        }
149    }
150
151    /// Creates a new generator with custom configuration.
152    pub fn with_config(seed: u64, config: TaxConfig) -> Self {
153        Self {
154            rng: seeded_rng(seed, 0),
155            config,
156        }
157    }
158
159    /// Generates tax jurisdictions and tax codes.
160    ///
161    /// Returns a tuple of `(Vec<TaxJurisdiction>, Vec<TaxCode>)`.
162    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    // -----------------------------------------------------------------------
193    // Internal helpers
194    // -----------------------------------------------------------------------
195
196    /// Resolves the list of country codes to generate.
197    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    /// Generates US federal + state jurisdictions and sales tax codes.
206    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        // Federal jurisdiction (no federal sales tax, but anchor the hierarchy)
216        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        // Determine which states to generate
228        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 non-empty, only generate those states
232            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    /// Generates a non-US country's federal jurisdiction, tax codes, and
267    /// optionally sub-national jurisdictions.
268    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        // Look up country in the built-in rate table
277        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                // Unknown country - skip silently (config may list countries
285                // for which we don't have built-in rates yet)
286                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        // Federal jurisdiction
300        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        // Resolve standard rate (config override > built-in)
311        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        // Standard-rate code
315        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        // For EU countries, enable reverse charge on the standard code
346        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        // Reduced-rate code (if applicable)
354        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        // Zero-rate code for GB (food, children's clothing)
388        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        // Exempt code
403        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        // Sub-national jurisdictions
428        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    /// Generates sub-national jurisdictions for countries that have them.
441    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                // India: state-level GST jurisdictions + slab codes
453                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                // India GST slab codes (attached to federal jurisdiction)
464                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                // Germany: Bundeslaender (no separate tax rates, but jurisdiction hierarchy)
482                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                // Canada: provincial HST/PST combined rates
494                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                // No sub-national jurisdictions for other countries
519            }
520        }
521    }
522
523    // -----------------------------------------------------------------------
524    // Country-pack-driven generation
525    // -----------------------------------------------------------------------
526
527    /// Generates tax jurisdictions and tax codes from a [`CountryPack`].
528    ///
529    /// This is an **alternative** to [`generate()`](Self::generate) that reads
530    /// tax rates and sub-national jurisdictions from a country pack instead of
531    /// using the hardcoded constants. If the pack carries no meaningful tax data
532    /// (e.g. `standard_rate == 0.0` and no sub-national entries), the method
533    /// returns empty vectors so the caller can fall back to `generate()`.
534    ///
535    /// # Arguments
536    ///
537    /// * `pack` - The country pack whose tax data should drive generation.
538    /// * `company_code` - Company code used to prefix generated IDs.
539    /// * `fiscal_year` - Fiscal year; used to derive the effective date
540    ///   (January 1 of that year).
541    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        // Guard: if the pack has no meaningful tax data, return empty.
556        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        // -------------------------------------------------------------------
572        // Federal jurisdiction
573        // -------------------------------------------------------------------
574        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        // -------------------------------------------------------------------
587        // VAT/GST codes from pack
588        // -------------------------------------------------------------------
589        if has_vat {
590            let std_rate = Decimal::try_from(tax.vat.standard_rate).unwrap_or_else(|_| dec!(0));
591
592            // Determine tax type: treat country packs as VAT by default,
593            // but use GST for known GST countries.
594            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            // Standard rate code
607            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            // Reduced rate codes
632            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            // Zero-rated code (if the pack lists zero-rated categories)
664            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            // Exempt code (if the pack lists exempt categories)
679            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        // -------------------------------------------------------------------
698        // Corporate income tax code
699        // -------------------------------------------------------------------
700        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        // -------------------------------------------------------------------
721        // Sub-national jurisdictions from pack
722        // -------------------------------------------------------------------
723        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            // Generate a tax code for this sub-national jurisdiction if it has a rate
744            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                        // Infer from country: US → SalesTax, else VAT/GST
754                        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        // Suppress unused-variable warning for the RNG (deterministic but unused
790        // in this path; kept for future jitter / randomised selection).
791        let _ = self.rng.random::<u32>();
792
793        (jurisdictions, codes)
794    }
795
796    // -----------------------------------------------------------------------
797    // Internal helpers
798    // -----------------------------------------------------------------------
799
800    /// Resolves the standard rate for a country, applying config overrides.
801    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    /// Resolves the reduced rate for a country, applying config overrides.
811    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
828// ---------------------------------------------------------------------------
829// Utility functions
830// ---------------------------------------------------------------------------
831
832/// Returns `true` for EU member state country codes.
833fn 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
866/// Returns `true` for countries that use GST rather than VAT.
867fn is_gst_country(cc: &str) -> bool {
868    matches!(cc, "SG" | "AU" | "NZ" | "IN" | "CA" | "MY" | "JP")
869}
870
871/// Formats a decimal rate as a percentage string (e.g., 0.19 -> "19%").
872fn format_rate_pct(rate: Decimal) -> String {
873    let pct = rate * dec!(100);
874    // Strip trailing zeros for cleaner display
875    let s = pct.normalize().to_string();
876    format!("{s}%")
877}
878
879// ---------------------------------------------------------------------------
880// Tests
881// ---------------------------------------------------------------------------
882
883#[cfg(test)]
884#[allow(clippy::unwrap_used)]
885mod tests {
886    use super::*;
887    #[test]
888    fn test_generate_default_countries() {
889        let mut gen = TaxCodeGenerator::new(42);
890        let (jurisdictions, codes) = gen.generate();
891
892        // Default countries: US, DE, GB
893        let countries: Vec<&str> = jurisdictions
894            .iter()
895            .map(|j| j.country_code.as_str())
896            .collect();
897        assert!(countries.contains(&"US"), "Should contain US");
898        assert!(countries.contains(&"DE"), "Should contain DE");
899        assert!(countries.contains(&"GB"), "Should contain GB");
900
901        // Should have at least one jurisdiction per country
902        assert!(
903            jurisdictions
904                .iter()
905                .any(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::Federal),
906            "US should have a federal jurisdiction"
907        );
908        assert!(
909            jurisdictions
910                .iter()
911                .any(|j| j.country_code == "DE" && j.jurisdiction_type == JurisdictionType::Federal),
912            "DE should have a federal jurisdiction"
913        );
914        assert!(
915            jurisdictions
916                .iter()
917                .any(|j| j.country_code == "GB" && j.jurisdiction_type == JurisdictionType::Federal),
918            "GB should have a federal jurisdiction"
919        );
920
921        // Should have tax codes generated
922        assert!(!codes.is_empty(), "Should produce tax codes");
923    }
924
925    #[test]
926    fn test_generate_specific_countries() {
927        let mut config = TaxConfig::default();
928        config.jurisdictions.countries = vec!["SG".into(), "JP".into()];
929
930        let mut gen = TaxCodeGenerator::with_config(42, config);
931        let (jurisdictions, codes) = gen.generate();
932
933        let country_codes: Vec<&str> = jurisdictions
934            .iter()
935            .map(|j| j.country_code.as_str())
936            .collect();
937
938        assert!(country_codes.contains(&"SG"), "Should contain SG");
939        assert!(country_codes.contains(&"JP"), "Should contain JP");
940        assert!(!country_codes.contains(&"US"), "Should NOT contain US");
941        assert!(!country_codes.contains(&"DE"), "Should NOT contain DE");
942
943        // SG should have GST codes
944        let sg_codes: Vec<&TaxCode> = codes
945            .iter()
946            .filter(|c| c.jurisdiction_id == "JUR-SG")
947            .collect();
948        assert!(!sg_codes.is_empty(), "SG should have tax codes");
949        assert!(
950            sg_codes.iter().any(|c| c.tax_type == TaxType::Gst),
951            "SG codes should be GST type"
952        );
953
954        // JP should have standard and reduced rates
955        let jp_codes: Vec<&TaxCode> = codes
956            .iter()
957            .filter(|c| c.jurisdiction_id == "JUR-JP")
958            .collect();
959        let jp_rates: Vec<Decimal> = jp_codes
960            .iter()
961            .filter(|c| !c.is_exempt)
962            .map(|c| c.rate)
963            .collect();
964        assert!(
965            jp_rates.contains(&dec!(0.10)),
966            "JP should have standard rate 10%"
967        );
968        assert!(
969            jp_rates.contains(&dec!(0.08)),
970            "JP should have reduced rate 8%"
971        );
972    }
973
974    #[test]
975    fn test_us_sales_tax_codes() {
976        let mut config = TaxConfig::default();
977        config.jurisdictions.countries = vec!["US".into()];
978        config.jurisdictions.include_subnational = true;
979
980        let mut gen = TaxCodeGenerator::with_config(42, config);
981        let (jurisdictions, codes) = gen.generate();
982
983        // Should have federal + state jurisdictions
984        let federal = jurisdictions
985            .iter()
986            .find(|j| j.id == "JUR-US")
987            .expect("US federal jurisdiction");
988        assert_eq!(federal.jurisdiction_type, JurisdictionType::Federal);
989
990        let state_jurs: Vec<&TaxJurisdiction> = jurisdictions
991            .iter()
992            .filter(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::State)
993            .collect();
994        assert_eq!(
995            state_jurs.len(),
996            10,
997            "Should have 10 US state jurisdictions"
998        );
999
1000        // Check specific state rates
1001        let ca_code = codes
1002            .iter()
1003            .find(|c| c.code == "ST-CA")
1004            .expect("California sales tax code");
1005        assert_eq!(ca_code.rate, dec!(0.0725));
1006        assert_eq!(ca_code.tax_type, TaxType::SalesTax);
1007
1008        let ny_code = codes
1009            .iter()
1010            .find(|c| c.code == "ST-NY")
1011            .expect("New York sales tax code");
1012        assert_eq!(ny_code.rate, dec!(0.08));
1013
1014        let tx_code = codes
1015            .iter()
1016            .find(|c| c.code == "ST-TX")
1017            .expect("Texas sales tax code");
1018        assert_eq!(tx_code.rate, dec!(0.0625));
1019    }
1020
1021    #[test]
1022    fn test_eu_vat_codes() {
1023        let mut config = TaxConfig::default();
1024        config.jurisdictions.countries = vec!["DE".into(), "GB".into(), "FR".into()];
1025
1026        let mut gen = TaxCodeGenerator::with_config(42, config);
1027        let (_jurisdictions, codes) = gen.generate();
1028
1029        // DE: standard 19%, reduced 7%
1030        let de_std = codes
1031            .iter()
1032            .find(|c| c.code == "VAT-STD-DE")
1033            .expect("DE standard VAT code");
1034        assert_eq!(de_std.rate, dec!(0.19));
1035        assert_eq!(de_std.tax_type, TaxType::Vat);
1036        assert!(de_std.is_reverse_charge, "DE should have reverse charge");
1037
1038        let de_red = codes
1039            .iter()
1040            .find(|c| c.code == "VAT-RED-DE")
1041            .expect("DE reduced VAT code");
1042        assert_eq!(de_red.rate, dec!(0.07));
1043
1044        // GB: standard 20%, reduced 5%, zero rate
1045        let gb_std = codes
1046            .iter()
1047            .find(|c| c.code == "VAT-STD-GB")
1048            .expect("GB standard VAT code");
1049        assert_eq!(gb_std.rate, dec!(0.20));
1050        assert!(
1051            !gb_std.is_reverse_charge,
1052            "GB should NOT have reverse charge (not EU)"
1053        );
1054
1055        let gb_red = codes
1056            .iter()
1057            .find(|c| c.code == "VAT-RED-GB")
1058            .expect("GB reduced VAT code");
1059        assert_eq!(gb_red.rate, dec!(0.05));
1060
1061        let gb_zero = codes
1062            .iter()
1063            .find(|c| c.code == "VAT-ZERO-GB")
1064            .expect("GB zero-rate VAT code");
1065        assert_eq!(gb_zero.rate, dec!(0));
1066
1067        // FR: standard 20%, reduced 5.5%
1068        let fr_std = codes
1069            .iter()
1070            .find(|c| c.code == "VAT-STD-FR")
1071            .expect("FR standard VAT code");
1072        assert_eq!(fr_std.rate, dec!(0.20));
1073        assert!(fr_std.is_reverse_charge, "FR should have reverse charge");
1074
1075        let fr_red = codes
1076            .iter()
1077            .find(|c| c.code == "VAT-RED-FR")
1078            .expect("FR reduced VAT code");
1079        assert_eq!(fr_red.rate, dec!(0.055));
1080    }
1081
1082    #[test]
1083    fn test_deterministic() {
1084        let mut gen1 = TaxCodeGenerator::new(12345);
1085        let (jur1, codes1) = gen1.generate();
1086
1087        let mut gen2 = TaxCodeGenerator::new(12345);
1088        let (jur2, codes2) = gen2.generate();
1089
1090        assert_eq!(jur1.len(), jur2.len(), "Same number of jurisdictions");
1091        assert_eq!(codes1.len(), codes2.len(), "Same number of codes");
1092
1093        for (j1, j2) in jur1.iter().zip(jur2.iter()) {
1094            assert_eq!(j1.id, j2.id);
1095            assert_eq!(j1.name, j2.name);
1096            assert_eq!(j1.country_code, j2.country_code);
1097            assert_eq!(j1.jurisdiction_type, j2.jurisdiction_type);
1098            assert_eq!(j1.vat_registered, j2.vat_registered);
1099        }
1100
1101        for (c1, c2) in codes1.iter().zip(codes2.iter()) {
1102            assert_eq!(c1.id, c2.id);
1103            assert_eq!(c1.code, c2.code);
1104            assert_eq!(c1.rate, c2.rate);
1105            assert_eq!(c1.tax_type, c2.tax_type);
1106        }
1107    }
1108
1109    #[test]
1110    fn test_config_rate_override() {
1111        let mut config = TaxConfig::default();
1112        config.jurisdictions.countries = vec!["DE".into()];
1113        config.vat_gst.standard_rates.insert("DE".into(), 0.25);
1114
1115        let mut gen = TaxCodeGenerator::with_config(42, config);
1116        let (_jurisdictions, codes) = gen.generate();
1117
1118        let de_std = codes
1119            .iter()
1120            .find(|c| c.code == "VAT-STD-DE")
1121            .expect("DE standard VAT code");
1122        assert_eq!(
1123            de_std.rate,
1124            dec!(0.25),
1125            "Config override should replace built-in rate"
1126        );
1127    }
1128
1129    #[test]
1130    fn test_subnational_generation() {
1131        let mut config = TaxConfig::default();
1132        config.jurisdictions.countries = vec!["US".into(), "IN".into(), "CA".into()];
1133        config.jurisdictions.include_subnational = true;
1134
1135        let mut gen = TaxCodeGenerator::with_config(42, config);
1136        let (jurisdictions, codes) = gen.generate();
1137
1138        // US: 1 federal + 10 states
1139        let us_jurs: Vec<&TaxJurisdiction> = jurisdictions
1140            .iter()
1141            .filter(|j| j.country_code == "US")
1142            .collect();
1143        assert_eq!(us_jurs.len(), 11, "US: 1 federal + 10 states");
1144
1145        // IN: 1 federal + 10 states
1146        let in_jurs: Vec<&TaxJurisdiction> = jurisdictions
1147            .iter()
1148            .filter(|j| j.country_code == "IN")
1149            .collect();
1150        assert_eq!(in_jurs.len(), 11, "IN: 1 federal + 10 states");
1151
1152        // IN state jurisdictions should be VAT-registered
1153        let in_states: Vec<&TaxJurisdiction> = in_jurs
1154            .iter()
1155            .filter(|j| j.jurisdiction_type == JurisdictionType::State)
1156            .copied()
1157            .collect();
1158        assert!(
1159            in_states.iter().all(|j| j.vat_registered),
1160            "IN states should be VAT-registered"
1161        );
1162
1163        // India should have GST slab codes
1164        let in_slab_codes: Vec<&TaxCode> = codes
1165            .iter()
1166            .filter(|c| c.code.starts_with("GST-SLAB-"))
1167            .collect();
1168        assert_eq!(in_slab_codes.len(), 4, "India should have 4 GST slab codes");
1169
1170        // CA: 1 federal + 10 provinces
1171        let ca_jurs: Vec<&TaxJurisdiction> = jurisdictions
1172            .iter()
1173            .filter(|j| j.country_code == "CA")
1174            .collect();
1175        assert_eq!(ca_jurs.len(), 11, "CA: 1 federal + 10 provinces");
1176
1177        // CA should have HST codes per province
1178        let ca_hst_codes: Vec<&TaxCode> = codes
1179            .iter()
1180            .filter(|c| c.code.starts_with("HST-"))
1181            .collect();
1182        assert_eq!(
1183            ca_hst_codes.len(),
1184            10,
1185            "CA should have 10 provincial HST codes"
1186        );
1187
1188        // Ontario HST should be 13%
1189        let on_code = ca_hst_codes
1190            .iter()
1191            .find(|c| c.code == "HST-ON")
1192            .expect("Ontario HST code");
1193        assert_eq!(on_code.rate, dec!(0.13));
1194    }
1195
1196    #[test]
1197    fn test_nexus_states_filter() {
1198        let mut config = TaxConfig::default();
1199        config.jurisdictions.countries = vec!["US".into()];
1200        config.jurisdictions.include_subnational = true;
1201        config.sales_tax.nexus_states = vec!["CA".into(), "NY".into()];
1202
1203        let mut gen = TaxCodeGenerator::with_config(42, config);
1204        let (jurisdictions, codes) = gen.generate();
1205
1206        let state_jurs: Vec<&TaxJurisdiction> = jurisdictions
1207            .iter()
1208            .filter(|j| j.country_code == "US" && j.jurisdiction_type == JurisdictionType::State)
1209            .collect();
1210        assert_eq!(state_jurs.len(), 2, "Should only generate nexus states");
1211
1212        let state_codes: Vec<String> = state_jurs
1213            .iter()
1214            .filter_map(|j| j.region_code.clone())
1215            .collect();
1216        assert!(state_codes.contains(&"CA".to_string()));
1217        assert!(state_codes.contains(&"NY".to_string()));
1218
1219        // Sales tax codes should only be for CA and NY
1220        let sales_codes: Vec<&TaxCode> = codes
1221            .iter()
1222            .filter(|c| c.tax_type == TaxType::SalesTax)
1223            .collect();
1224        assert_eq!(sales_codes.len(), 2);
1225    }
1226
1227    #[test]
1228    fn test_vat_registered_flag() {
1229        let mut config = TaxConfig::default();
1230        config.jurisdictions.countries = vec!["DE".into(), "SG".into(), "US".into()];
1231
1232        let mut gen = TaxCodeGenerator::with_config(42, config);
1233        let (jurisdictions, _codes) = gen.generate();
1234
1235        let de_federal = jurisdictions
1236            .iter()
1237            .find(|j| j.id == "JUR-DE")
1238            .expect("DE federal");
1239        assert!(de_federal.vat_registered, "DE should be VAT-registered");
1240
1241        let sg_federal = jurisdictions
1242            .iter()
1243            .find(|j| j.id == "JUR-SG")
1244            .expect("SG federal");
1245        assert!(
1246            sg_federal.vat_registered,
1247            "SG should be VAT-registered (GST)"
1248        );
1249
1250        let us_federal = jurisdictions
1251            .iter()
1252            .find(|j| j.id == "JUR-US")
1253            .expect("US federal");
1254        assert!(
1255            !us_federal.vat_registered,
1256            "US should NOT be VAT-registered (sales tax)"
1257        );
1258    }
1259
1260    #[test]
1261    fn test_exempt_codes_generated() {
1262        let mut config = TaxConfig::default();
1263        config.jurisdictions.countries = vec!["DE".into()];
1264
1265        let mut gen = TaxCodeGenerator::with_config(42, config);
1266        let (_jurisdictions, codes) = gen.generate();
1267
1268        let exempt = codes
1269            .iter()
1270            .find(|c| c.code == "VAT-EX-DE")
1271            .expect("DE exempt code");
1272        assert!(exempt.is_exempt);
1273        assert_eq!(exempt.rate, dec!(0));
1274        assert_eq!(exempt.tax_amount(dec!(10000)), dec!(0));
1275    }
1276
1277    #[test]
1278    fn test_effective_dates() {
1279        let mut gen = TaxCodeGenerator::new(42);
1280        let (_jurisdictions, codes) = gen.generate();
1281
1282        let expected_date = NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
1283        for code in &codes {
1284            assert_eq!(
1285                code.effective_date, expected_date,
1286                "All codes should have effective date 2020-01-01, got {} for {}",
1287                code.effective_date, code.code
1288            );
1289            assert!(
1290                code.expiry_date.is_none(),
1291                "Codes should not have an expiry date"
1292            );
1293        }
1294    }
1295
1296    #[test]
1297    fn test_reduced_rate_override() {
1298        let mut config = TaxConfig::default();
1299        config.jurisdictions.countries = vec!["JP".into()];
1300        config.vat_gst.reduced_rates.insert("JP".into(), 0.03);
1301
1302        let mut gen = TaxCodeGenerator::with_config(42, config);
1303        let (_jurisdictions, codes) = gen.generate();
1304
1305        let jp_red = codes
1306            .iter()
1307            .find(|c| c.code == "GST-RED-JP")
1308            .expect("JP reduced GST code");
1309        assert_eq!(
1310            jp_red.rate,
1311            dec!(0.03),
1312            "Reduced rate override should apply"
1313        );
1314    }
1315
1316    #[test]
1317    fn test_germany_subnational() {
1318        let mut config = TaxConfig::default();
1319        config.jurisdictions.countries = vec!["DE".into()];
1320        config.jurisdictions.include_subnational = true;
1321
1322        let mut gen = TaxCodeGenerator::with_config(42, config);
1323        let (jurisdictions, _codes) = gen.generate();
1324
1325        let de_states: Vec<&TaxJurisdiction> = jurisdictions
1326            .iter()
1327            .filter(|j| j.country_code == "DE" && j.jurisdiction_type == JurisdictionType::State)
1328            .collect();
1329        assert_eq!(de_states.len(), 16, "Germany should have 16 Bundeslaender");
1330
1331        // All states should reference the federal parent
1332        for state in &de_states {
1333            assert_eq!(
1334                state.parent_jurisdiction_id,
1335                Some("JUR-DE".to_string()),
1336                "State {} should have federal parent",
1337                state.name
1338            );
1339            assert!(state.vat_registered);
1340        }
1341    }
1342
1343    #[test]
1344    fn test_format_rate_pct() {
1345        assert_eq!(format_rate_pct(dec!(0.19)), "19%");
1346        assert_eq!(format_rate_pct(dec!(0.055)), "5.5%");
1347        assert_eq!(format_rate_pct(dec!(0.0725)), "7.25%");
1348        assert_eq!(format_rate_pct(dec!(0)), "0%");
1349    }
1350}