Skip to main content

datasynth_generators/
coa_generator.rs

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