Skip to main content

datasynth_standards/
framework.rs

1//! Accounting Framework Selection and Configuration.
2//!
3//! Provides the core types for selecting between US GAAP and IFRS accounting
4//! frameworks, along with framework-specific settings that control how
5//! accounting standards are applied during synthetic data generation.
6
7use serde::{Deserialize, Serialize};
8
9/// Primary accounting framework selection.
10///
11/// Determines which set of accounting standards governs the generation
12/// of financial data, affecting everything from revenue recognition
13/// timing to lease classification.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum AccountingFramework {
17    /// United States Generally Accepted Accounting Principles.
18    ///
19    /// Key characteristics:
20    /// - Rules-based approach
21    /// - LIFO inventory permitted
22    /// - No revaluation of PPE above cost
23    /// - No reversal of impairment losses (except for certain assets)
24    /// - Bright-line tests for lease classification
25    #[default]
26    UsGaap,
27
28    /// International Financial Reporting Standards.
29    ///
30    /// Key characteristics:
31    /// - Principles-based approach
32    /// - LIFO inventory prohibited
33    /// - Revaluation model permitted for PPE
34    /// - Reversal of impairment losses permitted (except goodwill)
35    /// - Principles-based lease classification
36    Ifrs,
37
38    /// Dual Reporting under both US GAAP and IFRS.
39    ///
40    /// Generates reconciliation data showing differences between
41    /// the two frameworks for the same underlying transactions.
42    DualReporting,
43
44    /// French GAAP (Plan Comptable Général – PCG).
45    ///
46    /// French statutory accounting framework:
47    /// - PCG chart of accounts (classes 1–9)
48    /// - LIFO prohibited (like IFRS)
49    /// - Impairment reversal permitted under French rules
50    /// - Principles-based lease classification (convergent with IFRS 16 for many entities)
51    FrenchGaap,
52
53    /// German GAAP (Handelsgesetzbuch – HGB, §238-263).
54    ///
55    /// German statutory accounting framework:
56    /// - SKR04 chart of accounts (classes 0–9, Abschlussgliederungsprinzip)
57    /// - LIFO prohibited since BilMoG 2009
58    /// - Mandatory impairment reversal (§253(5) HGB)
59    /// - Operating leases remain off-balance (BMF-Leasingerlasse)
60    /// - Pending loss provisions mandatory (§249(1) HGB)
61    /// - Low-value assets (GWG) immediate expensing ≤800 EUR
62    GermanGaap,
63}
64
65impl AccountingFramework {
66    /// Returns the standard name for revenue recognition.
67    pub fn revenue_standard(&self) -> &'static str {
68        match self {
69            Self::UsGaap => "ASC 606",
70            Self::Ifrs => "IFRS 15",
71            Self::DualReporting => "ASC 606 / IFRS 15",
72            Self::FrenchGaap => "PCG / ANC (IFRS 15 aligned)",
73            Self::GermanGaap => "HGB §277 / BilRUG",
74        }
75    }
76
77    /// Returns the standard name for lease accounting.
78    pub fn lease_standard(&self) -> &'static str {
79        match self {
80            Self::UsGaap => "ASC 842",
81            Self::Ifrs => "IFRS 16",
82            Self::DualReporting => "ASC 842 / IFRS 16",
83            Self::FrenchGaap => "PCG / ANC (IFRS 16 aligned)",
84            Self::GermanGaap => "HGB / BMF-Leasingerlasse",
85        }
86    }
87
88    /// Returns the standard name for fair value measurement.
89    pub fn fair_value_standard(&self) -> &'static str {
90        match self {
91            Self::UsGaap => "ASC 820",
92            Self::Ifrs => "IFRS 13",
93            Self::DualReporting => "ASC 820 / IFRS 13",
94            Self::FrenchGaap => "PCG / ANC (IFRS 13 aligned)",
95            Self::GermanGaap => "HGB §253 / IDW RS HFA 10",
96        }
97    }
98
99    /// Returns the standard name for impairment.
100    pub fn impairment_standard(&self) -> &'static str {
101        match self {
102            Self::UsGaap => "ASC 360",
103            Self::Ifrs => "IAS 36",
104            Self::DualReporting => "ASC 360 / IAS 36",
105            Self::FrenchGaap => "PCG / ANC (IAS 36 aligned)",
106            Self::GermanGaap => "HGB §253(3)-(5)",
107        }
108    }
109
110    /// Returns whether LIFO inventory costing is permitted.
111    pub fn allows_lifo(&self) -> bool {
112        matches!(self, Self::UsGaap)
113    }
114
115    /// Returns whether development cost capitalization is required.
116    pub fn requires_development_capitalization(&self) -> bool {
117        matches!(self, Self::Ifrs | Self::DualReporting | Self::FrenchGaap)
118        // GermanGaap: §248(2) optional, not required
119    }
120
121    /// Returns whether PPE revaluation above cost is permitted.
122    pub fn allows_ppe_revaluation(&self) -> bool {
123        matches!(self, Self::Ifrs | Self::DualReporting)
124        // GermanGaap: not permitted (strict Anschaffungskostenprinzip)
125    }
126
127    /// Returns whether impairment loss reversal is permitted.
128    pub fn allows_impairment_reversal(&self) -> bool {
129        matches!(
130            self,
131            Self::Ifrs | Self::DualReporting | Self::FrenchGaap | Self::GermanGaap
132        )
133        // GermanGaap: mandatory reversal per §253(5) HGB
134    }
135
136    /// Returns whether this framework uses bright-line lease tests.
137    pub fn uses_brightline_lease_tests(&self) -> bool {
138        matches!(self, Self::UsGaap)
139    }
140
141    /// Returns whether pending loss provisions are mandatory.
142    ///
143    /// Under HGB §249(1), provisions for pending losses (drohende Verluste
144    /// aus schwebenden Geschäften) are mandatory.
145    pub fn requires_pending_loss_provisions(&self) -> bool {
146        matches!(self, Self::GermanGaap)
147    }
148
149    /// Returns whether low-value assets can be immediately expensed.
150    ///
151    /// Under HGB / EStG §6(2), assets with acquisition cost ≤ 800 EUR
152    /// (GWG — geringwertige Wirtschaftsgüter) can be fully expensed.
153    pub fn allows_low_value_asset_expensing(&self) -> bool {
154        matches!(self, Self::GermanGaap)
155    }
156
157    /// Returns whether operating leases remain off-balance sheet.
158    ///
159    /// Under HGB with BMF-Leasingerlasse, the economic owner (usually lessor)
160    /// keeps the asset on their balance sheet for operating leases.
161    pub fn operating_leases_off_balance(&self) -> bool {
162        matches!(self, Self::GermanGaap)
163    }
164}
165
166impl std::fmt::Display for AccountingFramework {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            Self::UsGaap => write!(f, "US GAAP"),
170            Self::Ifrs => write!(f, "IFRS"),
171            Self::DualReporting => write!(f, "Dual Reporting (US GAAP & IFRS)"),
172            Self::FrenchGaap => write!(f, "French GAAP (PCG)"),
173            Self::GermanGaap => write!(f, "German GAAP (HGB)"),
174        }
175    }
176}
177
178/// Framework-specific settings that control accounting treatment options.
179///
180/// These settings allow fine-grained control over framework-specific
181/// accounting policies within the selected framework.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct FrameworkSettings {
184    /// The primary accounting framework.
185    pub framework: AccountingFramework,
186
187    /// Whether to use LIFO inventory costing (US GAAP only).
188    ///
189    /// Default: false (use FIFO/weighted average)
190    #[serde(default)]
191    pub use_lifo_inventory: bool,
192
193    /// Whether to capitalize development costs (IFRS requirement, US GAAP option).
194    ///
195    /// Under IFRS, development costs must be capitalized when criteria are met.
196    /// Under US GAAP, most development costs are expensed.
197    #[serde(default)]
198    pub capitalize_development_costs: bool,
199
200    /// Whether to use revaluation model for PPE (IFRS option).
201    ///
202    /// Under IFRS, entities can choose between cost model and revaluation model.
203    /// Under US GAAP, revaluation above cost is not permitted.
204    #[serde(default)]
205    pub use_ppe_revaluation: bool,
206
207    /// Whether to reverse impairment losses when conditions improve (IFRS option).
208    ///
209    /// Under IFRS, impairment losses (except for goodwill) can be reversed.
210    /// Under US GAAP, impairment losses generally cannot be reversed.
211    #[serde(default)]
212    pub allow_impairment_reversal: bool,
213
214    /// Threshold percentage for lease term test (US GAAP: 75%).
215    ///
216    /// A lease is classified as finance/capital if the lease term is >= this
217    /// percentage of the asset's economic life.
218    #[serde(default = "default_lease_term_threshold")]
219    pub lease_term_threshold: f64,
220
221    /// Threshold percentage for present value test (US GAAP: 90%).
222    ///
223    /// A lease is classified as finance/capital if the present value of lease
224    /// payments is >= this percentage of the asset's fair value.
225    #[serde(default = "default_lease_pv_threshold")]
226    pub lease_pv_threshold: f64,
227
228    /// Default incremental borrowing rate for lease calculations.
229    #[serde(default = "default_incremental_borrowing_rate")]
230    pub default_incremental_borrowing_rate: f64,
231
232    /// Revenue recognition constraint for variable consideration.
233    ///
234    /// Under both frameworks, variable consideration is constrained to the
235    /// amount that is highly probable (IFRS) or probable (US GAAP) not to
236    /// result in a significant revenue reversal.
237    #[serde(default = "default_variable_consideration_constraint")]
238    pub variable_consideration_constraint: f64,
239}
240
241fn default_lease_term_threshold() -> f64 {
242    0.75
243}
244
245fn default_lease_pv_threshold() -> f64 {
246    0.90
247}
248
249fn default_incremental_borrowing_rate() -> f64 {
250    0.05
251}
252
253fn default_variable_consideration_constraint() -> f64 {
254    0.80
255}
256
257impl Default for FrameworkSettings {
258    fn default() -> Self {
259        Self {
260            framework: AccountingFramework::default(),
261            use_lifo_inventory: false,
262            capitalize_development_costs: false,
263            use_ppe_revaluation: false,
264            allow_impairment_reversal: false,
265            lease_term_threshold: default_lease_term_threshold(),
266            lease_pv_threshold: default_lease_pv_threshold(),
267            default_incremental_borrowing_rate: default_incremental_borrowing_rate(),
268            variable_consideration_constraint: default_variable_consideration_constraint(),
269        }
270    }
271}
272
273impl FrameworkSettings {
274    /// Create settings for US GAAP with typical US company policies.
275    pub fn us_gaap() -> Self {
276        Self {
277            framework: AccountingFramework::UsGaap,
278            use_lifo_inventory: false, // Most companies use FIFO
279            capitalize_development_costs: false,
280            use_ppe_revaluation: false,
281            allow_impairment_reversal: false,
282            ..Default::default()
283        }
284    }
285
286    /// Create settings for IFRS with typical international policies.
287    pub fn ifrs() -> Self {
288        Self {
289            framework: AccountingFramework::Ifrs,
290            use_lifo_inventory: false,          // LIFO prohibited
291            capitalize_development_costs: true, // Required when criteria met
292            use_ppe_revaluation: false,         // Optional, most use cost model
293            allow_impairment_reversal: true,    // Permitted under IFRS
294            ..Default::default()
295        }
296    }
297
298    /// Create settings for dual reporting.
299    pub fn dual_reporting() -> Self {
300        Self {
301            framework: AccountingFramework::DualReporting,
302            use_lifo_inventory: false,
303            capitalize_development_costs: true,
304            use_ppe_revaluation: false,
305            allow_impairment_reversal: true,
306            ..Default::default()
307        }
308    }
309
310    /// Create settings for French GAAP (Plan Comptable Général).
311    pub fn french_gaap() -> Self {
312        Self {
313            framework: AccountingFramework::FrenchGaap,
314            use_lifo_inventory: false, // LIFO prohibited under French GAAP
315            capitalize_development_costs: true, // Permitted when criteria met
316            use_ppe_revaluation: false, // Cost model typical
317            allow_impairment_reversal: true, // Permitted under French rules
318            ..Default::default()
319        }
320    }
321
322    /// Create settings for German GAAP (Handelsgesetzbuch — HGB).
323    pub fn german_gaap() -> Self {
324        Self {
325            framework: AccountingFramework::GermanGaap,
326            use_lifo_inventory: false, // LIFO prohibited since BilMoG 2009
327            capitalize_development_costs: false, // §248(2) optional, most companies expense
328            use_ppe_revaluation: false, // Strict Anschaffungskostenprinzip
329            allow_impairment_reversal: true, // Mandatory per §253(5) HGB
330            ..Default::default()
331        }
332    }
333
334    /// Validate settings are consistent with the selected framework.
335    pub fn validate(&self) -> Result<(), FrameworkValidationError> {
336        // LIFO is only permitted under US GAAP (prohibited under IFRS, French GAAP, and German GAAP)
337        if self.use_lifo_inventory
338            && matches!(
339                self.framework,
340                AccountingFramework::Ifrs
341                    | AccountingFramework::FrenchGaap
342                    | AccountingFramework::GermanGaap
343            )
344        {
345            return Err(FrameworkValidationError::LifoNotPermittedUnderIfrs);
346        }
347
348        // PPE revaluation is not permitted under US GAAP
349        if self.use_ppe_revaluation && self.framework == AccountingFramework::UsGaap {
350            return Err(FrameworkValidationError::RevaluationNotPermittedUnderUsGaap);
351        }
352
353        // Impairment reversal is not permitted under US GAAP
354        if self.allow_impairment_reversal && self.framework == AccountingFramework::UsGaap {
355            return Err(FrameworkValidationError::ImpairmentReversalNotPermittedUnderUsGaap);
356        }
357
358        // Validate thresholds
359        if !(0.0..=1.0).contains(&self.lease_term_threshold) {
360            return Err(FrameworkValidationError::InvalidThreshold(
361                "lease_term_threshold".to_string(),
362            ));
363        }
364
365        if !(0.0..=1.0).contains(&self.lease_pv_threshold) {
366            return Err(FrameworkValidationError::InvalidThreshold(
367                "lease_pv_threshold".to_string(),
368            ));
369        }
370
371        Ok(())
372    }
373}
374
375/// Errors that can occur during framework settings validation.
376#[derive(Debug, Clone, thiserror::Error)]
377pub enum FrameworkValidationError {
378    #[error("LIFO inventory costing is not permitted under IFRS or French GAAP")]
379    LifoNotPermittedUnderIfrs,
380
381    #[error("PPE revaluation above cost is not permitted under US GAAP")]
382    RevaluationNotPermittedUnderUsGaap,
383
384    #[error("Reversal of impairment losses is not permitted under US GAAP")]
385    ImpairmentReversalNotPermittedUnderUsGaap,
386
387    #[error("Invalid threshold value for {0}: must be between 0.0 and 1.0")]
388    InvalidThreshold(String),
389}
390
391/// Key differences between US GAAP and IFRS for a specific area.
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct FrameworkDifference {
394    /// Area of accounting (e.g., "Revenue Recognition", "Lease Classification").
395    pub area: String,
396
397    /// US GAAP treatment description.
398    pub us_gaap_treatment: String,
399
400    /// IFRS treatment description.
401    pub ifrs_treatment: String,
402
403    /// Whether this difference typically results in material differences.
404    pub typically_material: bool,
405
406    /// Relevant US GAAP codification reference.
407    pub us_gaap_reference: String,
408
409    /// Relevant IFRS standard reference.
410    pub ifrs_reference: String,
411}
412
413impl FrameworkDifference {
414    /// Returns common framework differences for educational/documentation purposes.
415    pub fn common_differences() -> Vec<Self> {
416        vec![
417            Self {
418                area: "Inventory Costing".to_string(),
419                us_gaap_treatment: "LIFO, FIFO, and weighted average permitted".to_string(),
420                ifrs_treatment: "LIFO prohibited; FIFO and weighted average permitted".to_string(),
421                typically_material: true,
422                us_gaap_reference: "ASC 330".to_string(),
423                ifrs_reference: "IAS 2".to_string(),
424            },
425            Self {
426                area: "Development Costs".to_string(),
427                us_gaap_treatment: "Generally expensed as incurred".to_string(),
428                ifrs_treatment: "Capitalized when specified criteria are met".to_string(),
429                typically_material: true,
430                us_gaap_reference: "ASC 730".to_string(),
431                ifrs_reference: "IAS 38".to_string(),
432            },
433            Self {
434                area: "Property, Plant & Equipment".to_string(),
435                us_gaap_treatment: "Cost model only; no revaluation above cost".to_string(),
436                ifrs_treatment: "Cost model or revaluation model permitted".to_string(),
437                typically_material: true,
438                us_gaap_reference: "ASC 360".to_string(),
439                ifrs_reference: "IAS 16".to_string(),
440            },
441            Self {
442                area: "Impairment Reversal".to_string(),
443                us_gaap_treatment: "Not permitted for most assets".to_string(),
444                ifrs_treatment: "Permitted except for goodwill".to_string(),
445                typically_material: true,
446                us_gaap_reference: "ASC 360".to_string(),
447                ifrs_reference: "IAS 36".to_string(),
448            },
449            Self {
450                area: "Lease Classification".to_string(),
451                us_gaap_treatment: "Bright-line tests (75% term, 90% PV)".to_string(),
452                ifrs_treatment: "Principles-based; transfer of risks and rewards".to_string(),
453                typically_material: false,
454                us_gaap_reference: "ASC 842".to_string(),
455                ifrs_reference: "IFRS 16".to_string(),
456            },
457            Self {
458                area: "Contingent Liabilities".to_string(),
459                us_gaap_treatment: "Recognized when probable (>75%) and estimable".to_string(),
460                ifrs_treatment: "Recognized when probable (>50%) and estimable".to_string(),
461                typically_material: true,
462                us_gaap_reference: "ASC 450".to_string(),
463                ifrs_reference: "IAS 37".to_string(),
464            },
465        ]
466    }
467}
468
469#[cfg(test)]
470#[allow(clippy::unwrap_used)]
471mod tests {
472    use super::*;
473
474    #[test]
475    fn test_framework_defaults() {
476        let framework = AccountingFramework::default();
477        assert_eq!(framework, AccountingFramework::UsGaap);
478    }
479
480    #[test]
481    fn test_framework_standards() {
482        assert_eq!(AccountingFramework::UsGaap.revenue_standard(), "ASC 606");
483        assert_eq!(AccountingFramework::Ifrs.revenue_standard(), "IFRS 15");
484        assert_eq!(AccountingFramework::UsGaap.lease_standard(), "ASC 842");
485        assert_eq!(AccountingFramework::Ifrs.lease_standard(), "IFRS 16");
486        assert!(AccountingFramework::FrenchGaap
487            .revenue_standard()
488            .contains("PCG"));
489    }
490
491    #[test]
492    fn test_framework_features() {
493        assert!(AccountingFramework::UsGaap.allows_lifo());
494        assert!(!AccountingFramework::Ifrs.allows_lifo());
495        assert!(!AccountingFramework::FrenchGaap.allows_lifo());
496
497        assert!(!AccountingFramework::UsGaap.allows_ppe_revaluation());
498        assert!(AccountingFramework::Ifrs.allows_ppe_revaluation());
499
500        assert!(!AccountingFramework::UsGaap.allows_impairment_reversal());
501        assert!(AccountingFramework::Ifrs.allows_impairment_reversal());
502        assert!(AccountingFramework::FrenchGaap.allows_impairment_reversal());
503    }
504
505    #[test]
506    fn test_french_gaap_settings() {
507        let settings = FrameworkSettings::french_gaap();
508        assert!(settings.validate().is_ok());
509        assert_eq!(settings.framework, AccountingFramework::FrenchGaap);
510    }
511
512    #[test]
513    fn test_german_gaap_standards() {
514        let fw = AccountingFramework::GermanGaap;
515        assert_eq!(fw.revenue_standard(), "HGB §277 / BilRUG");
516        assert_eq!(fw.lease_standard(), "HGB / BMF-Leasingerlasse");
517        assert_eq!(fw.fair_value_standard(), "HGB §253 / IDW RS HFA 10");
518        assert_eq!(fw.impairment_standard(), "HGB §253(3)-(5)");
519    }
520
521    #[test]
522    fn test_german_gaap_features() {
523        let fw = AccountingFramework::GermanGaap;
524        assert!(!fw.allows_lifo(), "LIFO prohibited under HGB since BilMoG");
525        assert!(
526            !fw.allows_ppe_revaluation(),
527            "Strict Anschaffungskostenprinzip"
528        );
529        assert!(fw.allows_impairment_reversal(), "Mandatory per §253(5)");
530        assert!(!fw.uses_brightline_lease_tests(), "BMF uses 40-90% test");
531        assert!(fw.requires_pending_loss_provisions(), "§249(1) HGB");
532        assert!(fw.allows_low_value_asset_expensing(), "GWG ≤ 800 EUR");
533        assert!(fw.operating_leases_off_balance(), "BMF-Leasingerlasse");
534        assert!(
535            !fw.requires_development_capitalization(),
536            "§248(2) optional"
537        );
538    }
539
540    #[test]
541    fn test_german_gaap_settings() {
542        let settings = FrameworkSettings::german_gaap();
543        assert!(settings.validate().is_ok());
544        assert_eq!(settings.framework, AccountingFramework::GermanGaap);
545        assert!(!settings.use_lifo_inventory);
546        assert!(!settings.capitalize_development_costs);
547        assert!(!settings.use_ppe_revaluation);
548        assert!(settings.allow_impairment_reversal);
549    }
550
551    #[test]
552    fn test_german_gaap_lifo_validation_fails() {
553        let mut settings = FrameworkSettings::german_gaap();
554        settings.use_lifo_inventory = true;
555        assert!(matches!(
556            settings.validate(),
557            Err(FrameworkValidationError::LifoNotPermittedUnderIfrs)
558        ));
559    }
560
561    #[test]
562    fn test_german_gaap_serde_roundtrip() {
563        let framework = AccountingFramework::GermanGaap;
564        let json = serde_json::to_string(&framework).unwrap();
565        assert_eq!(json, "\"german_gaap\"");
566        let deserialized: AccountingFramework = serde_json::from_str(&json).unwrap();
567        assert_eq!(framework, deserialized);
568    }
569
570    #[test]
571    fn test_settings_validation_us_gaap() {
572        let settings = FrameworkSettings::us_gaap();
573        assert!(settings.validate().is_ok());
574    }
575
576    #[test]
577    fn test_settings_validation_ifrs() {
578        let settings = FrameworkSettings::ifrs();
579        assert!(settings.validate().is_ok());
580    }
581
582    #[test]
583    fn test_settings_validation_lifo_under_ifrs() {
584        let mut settings = FrameworkSettings::ifrs();
585        settings.use_lifo_inventory = true;
586        assert!(matches!(
587            settings.validate(),
588            Err(FrameworkValidationError::LifoNotPermittedUnderIfrs)
589        ));
590    }
591
592    #[test]
593    fn test_settings_validation_revaluation_under_us_gaap() {
594        let mut settings = FrameworkSettings::us_gaap();
595        settings.use_ppe_revaluation = true;
596        assert!(matches!(
597            settings.validate(),
598            Err(FrameworkValidationError::RevaluationNotPermittedUnderUsGaap)
599        ));
600    }
601
602    #[test]
603    fn test_common_differences() {
604        let differences = FrameworkDifference::common_differences();
605        assert!(!differences.is_empty());
606        assert!(differences.iter().any(|d| d.area == "Inventory Costing"));
607    }
608
609    #[test]
610    fn test_serde_roundtrip() {
611        let framework = AccountingFramework::Ifrs;
612        let json = serde_json::to_string(&framework).unwrap();
613        let deserialized: AccountingFramework = serde_json::from_str(&json).unwrap();
614        assert_eq!(framework, deserialized);
615
616        let settings = FrameworkSettings::ifrs();
617        let json = serde_json::to_string(&settings).unwrap();
618        let deserialized: FrameworkSettings = serde_json::from_str(&json).unwrap();
619        assert_eq!(settings.framework, deserialized.framework);
620    }
621}