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