Skip to main content

datasynth_core/models/balance/
account_balance.rs

1//! Account balance and balance snapshot models.
2
3use chrono::{NaiveDate, NaiveDateTime};
4use rust_decimal::Decimal;
5use rust_decimal_macros::dec;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Account balance for a single GL account.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AccountBalance {
12    /// Company code.
13    pub company_code: String,
14    /// Account code.
15    pub account_code: String,
16    /// Account description.
17    pub account_description: Option<String>,
18    /// Account type (Asset, Liability, Equity, Revenue, Expense).
19    pub account_type: AccountType,
20    /// Currency.
21    pub currency: String,
22    /// Opening balance (beginning of period).
23    pub opening_balance: Decimal,
24    /// Period debits.
25    pub period_debits: Decimal,
26    /// Period credits.
27    pub period_credits: Decimal,
28    /// Closing balance (end of period).
29    pub closing_balance: Decimal,
30    /// Fiscal year.
31    pub fiscal_year: i32,
32    /// Fiscal period.
33    pub fiscal_period: u32,
34    /// Balance in group currency (for consolidation).
35    pub group_currency_balance: Option<Decimal>,
36    /// Exchange rate used.
37    pub exchange_rate: Option<Decimal>,
38    /// Cost center (if applicable).
39    pub cost_center: Option<String>,
40    /// Profit center (if applicable).
41    pub profit_center: Option<String>,
42    /// Last updated timestamp.
43    pub last_updated: NaiveDateTime,
44}
45
46impl AccountBalance {
47    /// Create a new account balance.
48    pub fn new(
49        company_code: String,
50        account_code: String,
51        account_type: AccountType,
52        currency: String,
53        fiscal_year: i32,
54        fiscal_period: u32,
55    ) -> Self {
56        Self {
57            company_code,
58            account_code,
59            account_description: None,
60            account_type,
61            currency,
62            opening_balance: Decimal::ZERO,
63            period_debits: Decimal::ZERO,
64            period_credits: Decimal::ZERO,
65            closing_balance: Decimal::ZERO,
66            fiscal_year,
67            fiscal_period,
68            group_currency_balance: None,
69            exchange_rate: None,
70            cost_center: None,
71            profit_center: None,
72            last_updated: chrono::Utc::now().naive_utc(),
73        }
74    }
75
76    /// Apply a debit to this balance.
77    pub fn apply_debit(&mut self, amount: Decimal) {
78        self.period_debits += amount;
79        self.recalculate_closing();
80    }
81
82    /// Apply a credit to this balance.
83    pub fn apply_credit(&mut self, amount: Decimal) {
84        self.period_credits += amount;
85        self.recalculate_closing();
86    }
87
88    /// Recalculate closing balance based on account type.
89    fn recalculate_closing(&mut self) {
90        // Asset and Expense accounts: Debit increases, Credit decreases
91        // Liability, Equity, Revenue accounts: Credit increases, Debit decreases
92        match self.account_type {
93            AccountType::Asset
94            | AccountType::Expense
95            | AccountType::ContraLiability
96            | AccountType::ContraEquity => {
97                self.closing_balance =
98                    self.opening_balance + self.period_debits - self.period_credits;
99            }
100            AccountType::Liability
101            | AccountType::Equity
102            | AccountType::Revenue
103            | AccountType::ContraAsset => {
104                self.closing_balance =
105                    self.opening_balance - self.period_debits + self.period_credits;
106            }
107        }
108        self.last_updated = chrono::Utc::now().naive_utc();
109    }
110
111    /// Set opening balance.
112    pub fn set_opening_balance(&mut self, balance: Decimal) {
113        self.opening_balance = balance;
114        self.recalculate_closing();
115    }
116
117    /// Get the net change for the period.
118    pub fn net_change(&self) -> Decimal {
119        match self.account_type {
120            AccountType::Asset
121            | AccountType::Expense
122            | AccountType::ContraLiability
123            | AccountType::ContraEquity => self.period_debits - self.period_credits,
124            AccountType::Liability
125            | AccountType::Equity
126            | AccountType::Revenue
127            | AccountType::ContraAsset => self.period_credits - self.period_debits,
128        }
129    }
130
131    /// Check if this is a debit-normal account.
132    pub fn is_debit_normal(&self) -> bool {
133        matches!(
134            self.account_type,
135            AccountType::Asset
136                | AccountType::Expense
137                | AccountType::ContraLiability
138                | AccountType::ContraEquity
139        )
140    }
141
142    /// Get the normal balance (positive closing for correct sign).
143    pub fn normal_balance(&self) -> Decimal {
144        if self.is_debit_normal() {
145            self.closing_balance
146        } else {
147            -self.closing_balance
148        }
149    }
150
151    /// Roll forward to next period.
152    pub fn roll_forward(&mut self) {
153        self.opening_balance = self.closing_balance;
154        self.period_debits = Decimal::ZERO;
155        self.period_credits = Decimal::ZERO;
156
157        // Increment period
158        if self.fiscal_period == 12 {
159            self.fiscal_period = 1;
160            self.fiscal_year += 1;
161        } else {
162            self.fiscal_period += 1;
163        }
164
165        self.last_updated = chrono::Utc::now().naive_utc();
166    }
167
168    /// Check if this is a balance sheet account.
169    pub fn is_balance_sheet(&self) -> bool {
170        matches!(
171            self.account_type,
172            AccountType::Asset
173                | AccountType::Liability
174                | AccountType::Equity
175                | AccountType::ContraAsset
176                | AccountType::ContraLiability
177                | AccountType::ContraEquity
178        )
179    }
180
181    /// Check if this is an income statement account.
182    pub fn is_income_statement(&self) -> bool {
183        matches!(
184            self.account_type,
185            AccountType::Revenue | AccountType::Expense
186        )
187    }
188}
189
190/// Account type classification.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
192#[serde(rename_all = "snake_case")]
193pub enum AccountType {
194    /// Assets (debit normal).
195    #[default]
196    Asset,
197    /// Contra-asset (credit normal, e.g., accumulated depreciation).
198    ContraAsset,
199    /// Liabilities (credit normal).
200    Liability,
201    /// Contra-liability (debit normal).
202    ContraLiability,
203    /// Equity (credit normal).
204    Equity,
205    /// Contra-equity (debit normal, e.g., treasury stock).
206    ContraEquity,
207    /// Revenue (credit normal).
208    Revenue,
209    /// Expenses (debit normal).
210    Expense,
211}
212
213impl AccountType {
214    /// Determine account type from account code (simplified, US GAAP heuristic).
215    ///
216    /// For framework-aware classification, use
217    /// [`from_account_code_with_framework`](Self::from_account_code_with_framework).
218    pub fn from_account_code(code: &str) -> Self {
219        let first_char = code.chars().next().unwrap_or('0');
220        match first_char {
221            '1' => Self::Asset,
222            '2' => Self::Liability,
223            '3' => Self::Equity,
224            '4' => Self::Revenue,
225            '5' | '6' | '7' | '8' => Self::Expense,
226            _ => Self::Asset,
227        }
228    }
229
230    /// Determine account type using framework-aware classification.
231    ///
232    /// `framework` is the framework string (e.g. `"us_gaap"`, `"french_gaap"`,
233    /// `"german_gaap"`, `"ifrs"`). Uses [`FrameworkAccounts`] internally.
234    pub fn from_account_code_with_framework(code: &str, framework: &str) -> Self {
235        crate::framework_accounts::FrameworkAccounts::for_framework(framework)
236            .classify_account_type(code)
237    }
238
239    /// Check if contra account based on code pattern.
240    pub fn is_contra_from_code(code: &str) -> bool {
241        // Common patterns for contra accounts
242        code.contains("ACCUM") || code.contains("ALLOW") || code.contains("CONTRA")
243    }
244}
245
246/// A snapshot of all account balances at a point in time.
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct BalanceSnapshot {
249    /// Snapshot identifier.
250    pub snapshot_id: String,
251    /// Company code.
252    pub company_code: String,
253    /// Snapshot date.
254    pub as_of_date: NaiveDate,
255    /// Fiscal year.
256    pub fiscal_year: i32,
257    /// Fiscal period.
258    pub fiscal_period: u32,
259    /// Currency.
260    pub currency: String,
261    /// All account balances.
262    pub balances: HashMap<String, AccountBalance>,
263    /// Total assets.
264    pub total_assets: Decimal,
265    /// Total liabilities.
266    pub total_liabilities: Decimal,
267    /// Total equity.
268    pub total_equity: Decimal,
269    /// Total revenue.
270    pub total_revenue: Decimal,
271    /// Total expenses.
272    pub total_expenses: Decimal,
273    /// Net income.
274    pub net_income: Decimal,
275    /// Is the balance sheet balanced (A = L + E)?
276    pub is_balanced: bool,
277    /// Balance sheet difference (should be zero).
278    pub balance_difference: Decimal,
279    /// Created timestamp.
280    pub created_at: NaiveDateTime,
281}
282
283impl BalanceSnapshot {
284    /// Create a new balance snapshot.
285    pub fn new(
286        snapshot_id: String,
287        company_code: String,
288        as_of_date: NaiveDate,
289        fiscal_year: i32,
290        fiscal_period: u32,
291        currency: String,
292    ) -> Self {
293        Self {
294            snapshot_id,
295            company_code,
296            as_of_date,
297            fiscal_year,
298            fiscal_period,
299            currency,
300            balances: HashMap::new(),
301            total_assets: Decimal::ZERO,
302            total_liabilities: Decimal::ZERO,
303            total_equity: Decimal::ZERO,
304            total_revenue: Decimal::ZERO,
305            total_expenses: Decimal::ZERO,
306            net_income: Decimal::ZERO,
307            is_balanced: true,
308            balance_difference: Decimal::ZERO,
309            created_at: chrono::Utc::now().naive_utc(),
310        }
311    }
312
313    /// Add an account balance to the snapshot.
314    pub fn add_balance(&mut self, balance: AccountBalance) {
315        let closing = balance.closing_balance;
316
317        match balance.account_type {
318            AccountType::Asset => self.total_assets += closing,
319            AccountType::ContraAsset => self.total_assets -= closing,
320            AccountType::Liability => self.total_liabilities += closing,
321            AccountType::ContraLiability => self.total_liabilities -= closing,
322            AccountType::Equity => self.total_equity += closing,
323            AccountType::ContraEquity => self.total_equity -= closing,
324            AccountType::Revenue => self.total_revenue += closing,
325            AccountType::Expense => self.total_expenses += closing,
326        }
327
328        self.balances.insert(balance.account_code.clone(), balance);
329        self.recalculate_totals();
330    }
331
332    /// Recalculate totals and validate balance sheet equation.
333    pub fn recalculate_totals(&mut self) {
334        self.net_income = self.total_revenue - self.total_expenses;
335
336        // Balance sheet equation: Assets = Liabilities + Equity
337        // For current period, equity includes net income
338        let total_equity_with_income = self.total_equity + self.net_income;
339        self.balance_difference =
340            self.total_assets - self.total_liabilities - total_equity_with_income;
341        self.is_balanced = self.balance_difference.abs() < dec!(0.01);
342    }
343
344    /// Get balance for a specific account.
345    pub fn get_balance(&self, account_code: &str) -> Option<&AccountBalance> {
346        self.balances.get(account_code)
347    }
348
349    /// Get all asset balances.
350    pub fn get_asset_balances(&self) -> Vec<&AccountBalance> {
351        self.balances
352            .values()
353            .filter(|b| {
354                matches!(
355                    b.account_type,
356                    AccountType::Asset | AccountType::ContraAsset
357                )
358            })
359            .collect()
360    }
361
362    /// Get all liability balances.
363    pub fn get_liability_balances(&self) -> Vec<&AccountBalance> {
364        self.balances
365            .values()
366            .filter(|b| {
367                matches!(
368                    b.account_type,
369                    AccountType::Liability | AccountType::ContraLiability
370                )
371            })
372            .collect()
373    }
374
375    /// Get all equity balances.
376    pub fn get_equity_balances(&self) -> Vec<&AccountBalance> {
377        self.balances
378            .values()
379            .filter(|b| {
380                matches!(
381                    b.account_type,
382                    AccountType::Equity | AccountType::ContraEquity
383                )
384            })
385            .collect()
386    }
387
388    /// Get all income statement balances.
389    pub fn get_income_statement_balances(&self) -> Vec<&AccountBalance> {
390        self.balances
391            .values()
392            .filter(|b| b.is_income_statement())
393            .collect()
394    }
395
396    /// Get current ratio (Current Assets / Current Liabilities).
397    pub fn current_ratio(
398        &self,
399        current_asset_accounts: &[&str],
400        current_liability_accounts: &[&str],
401    ) -> Option<Decimal> {
402        let current_assets: Decimal = current_asset_accounts
403            .iter()
404            .filter_map(|code| self.balances.get(*code))
405            .map(|b| b.closing_balance)
406            .sum();
407
408        let current_liabilities: Decimal = current_liability_accounts
409            .iter()
410            .filter_map(|code| self.balances.get(*code))
411            .map(|b| b.closing_balance)
412            .sum();
413
414        if current_liabilities != Decimal::ZERO {
415            Some(current_assets / current_liabilities)
416        } else {
417            None
418        }
419    }
420
421    /// Get debt-to-equity ratio.
422    pub fn debt_to_equity_ratio(&self) -> Option<Decimal> {
423        if self.total_equity != Decimal::ZERO {
424            Some(self.total_liabilities / self.total_equity)
425        } else {
426            None
427        }
428    }
429
430    /// Get gross margin (Revenue - COGS) / Revenue.
431    pub fn gross_margin(&self, cogs_accounts: &[&str]) -> Option<Decimal> {
432        if self.total_revenue == Decimal::ZERO {
433            return None;
434        }
435
436        let cogs: Decimal = cogs_accounts
437            .iter()
438            .filter_map(|code| self.balances.get(*code))
439            .map(|b| b.closing_balance)
440            .sum();
441
442        Some((self.total_revenue - cogs) / self.total_revenue)
443    }
444}
445
446/// Period-over-period balance change analysis.
447#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct BalanceChange {
449    /// Account code.
450    pub account_code: String,
451    /// Account description.
452    pub account_description: Option<String>,
453    /// Prior period balance.
454    pub prior_balance: Decimal,
455    /// Current period balance.
456    pub current_balance: Decimal,
457    /// Absolute change.
458    pub change_amount: Decimal,
459    /// Percentage change.
460    pub change_percent: Option<Decimal>,
461    /// Is this a significant change (above threshold)?
462    pub is_significant: bool,
463}
464
465impl BalanceChange {
466    /// Create a new balance change analysis.
467    pub fn new(
468        account_code: String,
469        account_description: Option<String>,
470        prior_balance: Decimal,
471        current_balance: Decimal,
472        significance_threshold: Decimal,
473    ) -> Self {
474        let change_amount = current_balance - prior_balance;
475        let change_percent = if prior_balance != Decimal::ZERO {
476            Some((change_amount / prior_balance.abs()) * dec!(100))
477        } else {
478            None
479        };
480
481        let is_significant = change_amount.abs() >= significance_threshold
482            || change_percent.is_some_and(|p| p.abs() >= dec!(10));
483
484        Self {
485            account_code,
486            account_description,
487            prior_balance,
488            current_balance,
489            change_amount,
490            change_percent,
491            is_significant,
492        }
493    }
494}
495
496/// Account activity tracking within a period.
497///
498/// Tracks debits, credits, and transaction counts for an account
499/// over a specific period.
500#[derive(Debug, Clone, Serialize, Deserialize, Default)]
501pub struct AccountPeriodActivity {
502    /// Account code.
503    pub account_code: String,
504    /// Period start date.
505    pub period_start: NaiveDate,
506    /// Period end date.
507    pub period_end: NaiveDate,
508    /// Opening balance at period start.
509    pub opening_balance: Decimal,
510    /// Closing balance at period end.
511    pub closing_balance: Decimal,
512    /// Total debit amounts during period.
513    pub total_debits: Decimal,
514    /// Total credit amounts during period.
515    pub total_credits: Decimal,
516    /// Net change (total_debits - total_credits).
517    pub net_change: Decimal,
518    /// Number of transactions during period.
519    pub transaction_count: u32,
520}
521
522impl AccountPeriodActivity {
523    /// Create a new account period activity tracker.
524    pub fn new(account_code: String, period_start: NaiveDate, period_end: NaiveDate) -> Self {
525        Self {
526            account_code,
527            period_start,
528            period_end,
529            opening_balance: Decimal::ZERO,
530            closing_balance: Decimal::ZERO,
531            total_debits: Decimal::ZERO,
532            total_credits: Decimal::ZERO,
533            net_change: Decimal::ZERO,
534            transaction_count: 0,
535        }
536    }
537
538    /// Add a debit transaction.
539    pub fn add_debit(&mut self, amount: Decimal) {
540        self.total_debits += amount;
541        self.net_change += amount;
542        self.transaction_count += 1;
543    }
544
545    /// Add a credit transaction.
546    pub fn add_credit(&mut self, amount: Decimal) {
547        self.total_credits += amount;
548        self.net_change -= amount;
549        self.transaction_count += 1;
550    }
551}
552
553/// Compare two snapshots and identify changes.
554pub fn compare_snapshots(
555    prior: &BalanceSnapshot,
556    current: &BalanceSnapshot,
557    significance_threshold: Decimal,
558) -> Vec<BalanceChange> {
559    let mut changes = Vec::new();
560
561    // Get all unique account codes
562    let mut all_accounts: Vec<&str> = prior.balances.keys().map(|s| s.as_str()).collect();
563    for code in current.balances.keys() {
564        if !all_accounts.contains(&code.as_str()) {
565            all_accounts.push(code.as_str());
566        }
567    }
568
569    for account_code in all_accounts {
570        let prior_balance = prior
571            .balances
572            .get(account_code)
573            .map(|b| b.closing_balance)
574            .unwrap_or(Decimal::ZERO);
575
576        let current_balance = current
577            .balances
578            .get(account_code)
579            .map(|b| b.closing_balance)
580            .unwrap_or(Decimal::ZERO);
581
582        let description = current
583            .balances
584            .get(account_code)
585            .and_then(|b| b.account_description.clone())
586            .or_else(|| {
587                prior
588                    .balances
589                    .get(account_code)
590                    .and_then(|b| b.account_description.clone())
591            });
592
593        if prior_balance != current_balance {
594            changes.push(BalanceChange::new(
595                account_code.to_string(),
596                description,
597                prior_balance,
598                current_balance,
599                significance_threshold,
600            ));
601        }
602    }
603
604    changes
605}
606
607#[cfg(test)]
608#[allow(clippy::unwrap_used)]
609mod tests {
610    use super::*;
611
612    #[test]
613    fn test_account_balance_debit_normal() {
614        let mut balance = AccountBalance::new(
615            "1000".to_string(),
616            "1100".to_string(),
617            AccountType::Asset,
618            "USD".to_string(),
619            2022,
620            6,
621        );
622
623        balance.set_opening_balance(dec!(10000));
624        balance.apply_debit(dec!(5000));
625        balance.apply_credit(dec!(2000));
626
627        assert_eq!(balance.closing_balance, dec!(13000)); // 10000 + 5000 - 2000
628        assert_eq!(balance.net_change(), dec!(3000));
629    }
630
631    #[test]
632    fn test_account_balance_credit_normal() {
633        let mut balance = AccountBalance::new(
634            "1000".to_string(),
635            "2100".to_string(),
636            AccountType::Liability,
637            "USD".to_string(),
638            2022,
639            6,
640        );
641
642        balance.set_opening_balance(dec!(10000));
643        balance.apply_credit(dec!(5000));
644        balance.apply_debit(dec!(2000));
645
646        assert_eq!(balance.closing_balance, dec!(13000)); // 10000 - 2000 + 5000
647        assert_eq!(balance.net_change(), dec!(3000));
648    }
649
650    #[test]
651    fn test_balance_snapshot_balanced() {
652        let mut snapshot = BalanceSnapshot::new(
653            "SNAP001".to_string(),
654            "1000".to_string(),
655            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
656            2022,
657            6,
658            "USD".to_string(),
659        );
660
661        // Add asset
662        let mut cash = AccountBalance::new(
663            "1000".to_string(),
664            "1100".to_string(),
665            AccountType::Asset,
666            "USD".to_string(),
667            2022,
668            6,
669        );
670        cash.closing_balance = dec!(50000);
671        snapshot.add_balance(cash);
672
673        // Add liability
674        let mut ap = AccountBalance::new(
675            "1000".to_string(),
676            "2100".to_string(),
677            AccountType::Liability,
678            "USD".to_string(),
679            2022,
680            6,
681        );
682        ap.closing_balance = dec!(20000);
683        snapshot.add_balance(ap);
684
685        // Add equity
686        let mut equity = AccountBalance::new(
687            "1000".to_string(),
688            "3100".to_string(),
689            AccountType::Equity,
690            "USD".to_string(),
691            2022,
692            6,
693        );
694        equity.closing_balance = dec!(30000);
695        snapshot.add_balance(equity);
696
697        assert!(snapshot.is_balanced);
698        assert_eq!(snapshot.total_assets, dec!(50000));
699        assert_eq!(snapshot.total_liabilities, dec!(20000));
700        assert_eq!(snapshot.total_equity, dec!(30000));
701    }
702
703    #[test]
704    fn test_balance_snapshot_with_income() {
705        let mut snapshot = BalanceSnapshot::new(
706            "SNAP001".to_string(),
707            "1000".to_string(),
708            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
709            2022,
710            6,
711            "USD".to_string(),
712        );
713
714        // Assets = 60000
715        let mut cash = AccountBalance::new(
716            "1000".to_string(),
717            "1100".to_string(),
718            AccountType::Asset,
719            "USD".to_string(),
720            2022,
721            6,
722        );
723        cash.closing_balance = dec!(60000);
724        snapshot.add_balance(cash);
725
726        // Liabilities = 20000
727        let mut ap = AccountBalance::new(
728            "1000".to_string(),
729            "2100".to_string(),
730            AccountType::Liability,
731            "USD".to_string(),
732            2022,
733            6,
734        );
735        ap.closing_balance = dec!(20000);
736        snapshot.add_balance(ap);
737
738        // Equity = 30000
739        let mut equity = AccountBalance::new(
740            "1000".to_string(),
741            "3100".to_string(),
742            AccountType::Equity,
743            "USD".to_string(),
744            2022,
745            6,
746        );
747        equity.closing_balance = dec!(30000);
748        snapshot.add_balance(equity);
749
750        // Revenue = 50000
751        let mut revenue = AccountBalance::new(
752            "1000".to_string(),
753            "4100".to_string(),
754            AccountType::Revenue,
755            "USD".to_string(),
756            2022,
757            6,
758        );
759        revenue.closing_balance = dec!(50000);
760        snapshot.add_balance(revenue);
761
762        // Expenses = 40000
763        let mut expense = AccountBalance::new(
764            "1000".to_string(),
765            "5100".to_string(),
766            AccountType::Expense,
767            "USD".to_string(),
768            2022,
769            6,
770        );
771        expense.closing_balance = dec!(40000);
772        snapshot.add_balance(expense);
773
774        // Net income = 50000 - 40000 = 10000
775        // A = 60000, L = 20000, E = 30000, NI = 10000
776        // A = L + E + NI -> 60000 = 20000 + 30000 + 10000 ✓
777        assert!(snapshot.is_balanced);
778        assert_eq!(snapshot.net_income, dec!(10000));
779    }
780
781    #[test]
782    fn test_account_type_from_code() {
783        assert_eq!(AccountType::from_account_code("1100"), AccountType::Asset);
784        assert_eq!(
785            AccountType::from_account_code("2100"),
786            AccountType::Liability
787        );
788        assert_eq!(AccountType::from_account_code("3100"), AccountType::Equity);
789        assert_eq!(AccountType::from_account_code("4100"), AccountType::Revenue);
790        assert_eq!(AccountType::from_account_code("5100"), AccountType::Expense);
791    }
792
793    #[test]
794    fn test_account_type_from_code_with_framework_us_gaap() {
795        assert_eq!(
796            AccountType::from_account_code_with_framework("1100", "us_gaap"),
797            AccountType::Asset
798        );
799        assert_eq!(
800            AccountType::from_account_code_with_framework("4000", "us_gaap"),
801            AccountType::Revenue
802        );
803    }
804
805    #[test]
806    fn test_account_type_from_code_with_framework_french_gaap() {
807        // PCG class 1 (10x) = Equity, not Asset
808        assert_eq!(
809            AccountType::from_account_code_with_framework("101000", "french_gaap"),
810            AccountType::Equity
811        );
812        // PCG class 2 = Fixed Assets
813        assert_eq!(
814            AccountType::from_account_code_with_framework("210000", "french_gaap"),
815            AccountType::Asset
816        );
817        // PCG class 7 = Revenue
818        assert_eq!(
819            AccountType::from_account_code_with_framework("701000", "french_gaap"),
820            AccountType::Revenue
821        );
822    }
823
824    #[test]
825    fn test_account_type_from_code_with_framework_german_gaap() {
826        // SKR04 class 0 = Fixed Assets
827        assert_eq!(
828            AccountType::from_account_code_with_framework("0200", "german_gaap"),
829            AccountType::Asset
830        );
831        // SKR04 class 2 = Equity (not Liability as US GAAP would say)
832        assert_eq!(
833            AccountType::from_account_code_with_framework("2000", "german_gaap"),
834            AccountType::Equity
835        );
836        // SKR04 class 4 = Revenue
837        assert_eq!(
838            AccountType::from_account_code_with_framework("4000", "german_gaap"),
839            AccountType::Revenue
840        );
841    }
842
843    #[test]
844    fn test_balance_roll_forward() {
845        let mut balance = AccountBalance::new(
846            "1000".to_string(),
847            "1100".to_string(),
848            AccountType::Asset,
849            "USD".to_string(),
850            2022,
851            12,
852        );
853
854        balance.set_opening_balance(dec!(10000));
855        balance.apply_debit(dec!(5000));
856        balance.roll_forward();
857
858        assert_eq!(balance.opening_balance, dec!(15000));
859        assert_eq!(balance.period_debits, Decimal::ZERO);
860        assert_eq!(balance.fiscal_year, 2023);
861        assert_eq!(balance.fiscal_period, 1);
862    }
863}