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