Skip to main content

datasynth_generators/
coa_generator.rs

1//! Chart of Accounts generator.
2
3use tracing::debug;
4
5use datasynth_core::accounts::{
6    asset_class_accounts, cash_accounts, control_accounts, dividend_accounts, dormant_accounts,
7    equity_accounts, expense_accounts, intangible_accounts, inventory_accounts, liability_accounts,
8    manufacturing_accounts, provision_accounts, revenue_accounts, suspense_accounts, tax_accounts,
9    treasury_accounts,
10};
11use datasynth_core::models::*;
12use datasynth_core::pcg_loader;
13use datasynth_core::skr_loader;
14use datasynth_core::traits::Generator;
15use datasynth_core::utils::seeded_rng;
16use rand_chacha::ChaCha8Rng;
17
18/// Accounting framework for CoA generation.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum CoAFramework {
21    /// US GAAP (4-digit accounts)
22    #[default]
23    UsGaap,
24    /// French GAAP / Plan Comptable Général (6-digit accounts)
25    FrenchPcg,
26    /// German GAAP / SKR04 (4-digit accounts)
27    GermanSkr04,
28}
29
30/// Generator for Chart of Accounts.
31pub struct ChartOfAccountsGenerator {
32    rng: ChaCha8Rng,
33    seed: u64,
34    complexity: CoAComplexity,
35    industry: IndustrySector,
36    count: u64,
37    /// Accounting framework for CoA generation.
38    coa_framework: CoAFramework,
39}
40
41impl ChartOfAccountsGenerator {
42    /// Create a new CoA generator.
43    pub fn new(complexity: CoAComplexity, industry: IndustrySector, seed: u64) -> Self {
44        Self {
45            rng: seeded_rng(seed, 0),
46            seed,
47            complexity,
48            industry,
49            count: 0,
50            coa_framework: CoAFramework::UsGaap,
51        }
52    }
53
54    /// Use French GAAP (Plan Comptable Général) account structure.
55    ///
56    /// Deprecated: use `with_coa_framework(CoAFramework::FrenchPcg)` instead.
57    pub fn with_french_pcg(mut self, use_pcg: bool) -> Self {
58        if use_pcg {
59            self.coa_framework = CoAFramework::FrenchPcg;
60        }
61        self
62    }
63
64    /// Set the accounting framework for CoA generation.
65    pub fn with_coa_framework(mut self, framework: CoAFramework) -> Self {
66        self.coa_framework = framework;
67        self
68    }
69
70    /// Generate a complete chart of accounts.
71    pub fn generate(&mut self) -> ChartOfAccounts {
72        debug!(
73            complexity = ?self.complexity,
74            industry = ?self.industry,
75            seed = self.seed,
76            framework = ?self.coa_framework,
77            "Generating chart of accounts"
78        );
79
80        self.count += 1;
81        let mut coa = match self.coa_framework {
82            CoAFramework::UsGaap => self.generate_default(),
83            CoAFramework::FrenchPcg => self.generate_pcg(),
84            CoAFramework::GermanSkr04 => self.generate_skr(),
85        };
86
87        // v5.0.1 fix (Gap 5): stamp `accounting_framework` on every emitted
88        // GLAccount so the per-row column matches the sidecar
89        // `coa_meta.accounting_framework`. Until this fix the field
90        // defaulted to `None` even though the generator knew the framework.
91        let framework_label = match self.coa_framework {
92            CoAFramework::UsGaap => "us_gaap",
93            CoAFramework::FrenchPcg => "french_pcg",
94            CoAFramework::GermanSkr04 => "german_skr04",
95        };
96        for account in coa.accounts.iter_mut() {
97            account.accounting_framework = Some(framework_label.to_string());
98        }
99        coa
100    }
101
102    /// Generate default (US-style) chart of accounts.
103    fn generate_default(&mut self) -> ChartOfAccounts {
104        let target_count = self.complexity.target_count();
105        let mut coa = ChartOfAccounts::new(
106            format!("COA_{:?}_{}", self.industry, self.complexity.target_count()),
107            format!("{:?} Chart of Accounts", self.industry),
108            "US".to_string(),
109            self.industry,
110            self.complexity,
111        );
112
113        // Seed canonical accounts first so other generators can find them
114        Self::seed_canonical_accounts(&mut coa);
115
116        // Generate additional accounts by type
117        self.generate_asset_accounts(&mut coa, target_count / 5);
118        self.generate_liability_accounts(&mut coa, target_count / 6);
119        self.generate_equity_accounts(&mut coa, target_count / 10);
120        self.generate_revenue_accounts(&mut coa, target_count / 5);
121        self.generate_expense_accounts(&mut coa, target_count / 4);
122        self.generate_suspense_accounts(&mut coa);
123        coa
124    }
125
126    /// Generate Plan Comptable Général (French GAAP) chart of accounts.
127    /// Uses the comprehensive PCG 2024 structure from [arrhes/PCG](https://github.com/arrhes/PCG) when available.
128    fn generate_pcg(&mut self) -> ChartOfAccounts {
129        match pcg_loader::build_chart_of_accounts_from_pcg_2024(self.complexity, self.industry) {
130            Ok(coa) => coa,
131            Err(_) => self.generate_pcg_fallback(),
132        }
133    }
134
135    /// Generate SKR04 (German GAAP) chart of accounts.
136    /// Uses the embedded SKR04 2024 structure when available.
137    fn generate_skr(&mut self) -> ChartOfAccounts {
138        match skr_loader::build_chart_of_accounts_from_skr04(self.complexity, self.industry) {
139            Ok(coa) => coa,
140            Err(_) => self.generate_skr_fallback(),
141        }
142    }
143
144    /// Fallback simplified SKR04 when the embedded 2024 JSON cannot be loaded.
145    fn generate_skr_fallback(&mut self) -> ChartOfAccounts {
146        use datasynth_core::skr;
147        let target_count = self.complexity.target_count();
148        let mut coa = ChartOfAccounts::new(
149            format!("COA_SKR04_{:?}_{}", self.industry, target_count),
150            format!("Standardkontenrahmen 04 – {:?}", self.industry),
151            "DE".to_string(),
152            self.industry,
153            self.complexity,
154        );
155        coa.account_format = "####".to_string();
156
157        // Seed key SKR04 accounts
158        let key_accounts = [
159            (
160                skr::control_accounts::AR_CONTROL,
161                "Forderungen aus L+L",
162                AccountType::Asset,
163                AccountSubType::AccountsReceivable,
164            ),
165            (
166                skr::control_accounts::AP_CONTROL,
167                "Verbindlichkeiten aus L+L",
168                AccountType::Liability,
169                AccountSubType::AccountsPayable,
170            ),
171            (
172                skr::control_accounts::INVENTORY,
173                "Vorräte",
174                AccountType::Asset,
175                AccountSubType::Inventory,
176            ),
177            (
178                skr::control_accounts::FIXED_ASSETS,
179                "Sachanlagen",
180                AccountType::Asset,
181                AccountSubType::FixedAssets,
182            ),
183            (
184                skr::cash_accounts::OPERATING_CASH,
185                "Bank",
186                AccountType::Asset,
187                AccountSubType::Cash,
188            ),
189            (
190                skr::cash_accounts::PETTY_CASH,
191                "Kasse",
192                AccountType::Asset,
193                AccountSubType::Cash,
194            ),
195            (
196                skr::equity_accounts::COMMON_STOCK,
197                "Gezeichnetes Kapital",
198                AccountType::Equity,
199                AccountSubType::CommonStock,
200            ),
201            (
202                skr::equity_accounts::RETAINED_EARNINGS,
203                "Gewinnvortrag",
204                AccountType::Equity,
205                AccountSubType::RetainedEarnings,
206            ),
207            (
208                skr::revenue_accounts::PRODUCT_REVENUE,
209                "Umsatzerlöse",
210                AccountType::Revenue,
211                AccountSubType::ProductRevenue,
212            ),
213            (
214                skr::revenue_accounts::SERVICE_REVENUE,
215                "Erlöse Leistungen",
216                AccountType::Revenue,
217                AccountSubType::ServiceRevenue,
218            ),
219            (
220                skr::expense_accounts::COGS,
221                "Materialaufwand",
222                AccountType::Expense,
223                AccountSubType::CostOfGoodsSold,
224            ),
225            (
226                skr::expense_accounts::SALARIES_WAGES,
227                "Löhne und Gehälter",
228                AccountType::Expense,
229                AccountSubType::OperatingExpenses,
230            ),
231            (
232                skr::expense_accounts::DEPRECIATION,
233                "Abschreibungen",
234                AccountType::Expense,
235                AccountSubType::DepreciationExpense,
236            ),
237            (
238                skr::expense_accounts::RENT,
239                "Miete",
240                AccountType::Expense,
241                AccountSubType::OperatingExpenses,
242            ),
243        ];
244
245        for (code, name, acc_type, sub_type) in key_accounts {
246            let mut account =
247                GLAccount::new(code.to_string(), name.to_string(), acc_type, sub_type);
248            account.requires_cost_center = acc_type == AccountType::Expense;
249            coa.add_account(account);
250        }
251
252        // Add additional accounts to reach target count
253        let mut num = 4100u32;
254        while coa.account_count() < target_count && num < 9900 {
255            let code = format!("{num:04}");
256            if coa.get_account(&code).is_none() {
257                let class = (num / 1000) as u8;
258                let (acc_type, sub_type) = match class {
259                    0..=1 => (AccountType::Asset, AccountSubType::OtherAssets),
260                    2 => (AccountType::Equity, AccountSubType::RetainedEarnings),
261                    3 => (AccountType::Liability, AccountSubType::OtherLiabilities),
262                    4 => (AccountType::Revenue, AccountSubType::OtherIncome),
263                    5 => (AccountType::Expense, AccountSubType::CostOfGoodsSold),
264                    6 => (AccountType::Expense, AccountSubType::OperatingExpenses),
265                    7 => (AccountType::Expense, AccountSubType::InterestExpense),
266                    _ => (AccountType::Asset, AccountSubType::SuspenseClearing),
267                };
268                coa.add_account(GLAccount::new(
269                    code,
270                    format!("Konto {num}"),
271                    acc_type,
272                    sub_type,
273                ));
274            }
275            num += 10;
276        }
277
278        coa
279    }
280
281    /// Fallback simplified PCG when the embedded 2024 JSON cannot be loaded.
282    fn generate_pcg_fallback(&mut self) -> ChartOfAccounts {
283        let target_count = self.complexity.target_count();
284        let mut coa = ChartOfAccounts::new(
285            format!("COA_PCG_{:?}_{}", self.industry, target_count),
286            format!("Plan Comptable Général – {:?}", self.industry),
287            "FR".to_string(),
288            self.industry,
289            self.complexity,
290        );
291        coa.account_format = "######".to_string();
292
293        self.generate_pcg_class_1(&mut coa, target_count / 10);
294        self.generate_pcg_class_2(&mut coa, target_count / 6);
295        self.generate_pcg_class_3(&mut coa, target_count / 8);
296        self.generate_pcg_class_4(&mut coa, target_count / 5);
297        self.generate_pcg_class_5(&mut coa, target_count / 12);
298        self.generate_pcg_class_6(&mut coa, target_count / 4);
299        self.generate_pcg_class_7(&mut coa, target_count / 5);
300        self.generate_pcg_class_8(&mut coa);
301
302        coa
303    }
304
305    fn generate_pcg_class_1(&mut self, coa: &mut ChartOfAccounts, count: usize) {
306        let items = [
307            (
308                101,
309                "Capital",
310                AccountType::Equity,
311                AccountSubType::CommonStock,
312            ),
313            (
314                129,
315                "Résultat",
316                AccountType::Equity,
317                AccountSubType::RetainedEarnings,
318            ),
319            (
320                164,
321                "Emprunts",
322                AccountType::Liability,
323                AccountSubType::LongTermDebt,
324            ),
325            (
326                151,
327                "Provisions pour risques",
328                AccountType::Liability,
329                AccountSubType::AccruedLiabilities,
330            ),
331        ];
332        for (base, name, acc_type, sub_type) in items {
333            for i in 0..count.max(1) {
334                let num = base * 1000 + (i as u32 % 100);
335                coa.add_account(GLAccount::new(
336                    format!("{num:06}"),
337                    format!("{} {}", name, i + 1),
338                    acc_type,
339                    sub_type,
340                ));
341            }
342        }
343    }
344
345    fn generate_pcg_class_2(&mut self, coa: &mut ChartOfAccounts, count: usize) {
346        for i in 0..count.max(1) {
347            let num = 215000 + (i as u32 % 100);
348            coa.add_account(GLAccount::new(
349                format!("{num:06}"),
350                format!("Immobilisations {}", i + 1),
351                AccountType::Asset,
352                AccountSubType::FixedAssets,
353            ));
354        }
355        for i in 0..(count / 2).max(1) {
356            let num = 281000 + (i as u32 % 100);
357            coa.add_account(GLAccount::new(
358                format!("{num:06}"),
359                format!("Amortissements {}", i + 1),
360                AccountType::Asset,
361                AccountSubType::AccumulatedDepreciation,
362            ));
363        }
364    }
365
366    fn generate_pcg_class_3(&mut self, coa: &mut ChartOfAccounts, count: usize) {
367        for i in 0..count.max(1) {
368            let num = 310000 + (i as u32 % 1000);
369            coa.add_account(GLAccount::new(
370                format!("{num:06}"),
371                format!("Stocks {}", i + 1),
372                AccountType::Asset,
373                AccountSubType::Inventory,
374            ));
375        }
376    }
377
378    fn generate_pcg_class_4(&mut self, coa: &mut ChartOfAccounts, count: usize) {
379        for i in 0..count.max(1) {
380            let num = 411000 + (i as u32 % 1000);
381            coa.add_account(GLAccount::new(
382                format!("{num:06}"),
383                format!("Clients {}", i + 1),
384                AccountType::Asset,
385                AccountSubType::AccountsReceivable,
386            ));
387        }
388        for i in 0..count.max(1) {
389            let num = 401000 + (i as u32 % 1000);
390            coa.add_account(GLAccount::new(
391                format!("{num:06}"),
392                format!("Fournisseurs {}", i + 1),
393                AccountType::Liability,
394                AccountSubType::AccountsPayable,
395            ));
396        }
397        let clearing = GLAccount::new(
398            "408000".to_string(),
399            "Fournisseurs – non encore reçus".to_string(),
400            AccountType::Liability,
401            AccountSubType::GoodsReceivedClearing,
402        );
403        coa.add_account(clearing);
404    }
405
406    fn generate_pcg_class_5(&mut self, coa: &mut ChartOfAccounts, count: usize) {
407        let bases = [
408            (512, "Banque"),
409            (530, "Caisse"),
410            (511, "Valeurs à l'encaissement"),
411        ];
412        for (base, name) in bases {
413            for i in 0..(count / 3).max(1) {
414                let num = base * 1000 + (i as u32 % 100);
415                coa.add_account(GLAccount::new(
416                    format!("{num:06}"),
417                    format!("{} {}", name, i + 1),
418                    AccountType::Asset,
419                    AccountSubType::Cash,
420                ));
421            }
422        }
423    }
424
425    fn generate_pcg_class_6(&mut self, coa: &mut ChartOfAccounts, count: usize) {
426        let bases = [
427            (603, "Achats"),
428            (641, "Rémunérations"),
429            (681, "DAP"),
430            (613, "Loyers"),
431            (661, "Charges financières"),
432        ];
433        for (base, name) in bases {
434            for i in 0..(count / 5).max(1) {
435                let num = base * 1000 + (i as u32 % 100);
436                let mut account = GLAccount::new(
437                    format!("{num:06}"),
438                    format!("{} {}", name, i + 1),
439                    AccountType::Expense,
440                    AccountSubType::OperatingExpenses,
441                );
442                account.requires_cost_center = true;
443                coa.add_account(account);
444            }
445        }
446    }
447
448    fn generate_pcg_class_7(&mut self, coa: &mut ChartOfAccounts, count: usize) {
449        let bases = [
450            (701, "Ventes"),
451            (706, "Prestations"),
452            (758, "Produits divers"),
453        ];
454        for (base, name) in bases {
455            for i in 0..(count / 3).max(1) {
456                let num = base * 1000 + (i as u32 % 100);
457                coa.add_account(GLAccount::new(
458                    format!("{num:06}"),
459                    format!("{} {}", name, i + 1),
460                    AccountType::Revenue,
461                    AccountSubType::ProductRevenue,
462                ));
463            }
464        }
465    }
466
467    fn generate_pcg_class_8(&mut self, coa: &mut ChartOfAccounts) {
468        coa.add_account(GLAccount::new(
469            "808000".to_string(),
470            "Comptes spéciaux".to_string(),
471            AccountType::Asset,
472            AccountSubType::SuspenseClearing,
473        ));
474    }
475
476    /// Insert all canonical accounts from `datasynth_core::accounts` into the CoA.
477    ///
478    /// These are the well-known account numbers (4-digit, 1000-9300 range) that
479    /// other generators reference. They are added before auto-generated accounts
480    /// (which start at 100000+) so there are no collisions.
481    fn seed_canonical_accounts(coa: &mut ChartOfAccounts) {
482        // --- Cash accounts (1000-series, Asset / Cash) ---
483        coa.add_account(GLAccount::new(
484            cash_accounts::OPERATING_CASH.to_string(),
485            "Operating Cash".to_string(),
486            AccountType::Asset,
487            AccountSubType::Cash,
488        ));
489        coa.add_account(GLAccount::new(
490            cash_accounts::BANK_ACCOUNT.to_string(),
491            "Bank Account".to_string(),
492            AccountType::Asset,
493            AccountSubType::Cash,
494        ));
495        coa.add_account(GLAccount::new(
496            cash_accounts::PETTY_CASH.to_string(),
497            "Petty Cash".to_string(),
498            AccountType::Asset,
499            AccountSubType::Cash,
500        ));
501        coa.add_account(GLAccount::new(
502            cash_accounts::WIRE_CLEARING.to_string(),
503            "Wire Transfer Clearing".to_string(),
504            AccountType::Asset,
505            AccountSubType::BankClearing,
506        ));
507
508        // --- Control accounts (Asset side) ---
509        {
510            let mut acct = GLAccount::new(
511                control_accounts::AR_CONTROL.to_string(),
512                "Accounts Receivable Control".to_string(),
513                AccountType::Asset,
514                AccountSubType::AccountsReceivable,
515            );
516            acct.is_control_account = true;
517            coa.add_account(acct);
518        }
519        {
520            let mut acct = GLAccount::new(
521                control_accounts::IC_AR_CLEARING.to_string(),
522                "Intercompany AR Clearing".to_string(),
523                AccountType::Asset,
524                AccountSubType::AccountsReceivable,
525            );
526            acct.is_control_account = true;
527            coa.add_account(acct);
528        }
529        coa.add_account(GLAccount::new(
530            control_accounts::INVENTORY.to_string(),
531            "Inventory".to_string(),
532            AccountType::Asset,
533            AccountSubType::Inventory,
534        ));
535        coa.add_account(GLAccount::new(
536            control_accounts::FIXED_ASSETS.to_string(),
537            "Fixed Assets".to_string(),
538            AccountType::Asset,
539            AccountSubType::FixedAssets,
540        ));
541        coa.add_account(GLAccount::new(
542            control_accounts::ACCUMULATED_DEPRECIATION.to_string(),
543            "Accumulated Depreciation".to_string(),
544            AccountType::Asset,
545            AccountSubType::AccumulatedDepreciation,
546        ));
547
548        // --- Tax asset accounts ---
549        coa.add_account(GLAccount::new(
550            tax_accounts::INPUT_VAT.to_string(),
551            "Input VAT".to_string(),
552            AccountType::Asset,
553            AccountSubType::OtherReceivables,
554        ));
555        coa.add_account(GLAccount::new(
556            tax_accounts::DEFERRED_TAX_ASSET.to_string(),
557            "Deferred Tax Asset".to_string(),
558            AccountType::Asset,
559            AccountSubType::OtherAssets,
560        ));
561
562        // --- Liability / Control accounts (2000-series) ---
563        {
564            let mut acct = GLAccount::new(
565                control_accounts::AP_CONTROL.to_string(),
566                "Accounts Payable Control".to_string(),
567                AccountType::Liability,
568                AccountSubType::AccountsPayable,
569            );
570            acct.is_control_account = true;
571            coa.add_account(acct);
572        }
573        {
574            let mut acct = GLAccount::new(
575                control_accounts::IC_AP_CLEARING.to_string(),
576                "Intercompany AP Clearing".to_string(),
577                AccountType::Liability,
578                AccountSubType::AccountsPayable,
579            );
580            acct.is_control_account = true;
581            coa.add_account(acct);
582        }
583        coa.add_account(GLAccount::new(
584            tax_accounts::SALES_TAX_PAYABLE.to_string(),
585            "Sales Tax Payable".to_string(),
586            AccountType::Liability,
587            AccountSubType::TaxLiabilities,
588        ));
589        coa.add_account(GLAccount::new(
590            tax_accounts::VAT_PAYABLE.to_string(),
591            "VAT Payable".to_string(),
592            AccountType::Liability,
593            AccountSubType::TaxLiabilities,
594        ));
595        coa.add_account(GLAccount::new(
596            tax_accounts::WITHHOLDING_TAX_PAYABLE.to_string(),
597            "Withholding Tax Payable".to_string(),
598            AccountType::Liability,
599            AccountSubType::TaxLiabilities,
600        ));
601        coa.add_account(GLAccount::new(
602            liability_accounts::ACCRUED_EXPENSES.to_string(),
603            "Accrued Expenses".to_string(),
604            AccountType::Liability,
605            AccountSubType::AccruedLiabilities,
606        ));
607        coa.add_account(GLAccount::new(
608            liability_accounts::ACCRUED_SALARIES.to_string(),
609            "Accrued Salaries".to_string(),
610            AccountType::Liability,
611            AccountSubType::AccruedLiabilities,
612        ));
613        coa.add_account(GLAccount::new(
614            liability_accounts::ACCRUED_BENEFITS.to_string(),
615            "Accrued Benefits".to_string(),
616            AccountType::Liability,
617            AccountSubType::AccruedLiabilities,
618        ));
619        coa.add_account(GLAccount::new(
620            liability_accounts::UNEARNED_REVENUE.to_string(),
621            "Unearned Revenue".to_string(),
622            AccountType::Liability,
623            AccountSubType::DeferredRevenue,
624        ));
625        coa.add_account(GLAccount::new(
626            liability_accounts::SHORT_TERM_DEBT.to_string(),
627            "Short-Term Debt".to_string(),
628            AccountType::Liability,
629            AccountSubType::ShortTermDebt,
630        ));
631        coa.add_account(GLAccount::new(
632            tax_accounts::DEFERRED_TAX_LIABILITY.to_string(),
633            "Deferred Tax Liability".to_string(),
634            AccountType::Liability,
635            AccountSubType::TaxLiabilities,
636        ));
637        coa.add_account(GLAccount::new(
638            liability_accounts::LONG_TERM_DEBT.to_string(),
639            "Long-Term Debt".to_string(),
640            AccountType::Liability,
641            AccountSubType::LongTermDebt,
642        ));
643        coa.add_account(GLAccount::new(
644            liability_accounts::IC_PAYABLE.to_string(),
645            "Intercompany Payable".to_string(),
646            AccountType::Liability,
647            AccountSubType::OtherLiabilities,
648        ));
649        {
650            let mut acct = GLAccount::new(
651                control_accounts::GR_IR_CLEARING.to_string(),
652                "GR/IR Clearing".to_string(),
653                AccountType::Liability,
654                AccountSubType::GoodsReceivedClearing,
655            );
656            acct.is_suspense_account = true;
657            coa.add_account(acct);
658        }
659
660        // --- Equity accounts (3000-series) ---
661        coa.add_account(GLAccount::new(
662            equity_accounts::COMMON_STOCK.to_string(),
663            "Common Stock".to_string(),
664            AccountType::Equity,
665            AccountSubType::CommonStock,
666        ));
667        coa.add_account(GLAccount::new(
668            equity_accounts::APIC.to_string(),
669            "Additional Paid-In Capital".to_string(),
670            AccountType::Equity,
671            AccountSubType::AdditionalPaidInCapital,
672        ));
673        coa.add_account(GLAccount::new(
674            equity_accounts::RETAINED_EARNINGS.to_string(),
675            "Retained Earnings".to_string(),
676            AccountType::Equity,
677            AccountSubType::RetainedEarnings,
678        ));
679        coa.add_account(GLAccount::new(
680            equity_accounts::CURRENT_YEAR_EARNINGS.to_string(),
681            "Current Year Earnings".to_string(),
682            AccountType::Equity,
683            AccountSubType::NetIncome,
684        ));
685        coa.add_account(GLAccount::new(
686            equity_accounts::TREASURY_STOCK.to_string(),
687            "Treasury Stock".to_string(),
688            AccountType::Equity,
689            AccountSubType::TreasuryStock,
690        ));
691        coa.add_account(GLAccount::new(
692            equity_accounts::CTA.to_string(),
693            "Currency Translation Adjustment".to_string(),
694            AccountType::Equity,
695            AccountSubType::OtherComprehensiveIncome,
696        ));
697
698        // --- Revenue accounts (4000-series) ---
699        coa.add_account(GLAccount::new(
700            revenue_accounts::PRODUCT_REVENUE.to_string(),
701            "Product Revenue".to_string(),
702            AccountType::Revenue,
703            AccountSubType::ProductRevenue,
704        ));
705        coa.add_account(GLAccount::new(
706            revenue_accounts::SALES_DISCOUNTS.to_string(),
707            "Sales Discounts".to_string(),
708            AccountType::Revenue,
709            AccountSubType::ProductRevenue,
710        ));
711        coa.add_account(GLAccount::new(
712            revenue_accounts::SALES_RETURNS.to_string(),
713            "Sales Returns and Allowances".to_string(),
714            AccountType::Revenue,
715            AccountSubType::ProductRevenue,
716        ));
717        coa.add_account(GLAccount::new(
718            revenue_accounts::SERVICE_REVENUE.to_string(),
719            "Service Revenue".to_string(),
720            AccountType::Revenue,
721            AccountSubType::ServiceRevenue,
722        ));
723        coa.add_account(GLAccount::new(
724            revenue_accounts::IC_REVENUE.to_string(),
725            "Intercompany Revenue".to_string(),
726            AccountType::Revenue,
727            AccountSubType::OtherIncome,
728        ));
729        coa.add_account(GLAccount::new(
730            revenue_accounts::OTHER_REVENUE.to_string(),
731            "Other Revenue".to_string(),
732            AccountType::Revenue,
733            AccountSubType::OtherIncome,
734        ));
735
736        // --- Expense accounts (5000-7xxx series) ---
737        {
738            let mut acct = GLAccount::new(
739                expense_accounts::COGS.to_string(),
740                "Cost of Goods Sold".to_string(),
741                AccountType::Expense,
742                AccountSubType::CostOfGoodsSold,
743            );
744            acct.requires_cost_center = true;
745            coa.add_account(acct);
746        }
747        {
748            let mut acct = GLAccount::new(
749                expense_accounts::RAW_MATERIALS.to_string(),
750                "Raw Materials".to_string(),
751                AccountType::Expense,
752                AccountSubType::CostOfGoodsSold,
753            );
754            acct.requires_cost_center = true;
755            coa.add_account(acct);
756        }
757        {
758            let mut acct = GLAccount::new(
759                expense_accounts::DIRECT_LABOR.to_string(),
760                "Direct Labor".to_string(),
761                AccountType::Expense,
762                AccountSubType::CostOfGoodsSold,
763            );
764            acct.requires_cost_center = true;
765            coa.add_account(acct);
766        }
767        {
768            let mut acct = GLAccount::new(
769                expense_accounts::MANUFACTURING_OVERHEAD.to_string(),
770                "Manufacturing Overhead".to_string(),
771                AccountType::Expense,
772                AccountSubType::CostOfGoodsSold,
773            );
774            acct.requires_cost_center = true;
775            coa.add_account(acct);
776        }
777        {
778            let mut acct = GLAccount::new(
779                expense_accounts::DEPRECIATION.to_string(),
780                "Depreciation Expense".to_string(),
781                AccountType::Expense,
782                AccountSubType::DepreciationExpense,
783            );
784            acct.requires_cost_center = true;
785            coa.add_account(acct);
786        }
787        {
788            let mut acct = GLAccount::new(
789                expense_accounts::SALARIES_WAGES.to_string(),
790                "Salaries and Wages".to_string(),
791                AccountType::Expense,
792                AccountSubType::OperatingExpenses,
793            );
794            acct.requires_cost_center = true;
795            coa.add_account(acct);
796        }
797        {
798            let mut acct = GLAccount::new(
799                expense_accounts::BENEFITS.to_string(),
800                "Benefits Expense".to_string(),
801                AccountType::Expense,
802                AccountSubType::OperatingExpenses,
803            );
804            acct.requires_cost_center = true;
805            coa.add_account(acct);
806        }
807        {
808            let mut acct = GLAccount::new(
809                expense_accounts::RENT.to_string(),
810                "Rent Expense".to_string(),
811                AccountType::Expense,
812                AccountSubType::OperatingExpenses,
813            );
814            acct.requires_cost_center = true;
815            coa.add_account(acct);
816        }
817        {
818            let mut acct = GLAccount::new(
819                expense_accounts::UTILITIES.to_string(),
820                "Utilities Expense".to_string(),
821                AccountType::Expense,
822                AccountSubType::OperatingExpenses,
823            );
824            acct.requires_cost_center = true;
825            coa.add_account(acct);
826        }
827        {
828            let mut acct = GLAccount::new(
829                expense_accounts::OFFICE_SUPPLIES.to_string(),
830                "Office Supplies".to_string(),
831                AccountType::Expense,
832                AccountSubType::AdministrativeExpenses,
833            );
834            acct.requires_cost_center = true;
835            coa.add_account(acct);
836        }
837        {
838            let mut acct = GLAccount::new(
839                expense_accounts::TRAVEL_ENTERTAINMENT.to_string(),
840                "Travel and Entertainment".to_string(),
841                AccountType::Expense,
842                AccountSubType::SellingExpenses,
843            );
844            acct.requires_cost_center = true;
845            coa.add_account(acct);
846        }
847        {
848            let mut acct = GLAccount::new(
849                expense_accounts::PROFESSIONAL_FEES.to_string(),
850                "Professional Fees".to_string(),
851                AccountType::Expense,
852                AccountSubType::AdministrativeExpenses,
853            );
854            acct.requires_cost_center = true;
855            coa.add_account(acct);
856        }
857        {
858            let mut acct = GLAccount::new(
859                expense_accounts::INSURANCE.to_string(),
860                "Insurance Expense".to_string(),
861                AccountType::Expense,
862                AccountSubType::OperatingExpenses,
863            );
864            acct.requires_cost_center = true;
865            coa.add_account(acct);
866        }
867        {
868            let mut acct = GLAccount::new(
869                expense_accounts::BAD_DEBT.to_string(),
870                "Bad Debt Expense".to_string(),
871                AccountType::Expense,
872                AccountSubType::OperatingExpenses,
873            );
874            acct.requires_cost_center = true;
875            coa.add_account(acct);
876        }
877        {
878            let mut acct = GLAccount::new(
879                expense_accounts::INTEREST_EXPENSE.to_string(),
880                "Interest Expense".to_string(),
881                AccountType::Expense,
882                AccountSubType::InterestExpense,
883            );
884            acct.requires_cost_center = true;
885            coa.add_account(acct);
886        }
887        {
888            let mut acct = GLAccount::new(
889                expense_accounts::PURCHASE_DISCOUNTS.to_string(),
890                "Purchase Discounts".to_string(),
891                AccountType::Expense,
892                AccountSubType::OtherExpenses,
893            );
894            acct.requires_cost_center = true;
895            coa.add_account(acct);
896        }
897        {
898            let mut acct = GLAccount::new(
899                expense_accounts::FX_GAIN_LOSS.to_string(),
900                "FX Gain/Loss".to_string(),
901                AccountType::Expense,
902                AccountSubType::ForeignExchangeLoss,
903            );
904            acct.requires_cost_center = true;
905            coa.add_account(acct);
906        }
907
908        // --- Tax expense (8000-series) ---
909        {
910            let mut acct = GLAccount::new(
911                tax_accounts::TAX_EXPENSE.to_string(),
912                "Tax Expense".to_string(),
913                AccountType::Expense,
914                AccountSubType::TaxExpense,
915            );
916            acct.requires_cost_center = true;
917            coa.add_account(acct);
918        }
919
920        // --- Asset class accounts (FA subledger acquisition + acc. depreciation) ---
921        Self::seed_asset_class_accounts(coa);
922
923        // --- Manufacturing accounts (WIP, finished goods, variances, warranty) ---
924        Self::seed_manufacturing_accounts(coa);
925
926        // --- Intangible assets (goodwill, customer relationships, etc.) ---
927        Self::seed_intangible_accounts(coa);
928
929        // --- Treasury / hedging / debt accounts ---
930        Self::seed_treasury_accounts(coa);
931
932        // --- Provisions (IAS 37 / ASC 450) ---
933        Self::seed_provision_accounts(coa);
934
935        // --- Dividend accounts (declared / payable) ---
936        Self::seed_dividend_accounts(coa);
937
938        // --- Pension / share-based compensation ---
939        Self::seed_compensation_accounts(coa);
940
941        // --- Inventory subledger sub-accounts (write-up income / write-down expense) ---
942        Self::seed_inventory_subledger_accounts(coa);
943
944        // --- Tax accounts not seeded above (income tax payable, tax receivable) ---
945        Self::seed_additional_tax_accounts(coa);
946
947        // --- Equity accounts not seeded above (income summary, dividends paid) ---
948        Self::seed_additional_equity_accounts(coa);
949
950        // --- Dormant / blocked legacy accounts (anomaly-strategy targets) ---
951        Self::seed_dormant_accounts(coa);
952
953        // --- Suspense / Clearing accounts (9000-series) ---
954        {
955            let mut acct = GLAccount::new(
956                suspense_accounts::GENERAL_SUSPENSE.to_string(),
957                "General Suspense".to_string(),
958                AccountType::Asset,
959                AccountSubType::SuspenseClearing,
960            );
961            acct.is_suspense_account = true;
962            coa.add_account(acct);
963        }
964        {
965            let mut acct = GLAccount::new(
966                suspense_accounts::PAYROLL_CLEARING.to_string(),
967                "Payroll Clearing".to_string(),
968                AccountType::Asset,
969                AccountSubType::SuspenseClearing,
970            );
971            acct.is_suspense_account = true;
972            coa.add_account(acct);
973        }
974        {
975            let mut acct = GLAccount::new(
976                suspense_accounts::BANK_RECONCILIATION_SUSPENSE.to_string(),
977                "Bank Reconciliation Suspense".to_string(),
978                AccountType::Asset,
979                AccountSubType::BankClearing,
980            );
981            acct.is_suspense_account = true;
982            coa.add_account(acct);
983        }
984        {
985            let mut acct = GLAccount::new(
986                suspense_accounts::IC_ELIMINATION_SUSPENSE.to_string(),
987                "IC Elimination Suspense".to_string(),
988                AccountType::Asset,
989                AccountSubType::IntercompanyClearing,
990            );
991            acct.is_suspense_account = true;
992            coa.add_account(acct);
993        }
994    }
995
996    /// Seed asset-class acquisition + accumulated-depreciation contras +
997    /// FA-subledger expense / disposal accounts so JEs from the fixed-asset
998    /// generator always resolve.
999    fn seed_asset_class_accounts(coa: &mut ChartOfAccounts) {
1000        let acquisitions = [
1001            (asset_class_accounts::LAND, "Land"),
1002            (asset_class_accounts::BUILDINGS, "Buildings"),
1003            (
1004                asset_class_accounts::BUILDING_IMPROVEMENTS,
1005                "Building Improvements",
1006            ),
1007            (
1008                asset_class_accounts::MACHINERY_EQUIPMENT,
1009                "Machinery & Equipment",
1010            ),
1011            (asset_class_accounts::VEHICLES, "Vehicles"),
1012            (asset_class_accounts::OFFICE_EQUIPMENT, "Office Equipment"),
1013            (asset_class_accounts::COMPUTER_HARDWARE, "Computer Hardware"),
1014            (
1015                asset_class_accounts::SOFTWARE_INTANGIBLES,
1016                "Software / Intangibles",
1017            ),
1018            (
1019                asset_class_accounts::FURNITURE_FIXTURES,
1020                "Furniture & Fixtures",
1021            ),
1022            (
1023                asset_class_accounts::LEASEHOLD_IMPROVEMENTS,
1024                "Leasehold Improvements",
1025            ),
1026            (asset_class_accounts::OTHER_ASSETS, "Other Fixed Assets"),
1027            (asset_class_accounts::LOW_VALUE_ASSETS, "Low-Value Assets"),
1028            (
1029                asset_class_accounts::CONSTRUCTION_IN_PROGRESS,
1030                "Construction in Progress",
1031            ),
1032        ];
1033        for (number, name) in acquisitions {
1034            coa.add_account(GLAccount::new(
1035                number.to_string(),
1036                name.to_string(),
1037                AccountType::Asset,
1038                AccountSubType::FixedAssets,
1039            ));
1040        }
1041
1042        let depreciation_contras = [
1043            (asset_class_accounts::ACC_DEP_LAND, "Acc. Dep. — Land"),
1044            (
1045                asset_class_accounts::ACC_DEP_BUILDINGS,
1046                "Acc. Dep. — Buildings",
1047            ),
1048            (
1049                asset_class_accounts::ACC_DEP_MACHINERY,
1050                "Acc. Dep. — Machinery",
1051            ),
1052            (
1053                asset_class_accounts::ACC_DEP_VEHICLES,
1054                "Acc. Dep. — Vehicles",
1055            ),
1056            (
1057                asset_class_accounts::ACC_DEP_OFFICE_EQUIPMENT,
1058                "Acc. Dep. — Office Equipment",
1059            ),
1060            (
1061                asset_class_accounts::ACC_DEP_SOFTWARE,
1062                "Acc. Dep. — Software / Intangibles",
1063            ),
1064            (
1065                asset_class_accounts::ACC_DEP_FURNITURE,
1066                "Acc. Dep. — Furniture",
1067            ),
1068            (
1069                asset_class_accounts::ACC_DEP_LEASEHOLD,
1070                "Acc. Dep. — Leasehold Improvements",
1071            ),
1072            (asset_class_accounts::ACC_DEP_OTHER, "Acc. Dep. — Other"),
1073            (asset_class_accounts::ACC_DEP_CIP, "Acc. Dep. — CIP"),
1074        ];
1075        for (number, name) in depreciation_contras {
1076            coa.add_account(GLAccount::new(
1077                number.to_string(),
1078                name.to_string(),
1079                AccountType::Asset,
1080                AccountSubType::AccumulatedDepreciation,
1081            ));
1082        }
1083
1084        // FA-subledger depreciation expense (7100) — distinct from the
1085        // GL-level DEPRECIATION (6000) which is also seeded.
1086        {
1087            let mut acct = GLAccount::new(
1088                asset_class_accounts::DEPRECIATION_EXPENSE.to_string(),
1089                "FA Depreciation Expense".to_string(),
1090                AccountType::Expense,
1091                AccountSubType::DepreciationExpense,
1092            );
1093            acct.requires_cost_center = true;
1094            coa.add_account(acct);
1095        }
1096        coa.add_account(GLAccount::new(
1097            asset_class_accounts::GAIN_ON_DISPOSAL.to_string(),
1098            "Gain on Disposal of Fixed Assets".to_string(),
1099            AccountType::Revenue,
1100            AccountSubType::GainOnSale,
1101        ));
1102        coa.add_account(GLAccount::new(
1103            asset_class_accounts::LOSS_ON_DISPOSAL.to_string(),
1104            "Loss on Disposal of Fixed Assets".to_string(),
1105            AccountType::Expense,
1106            AccountSubType::LossOnSale,
1107        ));
1108    }
1109
1110    /// Seed manufacturing-cost-flow accounts (WIP, finished goods, variances, warranty).
1111    fn seed_manufacturing_accounts(coa: &mut ChartOfAccounts) {
1112        coa.add_account(GLAccount::new(
1113            manufacturing_accounts::FINISHED_GOODS.to_string(),
1114            "Finished Goods".to_string(),
1115            AccountType::Asset,
1116            AccountSubType::Inventory,
1117        ));
1118        coa.add_account(GLAccount::new(
1119            manufacturing_accounts::WIP.to_string(),
1120            "Work in Process".to_string(),
1121            AccountType::Asset,
1122            AccountSubType::Inventory,
1123        ));
1124        coa.add_account(GLAccount::new(
1125            manufacturing_accounts::LABOR_ACCRUAL.to_string(),
1126            "Labor Accrual".to_string(),
1127            AccountType::Liability,
1128            AccountSubType::AccruedLiabilities,
1129        ));
1130        coa.add_account(GLAccount::new(
1131            manufacturing_accounts::WARRANTY_PROVISION.to_string(),
1132            "Warranty Provision".to_string(),
1133            AccountType::Liability,
1134            AccountSubType::OtherLiabilities,
1135        ));
1136
1137        let variance_accounts = [
1138            (
1139                manufacturing_accounts::SCRAP_EXPENSE,
1140                "Scrap Expense",
1141                AccountSubType::CostOfGoodsSold,
1142            ),
1143            (
1144                manufacturing_accounts::OVERHEAD_APPLIED,
1145                "Overhead Applied",
1146                AccountSubType::CostOfGoodsSold,
1147            ),
1148            (
1149                manufacturing_accounts::MATERIAL_PRICE_VARIANCE,
1150                "Material Price Variance",
1151                AccountSubType::CostOfGoodsSold,
1152            ),
1153            (
1154                manufacturing_accounts::MATERIAL_USAGE_VARIANCE,
1155                "Material Usage Variance",
1156                AccountSubType::CostOfGoodsSold,
1157            ),
1158            (
1159                manufacturing_accounts::LABOR_RATE_VARIANCE,
1160                "Labor Rate Variance",
1161                AccountSubType::CostOfGoodsSold,
1162            ),
1163            (
1164                manufacturing_accounts::LABOR_EFFICIENCY_VARIANCE,
1165                "Labor Efficiency Variance",
1166                AccountSubType::CostOfGoodsSold,
1167            ),
1168            (
1169                manufacturing_accounts::OVERHEAD_VOLUME_VARIANCE,
1170                "Overhead Volume Variance",
1171                AccountSubType::CostOfGoodsSold,
1172            ),
1173            (
1174                manufacturing_accounts::WARRANTY_EXPENSE,
1175                "Warranty Expense",
1176                AccountSubType::OperatingExpenses,
1177            ),
1178        ];
1179        for (number, name, sub) in variance_accounts {
1180            let mut acct = GLAccount::new(
1181                number.to_string(),
1182                name.to_string(),
1183                AccountType::Expense,
1184                sub,
1185            );
1186            acct.requires_cost_center = true;
1187            coa.add_account(acct);
1188        }
1189    }
1190
1191    /// Seed intangible-asset accounts (goodwill, customer relationships, etc.).
1192    fn seed_intangible_accounts(coa: &mut ChartOfAccounts) {
1193        let intangibles = [
1194            (
1195                intangible_accounts::GOODWILL,
1196                "Goodwill",
1197                AccountType::Asset,
1198                AccountSubType::IntangibleAssets,
1199            ),
1200            (
1201                intangible_accounts::CUSTOMER_RELATIONSHIPS,
1202                "Customer Relationships",
1203                AccountType::Asset,
1204                AccountSubType::IntangibleAssets,
1205            ),
1206            (
1207                intangible_accounts::TRADE_NAME,
1208                "Trade Name / Brand",
1209                AccountType::Asset,
1210                AccountSubType::IntangibleAssets,
1211            ),
1212            (
1213                intangible_accounts::TECHNOLOGY,
1214                "Technology / Developed Software",
1215                AccountType::Asset,
1216                AccountSubType::IntangibleAssets,
1217            ),
1218            (
1219                intangible_accounts::ACCUMULATED_AMORTIZATION,
1220                "Accumulated Amortization",
1221                AccountType::Asset,
1222                AccountSubType::AccumulatedDepreciation,
1223            ),
1224            (
1225                intangible_accounts::AMORTIZATION_EXPENSE,
1226                "Amortization Expense — Intangibles",
1227                AccountType::Expense,
1228                AccountSubType::AmortizationExpense,
1229            ),
1230            (
1231                intangible_accounts::BARGAIN_PURCHASE_GAIN,
1232                "Bargain Purchase Gain",
1233                AccountType::Revenue,
1234                AccountSubType::OtherIncome,
1235            ),
1236        ];
1237        for (number, name, ty, sub) in intangibles {
1238            coa.add_account(GLAccount::new(
1239                number.to_string(),
1240                name.to_string(),
1241                ty,
1242                sub,
1243            ));
1244        }
1245    }
1246
1247    /// Seed treasury / hedging / debt accounts.
1248    fn seed_treasury_accounts(coa: &mut ChartOfAccounts) {
1249        let entries = [
1250            (
1251                treasury_accounts::INTEREST_PAYABLE,
1252                "Interest Payable",
1253                AccountType::Liability,
1254                AccountSubType::AccruedLiabilities,
1255            ),
1256            (
1257                treasury_accounts::DEBT_PREMIUM,
1258                "Debt Premium",
1259                AccountType::Liability,
1260                AccountSubType::LongTermDebt,
1261            ),
1262            (
1263                treasury_accounts::DEBT_DISCOUNT,
1264                "Debt Discount",
1265                AccountType::Liability,
1266                AccountSubType::LongTermDebt,
1267            ),
1268            (
1269                treasury_accounts::DERIVATIVE_ASSET,
1270                "Derivative Asset",
1271                AccountType::Asset,
1272                AccountSubType::OtherAssets,
1273            ),
1274            (
1275                treasury_accounts::DERIVATIVE_LIABILITY,
1276                "Derivative Liability",
1277                AccountType::Liability,
1278                AccountSubType::OtherLiabilities,
1279            ),
1280            (
1281                treasury_accounts::OCI_CASH_FLOW_HEDGE,
1282                "OCI — Cash Flow Hedge Reserve",
1283                AccountType::Equity,
1284                AccountSubType::OtherComprehensiveIncome,
1285            ),
1286            (
1287                treasury_accounts::HEDGE_INEFFECTIVENESS,
1288                "Hedge Ineffectiveness",
1289                AccountType::Expense,
1290                AccountSubType::OtherExpenses,
1291            ),
1292            (
1293                treasury_accounts::CASH_POOL_IC_RECEIVABLE,
1294                "IC Receivable — Cash Pool",
1295                AccountType::Asset,
1296                AccountSubType::AccountsReceivable,
1297            ),
1298            (
1299                treasury_accounts::CASH_POOL_IC_PAYABLE,
1300                "IC Payable — Cash Pool",
1301                AccountType::Liability,
1302                AccountSubType::AccountsPayable,
1303            ),
1304        ];
1305        for (number, name, ty, sub) in entries {
1306            coa.add_account(GLAccount::new(
1307                number.to_string(),
1308                name.to_string(),
1309                ty,
1310                sub,
1311            ));
1312        }
1313    }
1314
1315    /// Seed provision accounts (IAS 37 / ASC 450).
1316    fn seed_provision_accounts(coa: &mut ChartOfAccounts) {
1317        coa.add_account(GLAccount::new(
1318            provision_accounts::PROVISION_LIABILITY.to_string(),
1319            "Provision Liability".to_string(),
1320            AccountType::Liability,
1321            AccountSubType::OtherLiabilities,
1322        ));
1323        let mut prov_exp = GLAccount::new(
1324            provision_accounts::PROVISION_EXPENSE.to_string(),
1325            "Provision Expense".to_string(),
1326            AccountType::Expense,
1327            AccountSubType::OperatingExpenses,
1328        );
1329        prov_exp.requires_cost_center = true;
1330        coa.add_account(prov_exp);
1331    }
1332
1333    /// Seed dividend accounts.
1334    fn seed_dividend_accounts(coa: &mut ChartOfAccounts) {
1335        coa.add_account(GLAccount::new(
1336            dividend_accounts::DIVIDENDS_PAYABLE.to_string(),
1337            "Dividends Payable".to_string(),
1338            AccountType::Liability,
1339            AccountSubType::OtherLiabilities,
1340        ));
1341        coa.add_account(GLAccount::new(
1342            dividend_accounts::DIVIDENDS_DECLARED.to_string(),
1343            "Dividends Declared".to_string(),
1344            AccountType::Equity,
1345            AccountSubType::RetainedEarnings,
1346        ));
1347    }
1348
1349    /// Seed pension and share-based compensation accounts.
1350    fn seed_compensation_accounts(coa: &mut ChartOfAccounts) {
1351        coa.add_account(GLAccount::new(
1352            liability_accounts::NET_PENSION_LIABILITY.to_string(),
1353            "Net Pension Liability".to_string(),
1354            AccountType::Liability,
1355            AccountSubType::PensionLiabilities,
1356        ));
1357        coa.add_account(GLAccount::new(
1358            equity_accounts::OCI_REMEASUREMENTS.to_string(),
1359            "OCI — Pension Remeasurements".to_string(),
1360            AccountType::Equity,
1361            AccountSubType::OtherComprehensiveIncome,
1362        ));
1363        coa.add_account(GLAccount::new(
1364            equity_accounts::APIC_STOCK_COMP.to_string(),
1365            "APIC — Stock Compensation".to_string(),
1366            AccountType::Equity,
1367            AccountSubType::AdditionalPaidInCapital,
1368        ));
1369        let mut pension_exp = GLAccount::new(
1370            expense_accounts::PENSION_EXPENSE.to_string(),
1371            "Pension Expense".to_string(),
1372            AccountType::Expense,
1373            AccountSubType::OperatingExpenses,
1374        );
1375        pension_exp.requires_cost_center = true;
1376        coa.add_account(pension_exp);
1377        let mut stock_comp = GLAccount::new(
1378            expense_accounts::STOCK_COMP_EXPENSE.to_string(),
1379            "Stock-Based Compensation Expense".to_string(),
1380            AccountType::Expense,
1381            AccountSubType::OperatingExpenses,
1382        );
1383        stock_comp.requires_cost_center = true;
1384        coa.add_account(stock_comp);
1385    }
1386
1387    /// Seed inventory subledger sub-accounts beyond the GL control (1200).
1388    fn seed_inventory_subledger_accounts(coa: &mut ChartOfAccounts) {
1389        coa.add_account(GLAccount::new(
1390            inventory_accounts::WRITEUP_INCOME.to_string(),
1391            "Inventory Write-up Income".to_string(),
1392            AccountType::Revenue,
1393            AccountSubType::OtherIncome,
1394        ));
1395        let mut writedown = GLAccount::new(
1396            inventory_accounts::WRITEDOWN_EXPENSE.to_string(),
1397            "Inventory Write-down Expense".to_string(),
1398            AccountType::Expense,
1399            AccountSubType::CostOfGoodsSold,
1400        );
1401        writedown.requires_cost_center = true;
1402        coa.add_account(writedown);
1403    }
1404
1405    /// Seed tax accounts not already covered by `seed_canonical_accounts`.
1406    fn seed_additional_tax_accounts(coa: &mut ChartOfAccounts) {
1407        coa.add_account(GLAccount::new(
1408            tax_accounts::INCOME_TAX_PAYABLE.to_string(),
1409            "Income Tax Payable".to_string(),
1410            AccountType::Liability,
1411            AccountSubType::TaxLiabilities,
1412        ));
1413        coa.add_account(GLAccount::new(
1414            tax_accounts::TAX_RECEIVABLE.to_string(),
1415            "Tax Receivable".to_string(),
1416            AccountType::Asset,
1417            AccountSubType::OtherReceivables,
1418        ));
1419    }
1420
1421    /// Seed dormant / blocked legacy accounts.
1422    ///
1423    /// These mirror the targets of the `DormantAccountActivity` anomaly
1424    /// strategy in `datasynth_generators::anomaly::strategies`. Real
1425    /// COAs retain such accounts (legacy suspense, predecessor-system
1426    /// clearing, obsolete migration accounts, retained QA / test
1427    /// accounts) as `is_blocked = true` so audit trails remain
1428    /// resolvable; we follow the same pattern. With `is_postable =
1429    /// false`, normal generators won't pick them up — only the anomaly
1430    /// strategy does, which is exactly the fraud-signature behaviour
1431    /// being modelled.
1432    fn seed_dormant_accounts(coa: &mut ChartOfAccounts) {
1433        let entries = [
1434            (
1435                dormant_accounts::LEGACY_SUSPENSE,
1436                "Legacy Suspense (migrated)",
1437                AccountType::Asset,
1438                AccountSubType::SuspenseClearing,
1439            ),
1440            (
1441                dormant_accounts::LEGACY_CLEARING,
1442                "Legacy Clearing (predecessor system)",
1443                AccountType::Liability,
1444                AccountSubType::OtherLiabilities,
1445            ),
1446            (
1447                dormant_accounts::OBSOLETE,
1448                "Obsolete Account",
1449                AccountType::Equity,
1450                AccountSubType::OtherComprehensiveIncome,
1451            ),
1452            (
1453                dormant_accounts::TEST_ACCOUNT,
1454                "Test Account (QA residue)",
1455                AccountType::Asset,
1456                AccountSubType::SuspenseClearing,
1457            ),
1458        ];
1459        for (number, name, ty, sub) in entries {
1460            let mut acct = GLAccount::new(number.to_string(), name.to_string(), ty, sub);
1461            acct.is_blocked = true;
1462            acct.is_postable = false;
1463            acct.is_suspense_account = matches!(sub, AccountSubType::SuspenseClearing);
1464            coa.add_account(acct);
1465        }
1466    }
1467
1468    /// Seed equity accounts not already covered by `seed_canonical_accounts`.
1469    fn seed_additional_equity_accounts(coa: &mut ChartOfAccounts) {
1470        coa.add_account(GLAccount::new(
1471            equity_accounts::INCOME_SUMMARY.to_string(),
1472            "Income Summary".to_string(),
1473            AccountType::Equity,
1474            AccountSubType::NetIncome,
1475        ));
1476        coa.add_account(GLAccount::new(
1477            equity_accounts::DIVIDENDS_PAID.to_string(),
1478            "Dividends Paid".to_string(),
1479            AccountType::Equity,
1480            AccountSubType::RetainedEarnings,
1481        ));
1482    }
1483
1484    fn generate_asset_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1485        let sub_types = vec![
1486            (AccountSubType::Cash, "Cash", 0.15),
1487            (
1488                AccountSubType::AccountsReceivable,
1489                "Accounts Receivable",
1490                0.20,
1491            ),
1492            (AccountSubType::Inventory, "Inventory", 0.15),
1493            (AccountSubType::PrepaidExpenses, "Prepaid Expenses", 0.10),
1494            (AccountSubType::FixedAssets, "Fixed Assets", 0.25),
1495            (
1496                AccountSubType::AccumulatedDepreciation,
1497                "Accumulated Depreciation",
1498                0.10,
1499            ),
1500            (AccountSubType::OtherAssets, "Other Assets", 0.05),
1501        ];
1502
1503        let mut account_num = 100000u32;
1504        for (sub_type, name_prefix, weight) in sub_types {
1505            let sub_count = ((count as f64) * weight).round() as usize;
1506            for i in 0..sub_count.max(1) {
1507                let account = GLAccount::new(
1508                    format!("{account_num}"),
1509                    format!("{} {}", name_prefix, i + 1),
1510                    AccountType::Asset,
1511                    sub_type,
1512                );
1513                coa.add_account(account);
1514                account_num += 10;
1515            }
1516        }
1517    }
1518
1519    fn generate_liability_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1520        let sub_types = vec![
1521            (AccountSubType::AccountsPayable, "Accounts Payable", 0.25),
1522            (
1523                AccountSubType::AccruedLiabilities,
1524                "Accrued Liabilities",
1525                0.20,
1526            ),
1527            (AccountSubType::ShortTermDebt, "Short-Term Debt", 0.15),
1528            (AccountSubType::LongTermDebt, "Long-Term Debt", 0.15),
1529            (AccountSubType::DeferredRevenue, "Deferred Revenue", 0.15),
1530            (AccountSubType::TaxLiabilities, "Tax Liabilities", 0.10),
1531        ];
1532
1533        let mut account_num = 200000u32;
1534        for (sub_type, name_prefix, weight) in sub_types {
1535            let sub_count = ((count as f64) * weight).round() as usize;
1536            for i in 0..sub_count.max(1) {
1537                let account = GLAccount::new(
1538                    format!("{account_num}"),
1539                    format!("{} {}", name_prefix, i + 1),
1540                    AccountType::Liability,
1541                    sub_type,
1542                );
1543                coa.add_account(account);
1544                account_num += 10;
1545            }
1546        }
1547    }
1548
1549    fn generate_equity_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1550        let sub_types = vec![
1551            (AccountSubType::CommonStock, "Common Stock", 0.20),
1552            (AccountSubType::RetainedEarnings, "Retained Earnings", 0.30),
1553            (AccountSubType::AdditionalPaidInCapital, "APIC", 0.20),
1554            (AccountSubType::OtherComprehensiveIncome, "OCI", 0.30),
1555        ];
1556
1557        let mut account_num = 300000u32;
1558        for (sub_type, name_prefix, weight) in sub_types {
1559            let sub_count = ((count as f64) * weight).round() as usize;
1560            for i in 0..sub_count.max(1) {
1561                let account = GLAccount::new(
1562                    format!("{account_num}"),
1563                    format!("{} {}", name_prefix, i + 1),
1564                    AccountType::Equity,
1565                    sub_type,
1566                );
1567                coa.add_account(account);
1568                account_num += 10;
1569            }
1570        }
1571    }
1572
1573    fn generate_revenue_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1574        let sub_types = vec![
1575            (AccountSubType::ProductRevenue, "Product Revenue", 0.40),
1576            (AccountSubType::ServiceRevenue, "Service Revenue", 0.30),
1577            (AccountSubType::InterestIncome, "Interest Income", 0.10),
1578            (AccountSubType::OtherIncome, "Other Income", 0.20),
1579        ];
1580
1581        let mut account_num = 400000u32;
1582        for (sub_type, name_prefix, weight) in sub_types {
1583            let sub_count = ((count as f64) * weight).round() as usize;
1584            for i in 0..sub_count.max(1) {
1585                let account = GLAccount::new(
1586                    format!("{account_num}"),
1587                    format!("{} {}", name_prefix, i + 1),
1588                    AccountType::Revenue,
1589                    sub_type,
1590                );
1591                coa.add_account(account);
1592                account_num += 10;
1593            }
1594        }
1595    }
1596
1597    fn generate_expense_accounts(&mut self, coa: &mut ChartOfAccounts, count: usize) {
1598        let sub_types = vec![
1599            (AccountSubType::CostOfGoodsSold, "COGS", 0.20),
1600            (
1601                AccountSubType::OperatingExpenses,
1602                "Operating Expenses",
1603                0.25,
1604            ),
1605            (AccountSubType::SellingExpenses, "Selling Expenses", 0.15),
1606            (
1607                AccountSubType::AdministrativeExpenses,
1608                "Admin Expenses",
1609                0.15,
1610            ),
1611            (AccountSubType::DepreciationExpense, "Depreciation", 0.10),
1612            (AccountSubType::InterestExpense, "Interest Expense", 0.05),
1613            (AccountSubType::TaxExpense, "Tax Expense", 0.05),
1614            (AccountSubType::OtherExpenses, "Other Expenses", 0.05),
1615        ];
1616
1617        let mut account_num = 500000u32;
1618        for (sub_type, name_prefix, weight) in sub_types {
1619            let sub_count = ((count as f64) * weight).round() as usize;
1620            for i in 0..sub_count.max(1) {
1621                let mut account = GLAccount::new(
1622                    format!("{account_num}"),
1623                    format!("{} {}", name_prefix, i + 1),
1624                    AccountType::Expense,
1625                    sub_type,
1626                );
1627                account.requires_cost_center = true;
1628                coa.add_account(account);
1629                account_num += 10;
1630            }
1631        }
1632    }
1633
1634    fn generate_suspense_accounts(&mut self, coa: &mut ChartOfAccounts) {
1635        let suspense_types = vec![
1636            (AccountSubType::SuspenseClearing, "Suspense Clearing"),
1637            (AccountSubType::GoodsReceivedClearing, "GR/IR Clearing"),
1638            (AccountSubType::BankClearing, "Bank Clearing"),
1639            (
1640                AccountSubType::IntercompanyClearing,
1641                "Intercompany Clearing",
1642            ),
1643        ];
1644
1645        let mut account_num = 199000u32;
1646        for (sub_type, name) in suspense_types {
1647            let mut account = GLAccount::new(
1648                format!("{account_num}"),
1649                name.to_string(),
1650                AccountType::Asset,
1651                sub_type,
1652            );
1653            account.is_suspense_account = true;
1654            coa.add_account(account);
1655            account_num += 100;
1656        }
1657    }
1658}
1659
1660impl Generator for ChartOfAccountsGenerator {
1661    type Item = ChartOfAccounts;
1662    type Config = (CoAComplexity, IndustrySector);
1663
1664    fn new(config: Self::Config, seed: u64) -> Self {
1665        Self::new(config.0, config.1, seed)
1666    }
1667
1668    fn generate_one(&mut self) -> Self::Item {
1669        self.generate()
1670    }
1671
1672    fn reset(&mut self) {
1673        self.rng = seeded_rng(self.seed, 0);
1674        self.count = 0;
1675        self.coa_framework = CoAFramework::UsGaap;
1676    }
1677
1678    fn count(&self) -> u64 {
1679        self.count
1680    }
1681
1682    fn seed(&self) -> u64 {
1683        self.seed
1684    }
1685}
1686
1687#[cfg(test)]
1688#[allow(clippy::unwrap_used)]
1689mod tests {
1690    use super::*;
1691
1692    #[test]
1693    fn test_generate_small_coa() {
1694        let mut gen =
1695            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42);
1696        let coa = gen.generate();
1697
1698        assert!(coa.account_count() >= 50);
1699        assert!(!coa.get_suspense_accounts().is_empty());
1700    }
1701
1702    #[test]
1703    fn test_generate_pcg_coa() {
1704        let mut gen =
1705            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
1706                .with_french_pcg(true);
1707        let coa = gen.generate();
1708
1709        assert_eq!(coa.country, "FR");
1710        assert!(coa.name.contains("Plan Comptable") || coa.name.contains("PCG"));
1711        assert!(coa.account_count() >= 20);
1712        // PCG accounts are 6-digit (e.g. 411000, 601000)
1713        let first = coa.accounts.first().expect("has accounts");
1714        assert_eq!(first.account_number.len(), 6);
1715    }
1716
1717    /// Verifies PCG (Plan Comptable Général) account structure: 6-digit format,
1718    /// all accounts numeric, and coverage of at least two PCG classes (1–8).
1719    /// Works for both the embedded PCG 2024 loader and the fallback generator.
1720    #[test]
1721    fn test_pcg_account_structure() {
1722        let mut gen =
1723            ChartOfAccountsGenerator::new(CoAComplexity::Small, IndustrySector::Manufacturing, 42)
1724                .with_french_pcg(true);
1725        let coa = gen.generate();
1726
1727        assert_eq!(
1728            coa.account_format, "######",
1729            "PCG uses 6-digit account format"
1730        );
1731        assert!(
1732            coa.account_count() >= 20,
1733            "PCG CoA has minimum account count"
1734        );
1735
1736        let account_numbers: Vec<_> = coa
1737            .accounts
1738            .iter()
1739            .map(|a| a.account_number.as_str())
1740            .collect();
1741        for num in &account_numbers {
1742            assert_eq!(num.len(), 6, "every PCG account is 6 digits: {}", num);
1743            assert!(
1744                num.chars().all(|c| c.is_ascii_digit()),
1745                "PCG account is numeric: {}",
1746                num
1747            );
1748        }
1749
1750        // All account numbers must belong to a PCG class (first digit 1–8)
1751        let first_digits: std::collections::HashSet<char> = account_numbers
1752            .iter()
1753            .filter_map(|s| s.chars().next())
1754            .collect();
1755        let pcg_classes: std::collections::HashSet<_> =
1756            ['1', '2', '3', '4', '5', '6', '7', '8'].into();
1757        assert!(
1758            !first_digits.is_empty() && first_digits.is_subset(&pcg_classes),
1759            "PCG account numbers must be in classes 1–8, got first digits: {:?}",
1760            first_digits
1761        );
1762    }
1763}