Skip to main content

datasynth_core/
framework_accounts.rs

1//! Centralized framework-aware account mapping.
2//!
3//! `FrameworkAccounts` maps ~45 semantic account purposes to framework-specific
4//! GL account codes. Adding a new accounting framework requires writing one
5//! constructor and one constants module — all downstream consumers use the
6//! same struct.
7
8use std::sync::Arc;
9
10use crate::accounts::AccountCategory;
11use crate::models::balance::AccountCategory as TrialBalanceCategory;
12use crate::models::balance::AccountType;
13
14/// Audit export configuration flags.
15#[derive(Debug, Clone, Default)]
16pub struct AuditExportConfig {
17    /// French FEC (Fichier des Écritures Comptables), Art. A47 A-1 LPF.
18    pub fec_enabled: bool,
19    /// German GoBD (Grundsätze zur ordnungsmäßigen Führung und Aufbewahrung
20    /// von Büchern, Aufzeichnungen und Unterlagen in elektronischer Form).
21    pub gobd_enabled: bool,
22}
23
24/// Maps semantic account purposes to framework-specific GL codes.
25///
26/// Downstream generators use field names (`ar_control`, `cogs`, …) instead of
27/// hard-coded account numbers, making them framework-agnostic.
28#[derive(Clone)]
29pub struct FrameworkAccounts {
30    // ── Control ──────────────────────────────────────────────────────
31    pub ar_control: String,
32    pub ap_control: String,
33    pub inventory: String,
34    pub fixed_assets: String,
35    pub accumulated_depreciation: String,
36    pub gr_ir_clearing: String,
37    pub ic_ar_clearing: String,
38    pub ic_ap_clearing: String,
39
40    // ── Cash ─────────────────────────────────────────────────────────
41    pub operating_cash: String,
42    pub bank_account: String,
43    pub petty_cash: String,
44
45    // ── Revenue ──────────────────────────────────────────────────────
46    pub product_revenue: String,
47    pub service_revenue: String,
48    pub ic_revenue: String,
49    pub purchase_discount_income: String,
50    pub other_revenue: String,
51    pub sales_discounts: String,
52    pub sales_returns: String,
53
54    // ── Expense ──────────────────────────────────────────────────────
55    pub cogs: String,
56    pub raw_materials: String,
57    pub depreciation_expense: String,
58    pub salaries_wages: String,
59    pub rent: String,
60    pub interest_expense: String,
61    pub purchase_discounts: String,
62    pub fx_gain_loss: String,
63    pub bad_debt: String,
64
65    // ── Tax ──────────────────────────────────────────────────────────
66    pub sales_tax_payable: String,
67    pub vat_payable: String,
68    pub input_vat: String,
69    pub tax_receivable: String,
70    pub tax_expense: String,
71    pub deferred_tax_liability: String,
72    pub deferred_tax_asset: String,
73
74    // ── Liability ────────────────────────────────────────────────────
75    pub accrued_expenses: String,
76    pub accrued_salaries: String,
77    pub unearned_revenue: String,
78    pub short_term_debt: String,
79    pub long_term_debt: String,
80    pub ic_payable: String,
81
82    // ── Equity ───────────────────────────────────────────────────────
83    pub common_stock: String,
84    pub retained_earnings: String,
85    pub current_year_earnings: String,
86    pub cta: String,
87    pub income_summary: String,
88    pub dividends_paid: String,
89
90    // ── Suspense / Clearing ──────────────────────────────────────────
91    pub general_suspense: String,
92    pub payroll_clearing: String,
93    pub bank_reconciliation_suspense: String,
94
95    // ── HGB-specific ─────────────────────────────────────────────────
96    pub provisions: String,
97
98    // ── Audit export flags ───────────────────────────────────────────
99    pub audit_export: AuditExportConfig,
100
101    // ── Classification function ──────────────────────────────────────
102    classifier: Arc<dyn Fn(&str) -> AccountCategory + Send + Sync>,
103}
104
105impl std::fmt::Debug for FrameworkAccounts {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.debug_struct("FrameworkAccounts")
108            .field("ar_control", &self.ar_control)
109            .field("ap_control", &self.ap_control)
110            .field("audit_export", &self.audit_export)
111            .finish_non_exhaustive()
112    }
113}
114
115impl FrameworkAccounts {
116    /// Classify an account number using this framework's rules.
117    pub fn classify(&self, account: &str) -> AccountCategory {
118        (self.classifier)(account)
119    }
120
121    /// Classify an account code into an [`AccountType`] using this framework's rules.
122    ///
123    /// Maps the framework-specific [`AccountCategory`] to the balance-sheet
124    /// oriented [`AccountType`] (Asset, Liability, Equity, Revenue, Expense).
125    pub fn classify_account_type(&self, account_code: &str) -> AccountType {
126        account_type_from_category(self.classify(account_code))
127    }
128
129    /// Classify an account code into a [`TrialBalanceCategory`] using this
130    /// framework's rules.
131    ///
132    /// Provides the finer-grained trial-balance grouping (CurrentAssets,
133    /// NonCurrentAssets, etc.) derived from the framework classifier.
134    pub fn classify_trial_balance_category(&self, account_code: &str) -> TrialBalanceCategory {
135        trial_balance_category_from_category(self.classify(account_code))
136    }
137
138    /// US GAAP (default) — 4-digit accounts from `crate::accounts`.
139    pub fn us_gaap() -> Self {
140        use crate::accounts::*;
141        Self {
142            ar_control: control_accounts::AR_CONTROL.into(),
143            ap_control: control_accounts::AP_CONTROL.into(),
144            inventory: control_accounts::INVENTORY.into(),
145            fixed_assets: control_accounts::FIXED_ASSETS.into(),
146            accumulated_depreciation: control_accounts::ACCUMULATED_DEPRECIATION.into(),
147            gr_ir_clearing: control_accounts::GR_IR_CLEARING.into(),
148            ic_ar_clearing: control_accounts::IC_AR_CLEARING.into(),
149            ic_ap_clearing: control_accounts::IC_AP_CLEARING.into(),
150
151            operating_cash: cash_accounts::OPERATING_CASH.into(),
152            bank_account: cash_accounts::BANK_ACCOUNT.into(),
153            petty_cash: cash_accounts::PETTY_CASH.into(),
154
155            product_revenue: revenue_accounts::PRODUCT_REVENUE.into(),
156            service_revenue: revenue_accounts::SERVICE_REVENUE.into(),
157            ic_revenue: revenue_accounts::IC_REVENUE.into(),
158            purchase_discount_income: revenue_accounts::PURCHASE_DISCOUNT_INCOME.into(),
159            other_revenue: revenue_accounts::OTHER_REVENUE.into(),
160            sales_discounts: revenue_accounts::SALES_DISCOUNTS.into(),
161            sales_returns: revenue_accounts::SALES_RETURNS.into(),
162
163            cogs: expense_accounts::COGS.into(),
164            raw_materials: expense_accounts::RAW_MATERIALS.into(),
165            depreciation_expense: expense_accounts::DEPRECIATION.into(),
166            salaries_wages: expense_accounts::SALARIES_WAGES.into(),
167            rent: expense_accounts::RENT.into(),
168            interest_expense: expense_accounts::INTEREST_EXPENSE.into(),
169            purchase_discounts: expense_accounts::PURCHASE_DISCOUNTS.into(),
170            fx_gain_loss: expense_accounts::FX_GAIN_LOSS.into(),
171            bad_debt: expense_accounts::BAD_DEBT.into(),
172
173            sales_tax_payable: tax_accounts::SALES_TAX_PAYABLE.into(),
174            vat_payable: tax_accounts::VAT_PAYABLE.into(),
175            input_vat: tax_accounts::INPUT_VAT.into(),
176            tax_receivable: tax_accounts::TAX_RECEIVABLE.into(),
177            tax_expense: tax_accounts::TAX_EXPENSE.into(),
178            deferred_tax_liability: tax_accounts::DEFERRED_TAX_LIABILITY.into(),
179            deferred_tax_asset: tax_accounts::DEFERRED_TAX_ASSET.into(),
180
181            accrued_expenses: liability_accounts::ACCRUED_EXPENSES.into(),
182            accrued_salaries: liability_accounts::ACCRUED_SALARIES.into(),
183            unearned_revenue: liability_accounts::UNEARNED_REVENUE.into(),
184            short_term_debt: liability_accounts::SHORT_TERM_DEBT.into(),
185            long_term_debt: liability_accounts::LONG_TERM_DEBT.into(),
186            ic_payable: liability_accounts::IC_PAYABLE.into(),
187
188            common_stock: equity_accounts::COMMON_STOCK.into(),
189            retained_earnings: equity_accounts::RETAINED_EARNINGS.into(),
190            current_year_earnings: equity_accounts::CURRENT_YEAR_EARNINGS.into(),
191            cta: equity_accounts::CTA.into(),
192            income_summary: equity_accounts::INCOME_SUMMARY.into(),
193            dividends_paid: equity_accounts::DIVIDENDS_PAID.into(),
194
195            general_suspense: suspense_accounts::GENERAL_SUSPENSE.into(),
196            payroll_clearing: suspense_accounts::PAYROLL_CLEARING.into(),
197            bank_reconciliation_suspense: suspense_accounts::BANK_RECONCILIATION_SUSPENSE.into(),
198
199            provisions: liability_accounts::ACCRUED_EXPENSES.into(), // US GAAP: no separate provisions class
200
201            audit_export: AuditExportConfig::default(),
202            classifier: Arc::new(us_gaap_classify),
203        }
204    }
205
206    /// IFRS — uses the same numbering conventions as US GAAP.
207    pub fn ifrs() -> Self {
208        Self::us_gaap()
209    }
210
211    /// French GAAP (PCG) — 6-digit accounts from `crate::pcg`.
212    pub fn french_gaap() -> Self {
213        use crate::pcg::*;
214        Self {
215            ar_control: control_accounts::AR_CONTROL.into(),
216            ap_control: control_accounts::AP_CONTROL.into(),
217            inventory: control_accounts::INVENTORY.into(),
218            fixed_assets: control_accounts::FIXED_ASSETS.into(),
219            accumulated_depreciation: control_accounts::ACCUMULATED_DEPRECIATION.into(),
220            gr_ir_clearing: control_accounts::GR_IR_CLEARING.into(),
221            ic_ar_clearing: control_accounts::IC_AR_CLEARING.into(),
222            ic_ap_clearing: control_accounts::IC_AP_CLEARING.into(),
223
224            operating_cash: cash_accounts::OPERATING_CASH.into(),
225            bank_account: cash_accounts::BANK_ACCOUNT.into(),
226            petty_cash: cash_accounts::PETTY_CASH.into(),
227
228            product_revenue: revenue_accounts::PRODUCT_REVENUE.into(),
229            service_revenue: revenue_accounts::SERVICE_REVENUE.into(),
230            ic_revenue: additional_revenue::IC_REVENUE.into(),
231            purchase_discount_income: additional_revenue::PURCHASE_DISCOUNT_INCOME.into(),
232            other_revenue: revenue_accounts::OTHER_REVENUE.into(),
233            sales_discounts: revenue_accounts::SALES_DISCOUNTS.into(),
234            sales_returns: additional_revenue::SALES_RETURNS.into(),
235
236            cogs: expense_accounts::COGS.into(),
237            raw_materials: additional_expense::RAW_MATERIALS.into(),
238            depreciation_expense: expense_accounts::DEPRECIATION.into(),
239            salaries_wages: expense_accounts::SALARIES_WAGES.into(),
240            rent: expense_accounts::RENT.into(),
241            interest_expense: expense_accounts::INTEREST_EXPENSE.into(),
242            purchase_discounts: additional_expense::PURCHASE_DISCOUNTS.into(),
243            fx_gain_loss: additional_expense::FX_GAIN_LOSS.into(),
244            bad_debt: additional_expense::BAD_DEBT.into(),
245
246            sales_tax_payable: tax_accounts::OUTPUT_VAT.into(),
247            vat_payable: tax_accounts::OUTPUT_VAT.into(),
248            input_vat: tax_accounts::INPUT_VAT.into(),
249            tax_receivable: tax_accounts::TAX_RECEIVABLE.into(),
250            tax_expense: tax_accounts::TAX_EXPENSE.into(),
251            deferred_tax_liability: tax_accounts::DEFERRED_TAX_LIABILITY.into(),
252            deferred_tax_asset: tax_accounts::DEFERRED_TAX_ASSET.into(),
253
254            accrued_expenses: liability_accounts::ACCRUED_EXPENSES.into(),
255            accrued_salaries: liability_accounts::ACCRUED_SALARIES.into(),
256            unearned_revenue: liability_accounts::UNEARNED_REVENUE.into(),
257            short_term_debt: equity_liability_accounts::SHORT_TERM_DEBT.into(),
258            long_term_debt: equity_liability_accounts::LONG_TERM_DEBT.into(),
259            ic_payable: liability_accounts::IC_PAYABLE.into(),
260
261            common_stock: equity_liability_accounts::COMMON_STOCK.into(),
262            retained_earnings: equity_liability_accounts::RETAINED_EARNINGS.into(),
263            current_year_earnings: equity_accounts::CURRENT_YEAR_EARNINGS.into(),
264            cta: equity_accounts::CTA.into(),
265            income_summary: equity_accounts::INCOME_SUMMARY.into(),
266            dividends_paid: equity_accounts::DIVIDENDS_PAID.into(),
267
268            general_suspense: suspense_accounts::GENERAL_SUSPENSE.into(),
269            payroll_clearing: suspense_accounts::PAYROLL_CLEARING.into(),
270            bank_reconciliation_suspense: suspense_accounts::GENERAL_SUSPENSE.into(), // PCG uses 471000 for general suspense
271
272            provisions: equity_liability_accounts::PROVISIONS.into(),
273
274            audit_export: AuditExportConfig {
275                fec_enabled: true,
276                gobd_enabled: false,
277            },
278            classifier: Arc::new(pcg_classify),
279        }
280    }
281
282    /// German GAAP (HGB) — 4-digit SKR04 accounts from `crate::skr`.
283    pub fn german_gaap() -> Self {
284        use crate::skr::*;
285        Self {
286            ar_control: control_accounts::AR_CONTROL.into(),
287            ap_control: control_accounts::AP_CONTROL.into(),
288            inventory: control_accounts::INVENTORY.into(),
289            fixed_assets: control_accounts::FIXED_ASSETS.into(),
290            accumulated_depreciation: control_accounts::ACCUMULATED_DEPRECIATION.into(),
291            gr_ir_clearing: control_accounts::GR_IR_CLEARING.into(),
292            ic_ar_clearing: control_accounts::IC_AR_CLEARING.into(),
293            ic_ap_clearing: control_accounts::IC_AP_CLEARING.into(),
294
295            operating_cash: cash_accounts::OPERATING_CASH.into(),
296            bank_account: cash_accounts::BANK_ACCOUNT.into(),
297            petty_cash: cash_accounts::PETTY_CASH.into(),
298
299            product_revenue: revenue_accounts::PRODUCT_REVENUE.into(),
300            service_revenue: revenue_accounts::SERVICE_REVENUE.into(),
301            ic_revenue: revenue_accounts::IC_REVENUE.into(),
302            purchase_discount_income: revenue_accounts::PURCHASE_DISCOUNT_INCOME.into(),
303            other_revenue: revenue_accounts::OTHER_REVENUE.into(),
304            sales_discounts: revenue_accounts::SALES_DISCOUNTS.into(),
305            sales_returns: revenue_accounts::SALES_RETURNS.into(),
306
307            cogs: expense_accounts::COGS.into(),
308            raw_materials: expense_accounts::RAW_MATERIALS.into(),
309            depreciation_expense: expense_accounts::DEPRECIATION.into(),
310            salaries_wages: expense_accounts::SALARIES_WAGES.into(),
311            rent: expense_accounts::RENT.into(),
312            interest_expense: expense_accounts::INTEREST_EXPENSE.into(),
313            purchase_discounts: expense_accounts::PURCHASE_DISCOUNTS.into(),
314            fx_gain_loss: expense_accounts::FX_GAIN_LOSS.into(),
315            bad_debt: expense_accounts::BAD_DEBT.into(),
316
317            sales_tax_payable: tax_accounts::OUTPUT_VAT.into(),
318            vat_payable: tax_accounts::OUTPUT_VAT.into(),
319            input_vat: tax_accounts::INPUT_VAT.into(),
320            tax_receivable: tax_accounts::TAX_RECEIVABLE.into(),
321            tax_expense: tax_accounts::TAX_EXPENSE.into(),
322            deferred_tax_liability: tax_accounts::DEFERRED_TAX_LIABILITY.into(),
323            deferred_tax_asset: tax_accounts::DEFERRED_TAX_ASSET.into(),
324
325            accrued_expenses: liability_accounts::ACCRUED_EXPENSES.into(),
326            accrued_salaries: liability_accounts::ACCRUED_SALARIES.into(),
327            unearned_revenue: liability_accounts::UNEARNED_REVENUE.into(),
328            short_term_debt: liability_accounts::SHORT_TERM_DEBT.into(),
329            long_term_debt: liability_accounts::LONG_TERM_DEBT.into(),
330            ic_payable: liability_accounts::IC_PAYABLE.into(),
331
332            common_stock: equity_accounts::COMMON_STOCK.into(),
333            retained_earnings: equity_accounts::RETAINED_EARNINGS.into(),
334            current_year_earnings: equity_accounts::CURRENT_YEAR_EARNINGS.into(),
335            cta: equity_accounts::CTA.into(),
336            income_summary: equity_accounts::INCOME_SUMMARY.into(),
337            dividends_paid: equity_accounts::DIVIDENDS_PAID.into(),
338
339            general_suspense: suspense_accounts::GENERAL_SUSPENSE.into(),
340            payroll_clearing: suspense_accounts::PAYROLL_CLEARING.into(),
341            bank_reconciliation_suspense: suspense_accounts::BANK_RECONCILIATION_SUSPENSE.into(),
342
343            provisions: equity_accounts::PROVISIONS.into(),
344
345            audit_export: AuditExportConfig {
346                fec_enabled: false,
347                gobd_enabled: true,
348            },
349            classifier: Arc::new(skr04_classify),
350        }
351    }
352
353    /// Select accounts for a given `AccountingFramework` from `datasynth-standards`.
354    ///
355    /// This is the primary entry point — other code should not hard-code
356    /// framework detection logic.
357    pub fn for_framework(framework: &str) -> Self {
358        match framework {
359            "us_gaap" | "UsGaap" => Self::us_gaap(),
360            "ifrs" | "Ifrs" => Self::ifrs(),
361            "dual_reporting" | "DualReporting" => Self::us_gaap(),
362            "french_gaap" | "FrenchGaap" => Self::french_gaap(),
363            "german_gaap" | "GermanGaap" | "hgb" => Self::german_gaap(),
364            other => {
365                eprintln!(
366                    "FrameworkAccounts::for_framework: unknown framework {:?}, defaulting to US GAAP",
367                    other
368                );
369                Self::us_gaap()
370            }
371        }
372    }
373}
374
375// ── Conversion helpers ──────────────────────────────────────────────────
376
377/// Convert a framework [`AccountCategory`] to an [`AccountType`].
378fn account_type_from_category(cat: AccountCategory) -> AccountType {
379    match cat {
380        AccountCategory::Asset => AccountType::Asset,
381        AccountCategory::Liability => AccountType::Liability,
382        AccountCategory::Equity => AccountType::Equity,
383        AccountCategory::Revenue => AccountType::Revenue,
384        AccountCategory::Cogs
385        | AccountCategory::OperatingExpense
386        | AccountCategory::OtherIncomeExpense
387        | AccountCategory::Tax => AccountType::Expense,
388        AccountCategory::Suspense | AccountCategory::Unknown => AccountType::Asset,
389    }
390}
391
392/// Convert a framework [`AccountCategory`] to a [`TrialBalanceCategory`].
393fn trial_balance_category_from_category(cat: AccountCategory) -> TrialBalanceCategory {
394    match cat {
395        AccountCategory::Asset => TrialBalanceCategory::CurrentAssets,
396        AccountCategory::Liability => TrialBalanceCategory::CurrentLiabilities,
397        AccountCategory::Equity => TrialBalanceCategory::Equity,
398        AccountCategory::Revenue => TrialBalanceCategory::Revenue,
399        AccountCategory::Cogs => TrialBalanceCategory::CostOfGoodsSold,
400        AccountCategory::OperatingExpense => TrialBalanceCategory::OperatingExpenses,
401        AccountCategory::OtherIncomeExpense => TrialBalanceCategory::OtherExpenses,
402        AccountCategory::Tax => TrialBalanceCategory::OtherExpenses,
403        AccountCategory::Suspense | AccountCategory::Unknown => TrialBalanceCategory::OtherExpenses,
404    }
405}
406
407// ── Classification functions ─────────────────────────────────────────────
408
409/// US GAAP: first digit maps directly to category.
410///   1=Asset, 2=Liability, 3=Equity, 4=Revenue, 5=COGS,
411///   6=OpEx, 7=Other, 8=Tax, 9=Suspense
412fn us_gaap_classify(account: &str) -> AccountCategory {
413    AccountCategory::from_account(account)
414}
415
416/// French PCG: classes 1-9 have different semantics.
417///   1=Equity/Liabilities, 2=Fixed Assets, 3=Inventory,
418///   4=Mixed (AP/AR/Personnel), 5=Cash, 6=Expense, 7=Revenue,
419///   8=Special, 9=Analytical
420fn pcg_classify(account: &str) -> AccountCategory {
421    match account.chars().next().and_then(|c| c.to_digit(10)) {
422        Some(1) => {
423            // Class 1: Equity & liabilities — check subclass
424            let sub = account.get(1..2).and_then(|s| s.parse::<u8>().ok());
425            match sub {
426                Some(0..=4) => AccountCategory::Equity, // 10x-14x: capital & reserves
427                Some(5) => AccountCategory::Liability,  // 15x: provisions
428                Some(6..=7) => AccountCategory::Liability, // 16x-17x: debts
429                _ => AccountCategory::Liability,
430            }
431        }
432        Some(2) => AccountCategory::Asset, // Fixed assets
433        Some(3) => AccountCategory::Asset, // Inventory
434        Some(4) => {
435            // Class 4: third parties — check subclass
436            let sub = account.get(1..2).and_then(|s| s.parse::<u8>().ok());
437            match sub {
438                Some(0) => AccountCategory::Liability,     // 40x: suppliers
439                Some(1) => AccountCategory::Asset,         // 41x: customers
440                Some(2) => AccountCategory::Liability,     // 42x: personnel
441                Some(3..=4) => AccountCategory::Liability, // 43-44x: social/tax
442                Some(5) => AccountCategory::Liability,     // 45x: group companies
443                _ => AccountCategory::Liability,
444            }
445        }
446        Some(5) => AccountCategory::Asset, // Cash / financial
447        Some(6) => AccountCategory::OperatingExpense, // Expenses
448        Some(7) => AccountCategory::Revenue, // Revenue
449        Some(8) => AccountCategory::Suspense, // Special
450        Some(9) => AccountCategory::Suspense, // Analytical
451        _ => AccountCategory::Unknown,
452    }
453}
454
455/// German SKR04: classes 0-9 follow the Abschlussgliederungsprinzip.
456///   0=Fixed Assets, 1=Current Assets, 2=Equity, 3=Liabilities,
457///   4=Revenue, 5=COGS/Material, 6=OpEx, 7=Financial, 8=Tax/Extra,
458///   9=Statistical
459fn skr04_classify(account: &str) -> AccountCategory {
460    match account.chars().next().and_then(|c| c.to_digit(10)) {
461        Some(0) => AccountCategory::Asset,              // Fixed assets
462        Some(1) => AccountCategory::Asset,              // Current assets
463        Some(2) => AccountCategory::Equity,             // Equity
464        Some(3) => AccountCategory::Liability,          // Liabilities
465        Some(4) => AccountCategory::Revenue,            // Revenue
466        Some(5) => AccountCategory::Cogs,               // Material / COGS
467        Some(6) => AccountCategory::OperatingExpense,   // Personnel & other OpEx
468        Some(7) => AccountCategory::OtherIncomeExpense, // Financial income/expense
469        Some(8) => AccountCategory::Tax,                // Extraordinary / Tax
470        Some(9) => AccountCategory::Suspense,           // Statistical
471        _ => AccountCategory::Unknown,
472    }
473}
474
475#[cfg(test)]
476#[allow(clippy::unwrap_used)]
477mod tests {
478    use super::*;
479
480    #[test]
481    fn test_framework_accounts_us_gaap_defaults() {
482        use crate::accounts::*;
483        let fa = FrameworkAccounts::us_gaap();
484        assert_eq!(fa.ar_control, control_accounts::AR_CONTROL);
485        assert_eq!(fa.ap_control, control_accounts::AP_CONTROL);
486        assert_eq!(fa.inventory, control_accounts::INVENTORY);
487        assert_eq!(fa.cogs, expense_accounts::COGS);
488        assert_eq!(fa.product_revenue, revenue_accounts::PRODUCT_REVENUE);
489        assert_eq!(fa.retained_earnings, equity_accounts::RETAINED_EARNINGS);
490        assert_eq!(fa.general_suspense, suspense_accounts::GENERAL_SUSPENSE);
491        assert!(!fa.audit_export.fec_enabled);
492        assert!(!fa.audit_export.gobd_enabled);
493    }
494
495    #[test]
496    fn test_framework_accounts_french_gaap() {
497        use crate::pcg;
498        let fa = FrameworkAccounts::french_gaap();
499        assert_eq!(fa.ar_control, pcg::control_accounts::AR_CONTROL);
500        assert_eq!(fa.ap_control, pcg::control_accounts::AP_CONTROL);
501        assert_eq!(fa.inventory, pcg::control_accounts::INVENTORY);
502        assert_eq!(fa.cogs, pcg::expense_accounts::COGS);
503        assert_eq!(fa.product_revenue, pcg::revenue_accounts::PRODUCT_REVENUE);
504        assert!(fa.audit_export.fec_enabled);
505        assert!(!fa.audit_export.gobd_enabled);
506    }
507
508    #[test]
509    fn test_framework_accounts_german_gaap() {
510        use crate::skr;
511        let fa = FrameworkAccounts::german_gaap();
512        assert_eq!(fa.ar_control, skr::control_accounts::AR_CONTROL);
513        assert_eq!(fa.ap_control, skr::control_accounts::AP_CONTROL);
514        assert_eq!(fa.cogs, skr::expense_accounts::COGS);
515        assert!(!fa.audit_export.fec_enabled);
516        assert!(fa.audit_export.gobd_enabled);
517    }
518
519    #[test]
520    fn test_classify_us_gaap() {
521        let fa = FrameworkAccounts::us_gaap();
522        assert_eq!(fa.classify("1100"), AccountCategory::Asset);
523        assert_eq!(fa.classify("2000"), AccountCategory::Liability);
524        assert_eq!(fa.classify("3200"), AccountCategory::Equity);
525        assert_eq!(fa.classify("4000"), AccountCategory::Revenue);
526        assert_eq!(fa.classify("5000"), AccountCategory::Cogs);
527        assert_eq!(fa.classify("6100"), AccountCategory::OperatingExpense);
528        assert_eq!(fa.classify("9000"), AccountCategory::Suspense);
529    }
530
531    #[test]
532    fn test_classify_pcg() {
533        let fa = FrameworkAccounts::french_gaap();
534        assert_eq!(fa.classify("101000"), AccountCategory::Equity);
535        assert_eq!(fa.classify("151000"), AccountCategory::Liability);
536        assert_eq!(fa.classify("210000"), AccountCategory::Asset);
537        assert_eq!(fa.classify("310000"), AccountCategory::Asset);
538        assert_eq!(fa.classify("401000"), AccountCategory::Liability);
539        assert_eq!(fa.classify("411000"), AccountCategory::Asset);
540        assert_eq!(fa.classify("512000"), AccountCategory::Asset);
541        assert_eq!(fa.classify("603000"), AccountCategory::OperatingExpense);
542        assert_eq!(fa.classify("701000"), AccountCategory::Revenue);
543    }
544
545    #[test]
546    fn test_classify_skr04() {
547        let fa = FrameworkAccounts::german_gaap();
548        assert_eq!(fa.classify("0200"), AccountCategory::Asset);
549        assert_eq!(fa.classify("1200"), AccountCategory::Asset);
550        assert_eq!(fa.classify("2000"), AccountCategory::Equity);
551        assert_eq!(fa.classify("3300"), AccountCategory::Liability);
552        assert_eq!(fa.classify("4000"), AccountCategory::Revenue);
553        assert_eq!(fa.classify("5000"), AccountCategory::Cogs);
554        assert_eq!(fa.classify("6000"), AccountCategory::OperatingExpense);
555        assert_eq!(fa.classify("7300"), AccountCategory::OtherIncomeExpense);
556    }
557
558    #[test]
559    fn test_for_framework_dispatch() {
560        let us = FrameworkAccounts::for_framework("us_gaap");
561        assert_eq!(us.ar_control, "1100");
562
563        let fr = FrameworkAccounts::for_framework("french_gaap");
564        assert_eq!(fr.ar_control, "411000");
565
566        let de = FrameworkAccounts::for_framework("german_gaap");
567        assert_eq!(de.ar_control, "1200");
568
569        let hgb = FrameworkAccounts::for_framework("hgb");
570        assert_eq!(hgb.ar_control, "1200");
571
572        // IFRS and dual_reporting dispatch
573        let ifrs = FrameworkAccounts::for_framework("ifrs");
574        assert_eq!(ifrs.ar_control, "1100");
575
576        let dual = FrameworkAccounts::for_framework("dual_reporting");
577        assert_eq!(dual.ar_control, "1100");
578
579        // Unknown framework falls back to US GAAP (with eprintln warning)
580        let unknown = FrameworkAccounts::for_framework("martian_gaap");
581        assert_eq!(unknown.ar_control, "1100");
582    }
583
584    #[test]
585    fn test_ifrs_constructor() {
586        let ifrs = FrameworkAccounts::ifrs();
587        let us = FrameworkAccounts::us_gaap();
588        assert_eq!(ifrs.ar_control, us.ar_control);
589        assert_eq!(ifrs.ap_control, us.ap_control);
590        assert_eq!(ifrs.cogs, us.cogs);
591        assert_eq!(ifrs.product_revenue, us.product_revenue);
592        assert_eq!(ifrs.classify("1100"), us.classify("1100"));
593        assert_eq!(ifrs.classify("4000"), us.classify("4000"));
594    }
595
596    #[test]
597    fn test_classify_account_type_us_gaap() {
598        let fa = FrameworkAccounts::us_gaap();
599        assert_eq!(fa.classify_account_type("1100"), AccountType::Asset);
600        assert_eq!(fa.classify_account_type("2000"), AccountType::Liability);
601        assert_eq!(fa.classify_account_type("3200"), AccountType::Equity);
602        assert_eq!(fa.classify_account_type("4000"), AccountType::Revenue);
603        assert_eq!(fa.classify_account_type("5000"), AccountType::Expense);
604        assert_eq!(fa.classify_account_type("6100"), AccountType::Expense);
605    }
606
607    #[test]
608    fn test_classify_account_type_french_gaap() {
609        let fa = FrameworkAccounts::french_gaap();
610        assert_eq!(fa.classify_account_type("101000"), AccountType::Equity);
611        assert_eq!(fa.classify_account_type("210000"), AccountType::Asset);
612        assert_eq!(fa.classify_account_type("401000"), AccountType::Liability);
613        assert_eq!(fa.classify_account_type("603000"), AccountType::Expense);
614        assert_eq!(fa.classify_account_type("701000"), AccountType::Revenue);
615    }
616
617    #[test]
618    fn test_classify_account_type_german_gaap() {
619        let fa = FrameworkAccounts::german_gaap();
620        assert_eq!(fa.classify_account_type("0200"), AccountType::Asset);
621        assert_eq!(fa.classify_account_type("2000"), AccountType::Equity);
622        assert_eq!(fa.classify_account_type("3300"), AccountType::Liability);
623        assert_eq!(fa.classify_account_type("4000"), AccountType::Revenue);
624        assert_eq!(fa.classify_account_type("5000"), AccountType::Expense);
625        assert_eq!(fa.classify_account_type("6000"), AccountType::Expense);
626    }
627
628    #[test]
629    fn test_classify_trial_balance_category_us_gaap() {
630        let fa = FrameworkAccounts::us_gaap();
631        assert_eq!(
632            fa.classify_trial_balance_category("1100"),
633            TrialBalanceCategory::CurrentAssets
634        );
635        assert_eq!(
636            fa.classify_trial_balance_category("2000"),
637            TrialBalanceCategory::CurrentLiabilities
638        );
639        assert_eq!(
640            fa.classify_trial_balance_category("3200"),
641            TrialBalanceCategory::Equity
642        );
643        assert_eq!(
644            fa.classify_trial_balance_category("4000"),
645            TrialBalanceCategory::Revenue
646        );
647        assert_eq!(
648            fa.classify_trial_balance_category("5000"),
649            TrialBalanceCategory::CostOfGoodsSold
650        );
651        assert_eq!(
652            fa.classify_trial_balance_category("6100"),
653            TrialBalanceCategory::OperatingExpenses
654        );
655    }
656
657    #[test]
658    fn test_classify_trial_balance_category_french_gaap() {
659        let fa = FrameworkAccounts::french_gaap();
660        assert_eq!(
661            fa.classify_trial_balance_category("101000"),
662            TrialBalanceCategory::Equity
663        );
664        assert_eq!(
665            fa.classify_trial_balance_category("210000"),
666            TrialBalanceCategory::CurrentAssets
667        );
668        assert_eq!(
669            fa.classify_trial_balance_category("603000"),
670            TrialBalanceCategory::OperatingExpenses
671        );
672        assert_eq!(
673            fa.classify_trial_balance_category("701000"),
674            TrialBalanceCategory::Revenue
675        );
676    }
677}