Skip to main content

datasynth_generators/
coa_generator.rs

1//! Chart of Accounts generator.
2
3use tracing::debug;
4
5use datasynth_core::accounts::{
6    cash_accounts, control_accounts, equity_accounts, expense_accounts, liability_accounts,
7    revenue_accounts, suspense_accounts, tax_accounts,
8};
9use datasynth_core::models::*;
10use datasynth_core::pcg_loader;
11use datasynth_core::skr_loader;
12use datasynth_core::traits::Generator;
13use datasynth_core::utils::seeded_rng;
14use rand_chacha::ChaCha8Rng;
15
16/// Accounting framework for CoA generation.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum CoAFramework {
19    /// US GAAP (4-digit accounts)
20    #[default]
21    UsGaap,
22    /// French GAAP / Plan Comptable Général (6-digit accounts)
23    FrenchPcg,
24    /// German GAAP / SKR04 (4-digit accounts)
25    GermanSkr04,
26}
27
28/// Generator for Chart of Accounts.
29pub struct ChartOfAccountsGenerator {
30    rng: ChaCha8Rng,
31    seed: u64,
32    complexity: CoAComplexity,
33    industry: IndustrySector,
34    count: u64,
35    /// Accounting framework for CoA generation.
36    coa_framework: CoAFramework,
37}
38
39impl ChartOfAccountsGenerator {
40    /// Create a new CoA generator.
41    pub fn new(complexity: CoAComplexity, industry: IndustrySector, seed: u64) -> Self {
42        Self {
43            rng: seeded_rng(seed, 0),
44            seed,
45            complexity,
46            industry,
47            count: 0,
48            coa_framework: CoAFramework::UsGaap,
49        }
50    }
51
52    /// Use French GAAP (Plan Comptable Général) account structure.
53    ///
54    /// Deprecated: use `with_coa_framework(CoAFramework::FrenchPcg)` instead.
55    pub fn with_french_pcg(mut self, use_pcg: bool) -> Self {
56        if use_pcg {
57            self.coa_framework = CoAFramework::FrenchPcg;
58        }
59        self
60    }
61
62    /// Set the accounting framework for CoA generation.
63    pub fn with_coa_framework(mut self, framework: CoAFramework) -> Self {
64        self.coa_framework = framework;
65        self
66    }
67
68    /// Generate a complete chart of accounts.
69    pub fn generate(&mut self) -> ChartOfAccounts {
70        debug!(
71            complexity = ?self.complexity,
72            industry = ?self.industry,
73            seed = self.seed,
74            framework = ?self.coa_framework,
75            "Generating chart of accounts"
76        );
77
78        self.count += 1;
79        match self.coa_framework {
80            CoAFramework::UsGaap => self.generate_default(),
81            CoAFramework::FrenchPcg => self.generate_pcg(),
82            CoAFramework::GermanSkr04 => self.generate_skr(),
83        }
84    }
85
86    /// Generate default (US-style) chart of accounts.
87    fn generate_default(&mut self) -> ChartOfAccounts {
88        let target_count = self.complexity.target_count();
89        let mut coa = ChartOfAccounts::new(
90            format!("COA_{:?}_{}", self.industry, self.complexity.target_count()),
91            format!("{:?} Chart of Accounts", self.industry),
92            "US".to_string(),
93            self.industry,
94            self.complexity,
95        );
96
97        // Seed canonical accounts first so other generators can find them
98        Self::seed_canonical_accounts(&mut coa);
99
100        // Generate additional accounts by type
101        self.generate_asset_accounts(&mut coa, target_count / 5);
102        self.generate_liability_accounts(&mut coa, target_count / 6);
103        self.generate_equity_accounts(&mut coa, target_count / 10);
104        self.generate_revenue_accounts(&mut coa, target_count / 5);
105        self.generate_expense_accounts(&mut coa, target_count / 4);
106        self.generate_suspense_accounts(&mut coa);
107        coa
108    }
109
110    /// Generate Plan Comptable Général (French GAAP) chart of accounts.
111    /// Uses the comprehensive PCG 2024 structure from [arrhes/PCG](https://github.com/arrhes/PCG) when available.
112    fn generate_pcg(&mut self) -> ChartOfAccounts {
113        match pcg_loader::build_chart_of_accounts_from_pcg_2024(self.complexity, self.industry) {
114            Ok(coa) => coa,
115            Err(_) => self.generate_pcg_fallback(),
116        }
117    }
118
119    /// Generate SKR04 (German GAAP) chart of accounts.
120    /// Uses the embedded SKR04 2024 structure when available.
121    fn generate_skr(&mut self) -> ChartOfAccounts {
122        match skr_loader::build_chart_of_accounts_from_skr04(self.complexity, self.industry) {
123            Ok(coa) => coa,
124            Err(_) => self.generate_skr_fallback(),
125        }
126    }
127
128    /// Fallback simplified SKR04 when the embedded 2024 JSON cannot be loaded.
129    fn generate_skr_fallback(&mut self) -> ChartOfAccounts {
130        use datasynth_core::skr;
131        let target_count = self.complexity.target_count();
132        let mut coa = ChartOfAccounts::new(
133            format!("COA_SKR04_{:?}_{}", self.industry, target_count),
134            format!("Standardkontenrahmen 04 – {:?}", self.industry),
135            "DE".to_string(),
136            self.industry,
137            self.complexity,
138        );
139        coa.account_format = "####".to_string();
140
141        // Seed key SKR04 accounts
142        let key_accounts = [
143            (
144                skr::control_accounts::AR_CONTROL,
145                "Forderungen aus L+L",
146                AccountType::Asset,
147                AccountSubType::AccountsReceivable,
148            ),
149            (
150                skr::control_accounts::AP_CONTROL,
151                "Verbindlichkeiten aus L+L",
152                AccountType::Liability,
153                AccountSubType::AccountsPayable,
154            ),
155            (
156                skr::control_accounts::INVENTORY,
157                "Vorräte",
158                AccountType::Asset,
159                AccountSubType::Inventory,
160            ),
161            (
162                skr::control_accounts::FIXED_ASSETS,
163                "Sachanlagen",
164                AccountType::Asset,
165                AccountSubType::FixedAssets,
166            ),
167            (
168                skr::cash_accounts::OPERATING_CASH,
169                "Bank",
170                AccountType::Asset,
171                AccountSubType::Cash,
172            ),
173            (
174                skr::cash_accounts::PETTY_CASH,
175                "Kasse",
176                AccountType::Asset,
177                AccountSubType::Cash,
178            ),
179            (
180                skr::equity_accounts::COMMON_STOCK,
181                "Gezeichnetes Kapital",
182                AccountType::Equity,
183                AccountSubType::CommonStock,
184            ),
185            (
186                skr::equity_accounts::RETAINED_EARNINGS,
187                "Gewinnvortrag",
188                AccountType::Equity,
189                AccountSubType::RetainedEarnings,
190            ),
191            (
192                skr::revenue_accounts::PRODUCT_REVENUE,
193                "Umsatzerlöse",
194                AccountType::Revenue,
195                AccountSubType::ProductRevenue,
196            ),
197            (
198                skr::revenue_accounts::SERVICE_REVENUE,
199                "Erlöse Leistungen",
200                AccountType::Revenue,
201                AccountSubType::ServiceRevenue,
202            ),
203            (
204                skr::expense_accounts::COGS,
205                "Materialaufwand",
206                AccountType::Expense,
207                AccountSubType::CostOfGoodsSold,
208            ),
209            (
210                skr::expense_accounts::SALARIES_WAGES,
211                "Löhne und Gehälter",
212                AccountType::Expense,
213                AccountSubType::OperatingExpenses,
214            ),
215            (
216                skr::expense_accounts::DEPRECIATION,
217                "Abschreibungen",
218                AccountType::Expense,
219                AccountSubType::DepreciationExpense,
220            ),
221            (
222                skr::expense_accounts::RENT,
223                "Miete",
224                AccountType::Expense,
225                AccountSubType::OperatingExpenses,
226            ),
227        ];
228
229        for (code, name, acc_type, sub_type) in key_accounts {
230            let mut account =
231                GLAccount::new(code.to_string(), name.to_string(), acc_type, sub_type);
232            account.requires_cost_center = acc_type == AccountType::Expense;
233            coa.add_account(account);
234        }
235
236        // Add additional accounts to reach target count
237        let mut num = 4100u32;
238        while coa.account_count() < target_count && num < 9900 {
239            let code = format!("{:04}", num);
240            if coa.get_account(&code).is_none() {
241                let class = (num / 1000) as u8;
242                let (acc_type, sub_type) = match class {
243                    0..=1 => (AccountType::Asset, AccountSubType::OtherAssets),
244                    2 => (AccountType::Equity, AccountSubType::RetainedEarnings),
245                    3 => (AccountType::Liability, AccountSubType::OtherLiabilities),
246                    4 => (AccountType::Revenue, AccountSubType::OtherIncome),
247                    5 => (AccountType::Expense, AccountSubType::CostOfGoodsSold),
248                    6 => (AccountType::Expense, AccountSubType::OperatingExpenses),
249                    7 => (AccountType::Expense, AccountSubType::InterestExpense),
250                    _ => (AccountType::Asset, AccountSubType::SuspenseClearing),
251                };
252                coa.add_account(GLAccount::new(
253                    code,
254                    format!("Konto {}", num),
255                    acc_type,
256                    sub_type,
257                ));
258            }
259            num += 10;
260        }
261
262        coa
263    }
264
265    /// Fallback simplified PCG when the embedded 2024 JSON cannot be loaded.
266    fn generate_pcg_fallback(&mut self) -> ChartOfAccounts {
267        let target_count = self.complexity.target_count();
268        let mut coa = ChartOfAccounts::new(
269            format!("COA_PCG_{:?}_{}", self.industry, target_count),
270            format!("Plan Comptable Général – {:?}", self.industry),
271            "FR".to_string(),
272            self.industry,
273            self.complexity,
274        );
275        coa.account_format = "######".to_string();
276
277        self.generate_pcg_class_1(&mut coa, target_count / 10);
278        self.generate_pcg_class_2(&mut coa, target_count / 6);
279        self.generate_pcg_class_3(&mut coa, target_count / 8);
280        self.generate_pcg_class_4(&mut coa, target_count / 5);
281        self.generate_pcg_class_5(&mut coa, target_count / 12);
282        self.generate_pcg_class_6(&mut coa, target_count / 4);
283        self.generate_pcg_class_7(&mut coa, target_count / 5);
284        self.generate_pcg_class_8(&mut coa);
285
286        coa
287    }
288
289    fn generate_pcg_class_1(&mut self, coa: &mut ChartOfAccounts, count: usize) {
290        let items = [
291            (
292                101,
293                "Capital",
294                AccountType::Equity,
295                AccountSubType::CommonStock,
296            ),
297            (
298                129,
299                "Résultat",
300                AccountType::Equity,
301                AccountSubType::RetainedEarnings,
302            ),
303            (
304                164,
305                "Emprunts",
306                AccountType::Liability,
307                AccountSubType::LongTermDebt,
308            ),
309            (
310                151,
311                "Provisions pour risques",
312                AccountType::Liability,
313                AccountSubType::AccruedLiabilities,
314            ),
315        ];
316        for (base, name, acc_type, sub_type) in items {
317            for i in 0..count.max(1) {
318                let num = base * 1000 + (i as u32 % 100);
319                coa.add_account(GLAccount::new(
320                    format!("{:06}", num),
321                    format!("{} {}", name, i + 1),
322                    acc_type,
323                    sub_type,
324                ));
325            }
326        }
327    }
328
329    fn generate_pcg_class_2(&mut self, coa: &mut ChartOfAccounts, count: usize) {
330        for i in 0..count.max(1) {
331            let num = 215000 + (i as u32 % 100);
332            coa.add_account(GLAccount::new(
333                format!("{:06}", num),
334                format!("Immobilisations {}", i + 1),
335                AccountType::Asset,
336                AccountSubType::FixedAssets,
337            ));
338        }
339        for i in 0..(count / 2).max(1) {
340            let num = 281000 + (i as u32 % 100);
341            coa.add_account(GLAccount::new(
342                format!("{:06}", num),
343                format!("Amortissements {}", i + 1),
344                AccountType::Asset,
345                AccountSubType::AccumulatedDepreciation,
346            ));
347        }
348    }
349
350    fn generate_pcg_class_3(&mut self, coa: &mut ChartOfAccounts, count: usize) {
351        for i in 0..count.max(1) {
352            let num = 310000 + (i as u32 % 1000);
353            coa.add_account(GLAccount::new(
354                format!("{:06}", num),
355                format!("Stocks {}", i + 1),
356                AccountType::Asset,
357                AccountSubType::Inventory,
358            ));
359        }
360    }
361
362    fn generate_pcg_class_4(&mut self, coa: &mut ChartOfAccounts, count: usize) {
363        for i in 0..count.max(1) {
364            let num = 411000 + (i as u32 % 1000);
365            coa.add_account(GLAccount::new(
366                format!("{:06}", num),
367                format!("Clients {}", i + 1),
368                AccountType::Asset,
369                AccountSubType::AccountsReceivable,
370            ));
371        }
372        for i in 0..count.max(1) {
373            let num = 401000 + (i as u32 % 1000);
374            coa.add_account(GLAccount::new(
375                format!("{:06}", num),
376                format!("Fournisseurs {}", i + 1),
377                AccountType::Liability,
378                AccountSubType::AccountsPayable,
379            ));
380        }
381        let clearing = GLAccount::new(
382            "408000".to_string(),
383            "Fournisseurs – non encore reçus".to_string(),
384            AccountType::Liability,
385            AccountSubType::GoodsReceivedClearing,
386        );
387        coa.add_account(clearing);
388    }
389
390    fn generate_pcg_class_5(&mut self, coa: &mut ChartOfAccounts, count: usize) {
391        let bases = [
392            (512, "Banque"),
393            (530, "Caisse"),
394            (511, "Valeurs à l'encaissement"),
395        ];
396        for (base, name) in bases {
397            for i in 0..(count / 3).max(1) {
398                let num = base * 1000 + (i as u32 % 100);
399                coa.add_account(GLAccount::new(
400                    format!("{:06}", num),
401                    format!("{} {}", name, i + 1),
402                    AccountType::Asset,
403                    AccountSubType::Cash,
404                ));
405            }
406        }
407    }
408
409    fn generate_pcg_class_6(&mut self, coa: &mut ChartOfAccounts, count: usize) {
410        let bases = [
411            (603, "Achats"),
412            (641, "Rémunérations"),
413            (681, "DAP"),
414            (613, "Loyers"),
415            (661, "Charges financières"),
416        ];
417        for (base, name) in bases {
418            for i in 0..(count / 5).max(1) {
419                let num = base * 1000 + (i as u32 % 100);
420                let mut account = GLAccount::new(
421                    format!("{:06}", num),
422                    format!("{} {}", name, i + 1),
423                    AccountType::Expense,
424                    AccountSubType::OperatingExpenses,
425                );
426                account.requires_cost_center = true;
427                coa.add_account(account);
428            }
429        }
430    }
431
432    fn generate_pcg_class_7(&mut self, coa: &mut ChartOfAccounts, count: usize) {
433        let bases = [
434            (701, "Ventes"),
435            (706, "Prestations"),
436            (758, "Produits divers"),
437        ];
438        for (base, name) in bases {
439            for i in 0..(count / 3).max(1) {
440                let num = base * 1000 + (i as u32 % 100);
441                coa.add_account(GLAccount::new(
442                    format!("{:06}", num),
443                    format!("{} {}", name, i + 1),
444                    AccountType::Revenue,
445                    AccountSubType::ProductRevenue,
446                ));
447            }
448        }
449    }
450
451    fn generate_pcg_class_8(&mut self, coa: &mut ChartOfAccounts) {
452        coa.add_account(GLAccount::new(
453            "808000".to_string(),
454            "Comptes spéciaux".to_string(),
455            AccountType::Asset,
456            AccountSubType::SuspenseClearing,
457        ));
458    }
459
460    /// Insert all canonical accounts from `datasynth_core::accounts` into the CoA.
461    ///
462    /// These are the well-known account numbers (4-digit, 1000-9300 range) that
463    /// other generators reference. They are added before auto-generated accounts
464    /// (which start at 100000+) so there are no collisions.
465    fn seed_canonical_accounts(coa: &mut ChartOfAccounts) {
466        // --- Cash accounts (1000-series, Asset / Cash) ---
467        coa.add_account(GLAccount::new(
468            cash_accounts::OPERATING_CASH.to_string(),
469            "Operating Cash".to_string(),
470            AccountType::Asset,
471            AccountSubType::Cash,
472        ));
473        coa.add_account(GLAccount::new(
474            cash_accounts::BANK_ACCOUNT.to_string(),
475            "Bank Account".to_string(),
476            AccountType::Asset,
477            AccountSubType::Cash,
478        ));
479        coa.add_account(GLAccount::new(
480            cash_accounts::PETTY_CASH.to_string(),
481            "Petty Cash".to_string(),
482            AccountType::Asset,
483            AccountSubType::Cash,
484        ));
485        coa.add_account(GLAccount::new(
486            cash_accounts::WIRE_CLEARING.to_string(),
487            "Wire Transfer Clearing".to_string(),
488            AccountType::Asset,
489            AccountSubType::BankClearing,
490        ));
491
492        // --- Control accounts (Asset side) ---
493        {
494            let mut acct = GLAccount::new(
495                control_accounts::AR_CONTROL.to_string(),
496                "Accounts Receivable Control".to_string(),
497                AccountType::Asset,
498                AccountSubType::AccountsReceivable,
499            );
500            acct.is_control_account = true;
501            coa.add_account(acct);
502        }
503        {
504            let mut acct = GLAccount::new(
505                control_accounts::IC_AR_CLEARING.to_string(),
506                "Intercompany AR Clearing".to_string(),
507                AccountType::Asset,
508                AccountSubType::AccountsReceivable,
509            );
510            acct.is_control_account = true;
511            coa.add_account(acct);
512        }
513        coa.add_account(GLAccount::new(
514            control_accounts::INVENTORY.to_string(),
515            "Inventory".to_string(),
516            AccountType::Asset,
517            AccountSubType::Inventory,
518        ));
519        coa.add_account(GLAccount::new(
520            control_accounts::FIXED_ASSETS.to_string(),
521            "Fixed Assets".to_string(),
522            AccountType::Asset,
523            AccountSubType::FixedAssets,
524        ));
525        coa.add_account(GLAccount::new(
526            control_accounts::ACCUMULATED_DEPRECIATION.to_string(),
527            "Accumulated Depreciation".to_string(),
528            AccountType::Asset,
529            AccountSubType::AccumulatedDepreciation,
530        ));
531
532        // --- Tax asset accounts ---
533        coa.add_account(GLAccount::new(
534            tax_accounts::INPUT_VAT.to_string(),
535            "Input VAT".to_string(),
536            AccountType::Asset,
537            AccountSubType::OtherReceivables,
538        ));
539        coa.add_account(GLAccount::new(
540            tax_accounts::DEFERRED_TAX_ASSET.to_string(),
541            "Deferred Tax Asset".to_string(),
542            AccountType::Asset,
543            AccountSubType::OtherAssets,
544        ));
545
546        // --- Liability / Control accounts (2000-series) ---
547        {
548            let mut acct = GLAccount::new(
549                control_accounts::AP_CONTROL.to_string(),
550                "Accounts Payable Control".to_string(),
551                AccountType::Liability,
552                AccountSubType::AccountsPayable,
553            );
554            acct.is_control_account = true;
555            coa.add_account(acct);
556        }
557        {
558            let mut acct = GLAccount::new(
559                control_accounts::IC_AP_CLEARING.to_string(),
560                "Intercompany AP Clearing".to_string(),
561                AccountType::Liability,
562                AccountSubType::AccountsPayable,
563            );
564            acct.is_control_account = true;
565            coa.add_account(acct);
566        }
567        coa.add_account(GLAccount::new(
568            tax_accounts::SALES_TAX_PAYABLE.to_string(),
569            "Sales Tax Payable".to_string(),
570            AccountType::Liability,
571            AccountSubType::TaxLiabilities,
572        ));
573        coa.add_account(GLAccount::new(
574            tax_accounts::VAT_PAYABLE.to_string(),
575            "VAT Payable".to_string(),
576            AccountType::Liability,
577            AccountSubType::TaxLiabilities,
578        ));
579        coa.add_account(GLAccount::new(
580            tax_accounts::WITHHOLDING_TAX_PAYABLE.to_string(),
581            "Withholding Tax Payable".to_string(),
582            AccountType::Liability,
583            AccountSubType::TaxLiabilities,
584        ));
585        coa.add_account(GLAccount::new(
586            liability_accounts::ACCRUED_EXPENSES.to_string(),
587            "Accrued Expenses".to_string(),
588            AccountType::Liability,
589            AccountSubType::AccruedLiabilities,
590        ));
591        coa.add_account(GLAccount::new(
592            liability_accounts::ACCRUED_SALARIES.to_string(),
593            "Accrued Salaries".to_string(),
594            AccountType::Liability,
595            AccountSubType::AccruedLiabilities,
596        ));
597        coa.add_account(GLAccount::new(
598            liability_accounts::ACCRUED_BENEFITS.to_string(),
599            "Accrued Benefits".to_string(),
600            AccountType::Liability,
601            AccountSubType::AccruedLiabilities,
602        ));
603        coa.add_account(GLAccount::new(
604            liability_accounts::UNEARNED_REVENUE.to_string(),
605            "Unearned Revenue".to_string(),
606            AccountType::Liability,
607            AccountSubType::DeferredRevenue,
608        ));
609        coa.add_account(GLAccount::new(
610            liability_accounts::SHORT_TERM_DEBT.to_string(),
611            "Short-Term Debt".to_string(),
612            AccountType::Liability,
613            AccountSubType::ShortTermDebt,
614        ));
615        coa.add_account(GLAccount::new(
616            tax_accounts::DEFERRED_TAX_LIABILITY.to_string(),
617            "Deferred Tax Liability".to_string(),
618            AccountType::Liability,
619            AccountSubType::TaxLiabilities,
620        ));
621        coa.add_account(GLAccount::new(
622            liability_accounts::LONG_TERM_DEBT.to_string(),
623            "Long-Term Debt".to_string(),
624            AccountType::Liability,
625            AccountSubType::LongTermDebt,
626        ));
627        coa.add_account(GLAccount::new(
628            liability_accounts::IC_PAYABLE.to_string(),
629            "Intercompany Payable".to_string(),
630            AccountType::Liability,
631            AccountSubType::OtherLiabilities,
632        ));
633        {
634            let mut acct = GLAccount::new(
635                control_accounts::GR_IR_CLEARING.to_string(),
636                "GR/IR Clearing".to_string(),
637                AccountType::Liability,
638                AccountSubType::GoodsReceivedClearing,
639            );
640            acct.is_suspense_account = true;
641            coa.add_account(acct);
642        }
643
644        // --- Equity accounts (3000-series) ---
645        coa.add_account(GLAccount::new(
646            equity_accounts::COMMON_STOCK.to_string(),
647            "Common Stock".to_string(),
648            AccountType::Equity,
649            AccountSubType::CommonStock,
650        ));
651        coa.add_account(GLAccount::new(
652            equity_accounts::APIC.to_string(),
653            "Additional Paid-In Capital".to_string(),
654            AccountType::Equity,
655            AccountSubType::AdditionalPaidInCapital,
656        ));
657        coa.add_account(GLAccount::new(
658            equity_accounts::RETAINED_EARNINGS.to_string(),
659            "Retained Earnings".to_string(),
660            AccountType::Equity,
661            AccountSubType::RetainedEarnings,
662        ));
663        coa.add_account(GLAccount::new(
664            equity_accounts::CURRENT_YEAR_EARNINGS.to_string(),
665            "Current Year Earnings".to_string(),
666            AccountType::Equity,
667            AccountSubType::NetIncome,
668        ));
669        coa.add_account(GLAccount::new(
670            equity_accounts::TREASURY_STOCK.to_string(),
671            "Treasury Stock".to_string(),
672            AccountType::Equity,
673            AccountSubType::TreasuryStock,
674        ));
675        coa.add_account(GLAccount::new(
676            equity_accounts::CTA.to_string(),
677            "Currency Translation Adjustment".to_string(),
678            AccountType::Equity,
679            AccountSubType::OtherComprehensiveIncome,
680        ));
681
682        // --- Revenue accounts (4000-series) ---
683        coa.add_account(GLAccount::new(
684            revenue_accounts::PRODUCT_REVENUE.to_string(),
685            "Product Revenue".to_string(),
686            AccountType::Revenue,
687            AccountSubType::ProductRevenue,
688        ));
689        coa.add_account(GLAccount::new(
690            revenue_accounts::SALES_DISCOUNTS.to_string(),
691            "Sales Discounts".to_string(),
692            AccountType::Revenue,
693            AccountSubType::ProductRevenue,
694        ));
695        coa.add_account(GLAccount::new(
696            revenue_accounts::SALES_RETURNS.to_string(),
697            "Sales Returns and Allowances".to_string(),
698            AccountType::Revenue,
699            AccountSubType::ProductRevenue,
700        ));
701        coa.add_account(GLAccount::new(
702            revenue_accounts::SERVICE_REVENUE.to_string(),
703            "Service Revenue".to_string(),
704            AccountType::Revenue,
705            AccountSubType::ServiceRevenue,
706        ));
707        coa.add_account(GLAccount::new(
708            revenue_accounts::IC_REVENUE.to_string(),
709            "Intercompany Revenue".to_string(),
710            AccountType::Revenue,
711            AccountSubType::OtherIncome,
712        ));
713        coa.add_account(GLAccount::new(
714            revenue_accounts::OTHER_REVENUE.to_string(),
715            "Other Revenue".to_string(),
716            AccountType::Revenue,
717            AccountSubType::OtherIncome,
718        ));
719
720        // --- Expense accounts (5000-7xxx series) ---
721        {
722            let mut acct = GLAccount::new(
723                expense_accounts::COGS.to_string(),
724                "Cost of Goods Sold".to_string(),
725                AccountType::Expense,
726                AccountSubType::CostOfGoodsSold,
727            );
728            acct.requires_cost_center = true;
729            coa.add_account(acct);
730        }
731        {
732            let mut acct = GLAccount::new(
733                expense_accounts::RAW_MATERIALS.to_string(),
734                "Raw Materials".to_string(),
735                AccountType::Expense,
736                AccountSubType::CostOfGoodsSold,
737            );
738            acct.requires_cost_center = true;
739            coa.add_account(acct);
740        }
741        {
742            let mut acct = GLAccount::new(
743                expense_accounts::DIRECT_LABOR.to_string(),
744                "Direct Labor".to_string(),
745                AccountType::Expense,
746                AccountSubType::CostOfGoodsSold,
747            );
748            acct.requires_cost_center = true;
749            coa.add_account(acct);
750        }
751        {
752            let mut acct = GLAccount::new(
753                expense_accounts::MANUFACTURING_OVERHEAD.to_string(),
754                "Manufacturing Overhead".to_string(),
755                AccountType::Expense,
756                AccountSubType::CostOfGoodsSold,
757            );
758            acct.requires_cost_center = true;
759            coa.add_account(acct);
760        }
761        {
762            let mut acct = GLAccount::new(
763                expense_accounts::DEPRECIATION.to_string(),
764                "Depreciation Expense".to_string(),
765                AccountType::Expense,
766                AccountSubType::DepreciationExpense,
767            );
768            acct.requires_cost_center = true;
769            coa.add_account(acct);
770        }
771        {
772            let mut acct = GLAccount::new(
773                expense_accounts::SALARIES_WAGES.to_string(),
774                "Salaries and Wages".to_string(),
775                AccountType::Expense,
776                AccountSubType::OperatingExpenses,
777            );
778            acct.requires_cost_center = true;
779            coa.add_account(acct);
780        }
781        {
782            let mut acct = GLAccount::new(
783                expense_accounts::BENEFITS.to_string(),
784                "Benefits Expense".to_string(),
785                AccountType::Expense,
786                AccountSubType::OperatingExpenses,
787            );
788            acct.requires_cost_center = true;
789            coa.add_account(acct);
790        }
791        {
792            let mut acct = GLAccount::new(
793                expense_accounts::RENT.to_string(),
794                "Rent Expense".to_string(),
795                AccountType::Expense,
796                AccountSubType::OperatingExpenses,
797            );
798            acct.requires_cost_center = true;
799            coa.add_account(acct);
800        }
801        {
802            let mut acct = GLAccount::new(
803                expense_accounts::UTILITIES.to_string(),
804                "Utilities Expense".to_string(),
805                AccountType::Expense,
806                AccountSubType::OperatingExpenses,
807            );
808            acct.requires_cost_center = true;
809            coa.add_account(acct);
810        }
811        {
812            let mut acct = GLAccount::new(
813                expense_accounts::OFFICE_SUPPLIES.to_string(),
814                "Office Supplies".to_string(),
815                AccountType::Expense,
816                AccountSubType::AdministrativeExpenses,
817            );
818            acct.requires_cost_center = true;
819            coa.add_account(acct);
820        }
821        {
822            let mut acct = GLAccount::new(
823                expense_accounts::TRAVEL_ENTERTAINMENT.to_string(),
824                "Travel and Entertainment".to_string(),
825                AccountType::Expense,
826                AccountSubType::SellingExpenses,
827            );
828            acct.requires_cost_center = true;
829            coa.add_account(acct);
830        }
831        {
832            let mut acct = GLAccount::new(
833                expense_accounts::PROFESSIONAL_FEES.to_string(),
834                "Professional Fees".to_string(),
835                AccountType::Expense,
836                AccountSubType::AdministrativeExpenses,
837            );
838            acct.requires_cost_center = true;
839            coa.add_account(acct);
840        }
841        {
842            let mut acct = GLAccount::new(
843                expense_accounts::INSURANCE.to_string(),
844                "Insurance Expense".to_string(),
845                AccountType::Expense,
846                AccountSubType::OperatingExpenses,
847            );
848            acct.requires_cost_center = true;
849            coa.add_account(acct);
850        }
851        {
852            let mut acct = GLAccount::new(
853                expense_accounts::BAD_DEBT.to_string(),
854                "Bad Debt Expense".to_string(),
855                AccountType::Expense,
856                AccountSubType::OperatingExpenses,
857            );
858            acct.requires_cost_center = true;
859            coa.add_account(acct);
860        }
861        {
862            let mut acct = GLAccount::new(
863                expense_accounts::INTEREST_EXPENSE.to_string(),
864                "Interest Expense".to_string(),
865                AccountType::Expense,
866                AccountSubType::InterestExpense,
867            );
868            acct.requires_cost_center = true;
869            coa.add_account(acct);
870        }
871        {
872            let mut acct = GLAccount::new(
873                expense_accounts::PURCHASE_DISCOUNTS.to_string(),
874                "Purchase Discounts".to_string(),
875                AccountType::Expense,
876                AccountSubType::OtherExpenses,
877            );
878            acct.requires_cost_center = true;
879            coa.add_account(acct);
880        }
881        {
882            let mut acct = GLAccount::new(
883                expense_accounts::FX_GAIN_LOSS.to_string(),
884                "FX Gain/Loss".to_string(),
885                AccountType::Expense,
886                AccountSubType::ForeignExchangeLoss,
887            );
888            acct.requires_cost_center = true;
889            coa.add_account(acct);
890        }
891
892        // --- Tax expense (8000-series) ---
893        {
894            let mut acct = GLAccount::new(
895                tax_accounts::TAX_EXPENSE.to_string(),
896                "Tax Expense".to_string(),
897                AccountType::Expense,
898                AccountSubType::TaxExpense,
899            );
900            acct.requires_cost_center = true;
901            coa.add_account(acct);
902        }
903
904        // --- Suspense / Clearing accounts (9000-series) ---
905        {
906            let mut acct = GLAccount::new(
907                suspense_accounts::GENERAL_SUSPENSE.to_string(),
908                "General Suspense".to_string(),
909                AccountType::Asset,
910                AccountSubType::SuspenseClearing,
911            );
912            acct.is_suspense_account = true;
913            coa.add_account(acct);
914        }
915        {
916            let mut acct = GLAccount::new(
917                suspense_accounts::PAYROLL_CLEARING.to_string(),
918                "Payroll Clearing".to_string(),
919                AccountType::Asset,
920                AccountSubType::SuspenseClearing,
921            );
922            acct.is_suspense_account = true;
923            coa.add_account(acct);
924        }
925        {
926            let mut acct = GLAccount::new(
927                suspense_accounts::BANK_RECONCILIATION_SUSPENSE.to_string(),
928                "Bank Reconciliation Suspense".to_string(),
929                AccountType::Asset,
930                AccountSubType::BankClearing,
931            );
932            acct.is_suspense_account = true;
933            coa.add_account(acct);
934        }
935        {
936            let mut acct = GLAccount::new(
937                suspense_accounts::IC_ELIMINATION_SUSPENSE.to_string(),
938                "IC Elimination Suspense".to_string(),
939                AccountType::Asset,
940                AccountSubType::IntercompanyClearing,
941            );
942            acct.is_suspense_account = true;
943            coa.add_account(acct);
944        }
945    }
946
947    fn generate_asset_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
948        let sub_types = vec![
949            (AccountSubType::Cash, "Cash", 0.15),
950            (
951                AccountSubType::AccountsReceivable,
952                "Accounts Receivable",
953                0.20,
954            ),
955            (AccountSubType::Inventory, "Inventory", 0.15),
956            (AccountSubType::PrepaidExpenses, "Prepaid Expenses", 0.10),
957            (AccountSubType::FixedAssets, "Fixed Assets", 0.25),
958            (
959                AccountSubType::AccumulatedDepreciation,
960                "Accumulated Depreciation",
961                0.10,
962            ),
963            (AccountSubType::OtherAssets, "Other Assets", 0.05),
964        ];
965
966        let mut account_num = 100000u32;
967        for (sub_type, name_prefix, weight) in sub_types {
968            let sub_count = ((count as f64) * weight).round() as usize;
969            for i in 0..sub_count.max(1) {
970                let account = GLAccount::new(
971                    format!("{}", account_num),
972                    format!("{} {}", name_prefix, i + 1),
973                    AccountType::Asset,
974                    sub_type,
975                );
976                coa.add_account(account);
977                account_num += 10;
978            }
979        }
980    }
981
982    fn generate_liability_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
983        let sub_types = vec![
984            (AccountSubType::AccountsPayable, "Accounts Payable", 0.25),
985            (
986                AccountSubType::AccruedLiabilities,
987                "Accrued Liabilities",
988                0.20,
989            ),
990            (AccountSubType::ShortTermDebt, "Short-Term Debt", 0.15),
991            (AccountSubType::LongTermDebt, "Long-Term Debt", 0.15),
992            (AccountSubType::DeferredRevenue, "Deferred Revenue", 0.15),
993            (AccountSubType::TaxLiabilities, "Tax Liabilities", 0.10),
994        ];
995
996        let mut account_num = 200000u32;
997        for (sub_type, name_prefix, weight) in sub_types {
998            let sub_count = ((count as f64) * weight).round() as usize;
999            for i in 0..sub_count.max(1) {
1000                let account = GLAccount::new(
1001                    format!("{}", account_num),
1002                    format!("{} {}", name_prefix, i + 1),
1003                    AccountType::Liability,
1004                    sub_type,
1005                );
1006                coa.add_account(account);
1007                account_num += 10;
1008            }
1009        }
1010    }
1011
1012    fn generate_equity_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1013        let sub_types = vec![
1014            (AccountSubType::CommonStock, "Common Stock", 0.20),
1015            (AccountSubType::RetainedEarnings, "Retained Earnings", 0.30),
1016            (AccountSubType::AdditionalPaidInCapital, "APIC", 0.20),
1017            (AccountSubType::OtherComprehensiveIncome, "OCI", 0.30),
1018        ];
1019
1020        let mut account_num = 300000u32;
1021        for (sub_type, name_prefix, weight) in sub_types {
1022            let sub_count = ((count as f64) * weight).round() as usize;
1023            for i in 0..sub_count.max(1) {
1024                let account = GLAccount::new(
1025                    format!("{}", account_num),
1026                    format!("{} {}", name_prefix, i + 1),
1027                    AccountType::Equity,
1028                    sub_type,
1029                );
1030                coa.add_account(account);
1031                account_num += 10;
1032            }
1033        }
1034    }
1035
1036    fn generate_revenue_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1037        let sub_types = vec![
1038            (AccountSubType::ProductRevenue, "Product Revenue", 0.40),
1039            (AccountSubType::ServiceRevenue, "Service Revenue", 0.30),
1040            (AccountSubType::InterestIncome, "Interest Income", 0.10),
1041            (AccountSubType::OtherIncome, "Other Income", 0.20),
1042        ];
1043
1044        let mut account_num = 400000u32;
1045        for (sub_type, name_prefix, weight) in sub_types {
1046            let sub_count = ((count as f64) * weight).round() as usize;
1047            for i in 0..sub_count.max(1) {
1048                let account = GLAccount::new(
1049                    format!("{}", account_num),
1050                    format!("{} {}", name_prefix, i + 1),
1051                    AccountType::Revenue,
1052                    sub_type,
1053                );
1054                coa.add_account(account);
1055                account_num += 10;
1056            }
1057        }
1058    }
1059
1060    fn generate_expense_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1061        let sub_types = vec![
1062            (AccountSubType::CostOfGoodsSold, "COGS", 0.20),
1063            (
1064                AccountSubType::OperatingExpenses,
1065                "Operating Expenses",
1066                0.25,
1067            ),
1068            (AccountSubType::SellingExpenses, "Selling Expenses", 0.15),
1069            (
1070                AccountSubType::AdministrativeExpenses,
1071                "Admin Expenses",
1072                0.15,
1073            ),
1074            (AccountSubType::DepreciationExpense, "Depreciation", 0.10),
1075            (AccountSubType::InterestExpense, "Interest Expense", 0.05),
1076            (AccountSubType::TaxExpense, "Tax Expense", 0.05),
1077            (AccountSubType::OtherExpenses, "Other Expenses", 0.05),
1078        ];
1079
1080        let mut account_num = 500000u32;
1081        for (sub_type, name_prefix, weight) in sub_types {
1082            let sub_count = ((count as f64) * weight).round() as usize;
1083            for i in 0..sub_count.max(1) {
1084                let mut account = GLAccount::new(
1085                    format!("{}", account_num),
1086                    format!("{} {}", name_prefix, i + 1),
1087                    AccountType::Expense,
1088                    sub_type,
1089                );
1090                account.requires_cost_center = true;
1091                coa.add_account(account);
1092                account_num += 10;
1093            }
1094        }
1095    }
1096
1097    fn generate_suspense_accounts(&mut self, coa: &mut ChartOfAccounts) {
1098        let suspense_types = vec![
1099            (AccountSubType::SuspenseClearing, "Suspense Clearing"),
1100            (AccountSubType::GoodsReceivedClearing, "GR/IR Clearing"),
1101            (AccountSubType::BankClearing, "Bank Clearing"),
1102            (
1103                AccountSubType::IntercompanyClearing,
1104                "Intercompany Clearing",
1105            ),
1106        ];
1107
1108        let mut account_num = 199000u32;
1109        for (sub_type, name) in suspense_types {
1110            let mut account = GLAccount::new(
1111                format!("{}", account_num),
1112                name.to_string(),
1113                AccountType::Asset,
1114                sub_type,
1115            );
1116            account.is_suspense_account = true;
1117            coa.add_account(account);
1118            account_num += 100;
1119        }
1120    }
1121}
1122
1123impl Generator for ChartOfAccountsGenerator {
1124    type Item = ChartOfAccounts;
1125    type Config = (CoAComplexity, IndustrySector);
1126
1127    fn new(config: Self::Config, seed: u64) -> Self {
1128        Self::new(config.0, config.1, seed)
1129    }
1130
1131    fn generate_one(&mut self) -> Self::Item {
1132        self.generate()
1133    }
1134
1135    fn reset(&mut self) {
1136        self.rng = seeded_rng(self.seed, 0);
1137        self.count = 0;
1138        self.coa_framework = CoAFramework::UsGaap;
1139    }
1140
1141    fn count(&self) -> u64 {
1142        self.count
1143    }
1144
1145    fn seed(&self) -> u64 {
1146        self.seed
1147    }
1148}
1149
1150#[cfg(test)]
1151#[allow(clippy::unwrap_used)]
1152mod tests {
1153    use super::*;
1154
1155    #[test]
1156    fn test_generate_small_coa() {
1157        let mut gen =
1158            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42);
1159        let coa = gen.generate();
1160
1161        assert!(coa.account_count() >= 50);
1162        assert!(!coa.get_suspense_accounts().is_empty());
1163    }
1164
1165    #[test]
1166    fn test_generate_pcg_coa() {
1167        let mut gen =
1168            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
1169                .with_french_pcg(true);
1170        let coa = gen.generate();
1171
1172        assert_eq!(coa.country, "FR");
1173        assert!(coa.name.contains("Plan Comptable") || coa.name.contains("PCG"));
1174        assert!(coa.account_count() >= 20);
1175        // PCG accounts are 6-digit (e.g. 411000, 601000)
1176        let first = coa.accounts.first().expect("has accounts");
1177        assert_eq!(first.account_number.len(), 6);
1178    }
1179
1180    /// Verifies PCG (Plan Comptable Général) account structure: 6-digit format,
1181    /// all accounts numeric, and coverage of at least two PCG classes (1–8).
1182    /// Works for both the embedded PCG 2024 loader and the fallback generator.
1183    #[test]
1184    fn test_pcg_account_structure() {
1185        let mut gen =
1186            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
1187                .with_french_pcg(true);
1188        let coa = gen.generate();
1189
1190        assert_eq!(
1191            coa.account_format, "######",
1192            "PCG uses 6-digit account format"
1193        );
1194        assert!(
1195            coa.account_count() >= 20,
1196            "PCG CoA has minimum account count"
1197        );
1198
1199        let account_numbers: Vec<_> = coa
1200            .accounts
1201            .iter()
1202            .map(|a| a.account_number.as_str())
1203            .collect();
1204        for num in &account_numbers {
1205            assert_eq!(num.len(), 6, "every PCG account is 6 digits: {}", num);
1206            assert!(
1207                num.chars().all(|c| c.is_ascii_digit()),
1208                "PCG account is numeric: {}",
1209                num
1210            );
1211        }
1212
1213        // All account numbers must belong to a PCG class (first digit 1–8)
1214        let first_digits: std::collections::HashSet<char> = account_numbers
1215            .iter()
1216            .filter_map(|s| s.chars().next())
1217            .collect();
1218        let pcg_classes: std::collections::HashSet<_> =
1219            ['1', '2', '3', '4', '5', '6', '7', '8'].into();
1220        assert!(
1221            !first_digits.is_empty() && first_digits.is_subset(&pcg_classes),
1222            "PCG account numbers must be in classes 1–8, got first digits: {:?}",
1223            first_digits
1224        );
1225    }
1226}