1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum AccountingFramework {
17 #[default]
26 UsGaap,
27
28 Ifrs,
37
38 DualReporting,
43
44 FrenchGaap,
52
53 GermanGaap,
63}
64
65impl AccountingFramework {
66 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 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 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 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 pub fn allows_lifo(&self) -> bool {
112 matches!(self, Self::UsGaap)
113 }
114
115 pub fn requires_development_capitalization(&self) -> bool {
117 matches!(self, Self::Ifrs | Self::DualReporting | Self::FrenchGaap)
118 }
120
121 pub fn allows_ppe_revaluation(&self) -> bool {
123 matches!(self, Self::Ifrs | Self::DualReporting)
124 }
126
127 pub fn allows_impairment_reversal(&self) -> bool {
129 matches!(
130 self,
131 Self::Ifrs | Self::DualReporting | Self::FrenchGaap | Self::GermanGaap
132 )
133 }
135
136 pub fn uses_brightline_lease_tests(&self) -> bool {
138 matches!(self, Self::UsGaap)
139 }
140
141 pub fn requires_pending_loss_provisions(&self) -> bool {
146 matches!(self, Self::GermanGaap)
147 }
148
149 pub fn allows_low_value_asset_expensing(&self) -> bool {
154 matches!(self, Self::GermanGaap)
155 }
156
157 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#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct FrameworkSettings {
184 pub framework: AccountingFramework,
186
187 #[serde(default)]
191 pub use_lifo_inventory: bool,
192
193 #[serde(default)]
198 pub capitalize_development_costs: bool,
199
200 #[serde(default)]
205 pub use_ppe_revaluation: bool,
206
207 #[serde(default)]
212 pub allow_impairment_reversal: bool,
213
214 #[serde(default = "default_lease_term_threshold")]
219 pub lease_term_threshold: f64,
220
221 #[serde(default = "default_lease_pv_threshold")]
226 pub lease_pv_threshold: f64,
227
228 #[serde(default = "default_incremental_borrowing_rate")]
230 pub default_incremental_borrowing_rate: f64,
231
232 #[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 pub fn us_gaap() -> Self {
276 Self {
277 framework: AccountingFramework::UsGaap,
278 use_lifo_inventory: false, capitalize_development_costs: false,
280 use_ppe_revaluation: false,
281 allow_impairment_reversal: false,
282 ..Default::default()
283 }
284 }
285
286 pub fn ifrs() -> Self {
288 Self {
289 framework: AccountingFramework::Ifrs,
290 use_lifo_inventory: false, capitalize_development_costs: true, use_ppe_revaluation: false, allow_impairment_reversal: true, ..Default::default()
295 }
296 }
297
298 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 pub fn french_gaap() -> Self {
312 Self {
313 framework: AccountingFramework::FrenchGaap,
314 use_lifo_inventory: false, capitalize_development_costs: true, use_ppe_revaluation: false, allow_impairment_reversal: true, ..Default::default()
319 }
320 }
321
322 pub fn german_gaap() -> Self {
324 Self {
325 framework: AccountingFramework::GermanGaap,
326 use_lifo_inventory: false, capitalize_development_costs: false, use_ppe_revaluation: false, allow_impairment_reversal: true, ..Default::default()
331 }
332 }
333
334 pub fn validate(&self) -> Result<(), FrameworkValidationError> {
336 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 if self.use_ppe_revaluation && self.framework == AccountingFramework::UsGaap {
350 return Err(FrameworkValidationError::RevaluationNotPermittedUnderUsGaap);
351 }
352
353 if self.allow_impairment_reversal && self.framework == AccountingFramework::UsGaap {
355 return Err(FrameworkValidationError::ImpairmentReversalNotPermittedUnderUsGaap);
356 }
357
358 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct FrameworkDifference {
394 pub area: String,
396
397 pub us_gaap_treatment: String,
399
400 pub ifrs_treatment: String,
402
403 pub typically_material: bool,
405
406 pub us_gaap_reference: String,
408
409 pub ifrs_reference: String,
411}
412
413impl FrameworkDifference {
414 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)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn test_framework_defaults() {
475 let framework = AccountingFramework::default();
476 assert_eq!(framework, AccountingFramework::UsGaap);
477 }
478
479 #[test]
480 fn test_framework_standards() {
481 assert_eq!(AccountingFramework::UsGaap.revenue_standard(), "ASC 606");
482 assert_eq!(AccountingFramework::Ifrs.revenue_standard(), "IFRS 15");
483 assert_eq!(AccountingFramework::UsGaap.lease_standard(), "ASC 842");
484 assert_eq!(AccountingFramework::Ifrs.lease_standard(), "IFRS 16");
485 assert!(AccountingFramework::FrenchGaap
486 .revenue_standard()
487 .contains("PCG"));
488 }
489
490 #[test]
491 fn test_framework_features() {
492 assert!(AccountingFramework::UsGaap.allows_lifo());
493 assert!(!AccountingFramework::Ifrs.allows_lifo());
494 assert!(!AccountingFramework::FrenchGaap.allows_lifo());
495
496 assert!(!AccountingFramework::UsGaap.allows_ppe_revaluation());
497 assert!(AccountingFramework::Ifrs.allows_ppe_revaluation());
498
499 assert!(!AccountingFramework::UsGaap.allows_impairment_reversal());
500 assert!(AccountingFramework::Ifrs.allows_impairment_reversal());
501 assert!(AccountingFramework::FrenchGaap.allows_impairment_reversal());
502 }
503
504 #[test]
505 fn test_french_gaap_settings() {
506 let settings = FrameworkSettings::french_gaap();
507 assert!(settings.validate().is_ok());
508 assert_eq!(settings.framework, AccountingFramework::FrenchGaap);
509 }
510
511 #[test]
512 fn test_german_gaap_standards() {
513 let fw = AccountingFramework::GermanGaap;
514 assert_eq!(fw.revenue_standard(), "HGB §277 / BilRUG");
515 assert_eq!(fw.lease_standard(), "HGB / BMF-Leasingerlasse");
516 assert_eq!(fw.fair_value_standard(), "HGB §253 / IDW RS HFA 10");
517 assert_eq!(fw.impairment_standard(), "HGB §253(3)-(5)");
518 }
519
520 #[test]
521 fn test_german_gaap_features() {
522 let fw = AccountingFramework::GermanGaap;
523 assert!(!fw.allows_lifo(), "LIFO prohibited under HGB since BilMoG");
524 assert!(
525 !fw.allows_ppe_revaluation(),
526 "Strict Anschaffungskostenprinzip"
527 );
528 assert!(fw.allows_impairment_reversal(), "Mandatory per §253(5)");
529 assert!(!fw.uses_brightline_lease_tests(), "BMF uses 40-90% test");
530 assert!(fw.requires_pending_loss_provisions(), "§249(1) HGB");
531 assert!(fw.allows_low_value_asset_expensing(), "GWG ≤ 800 EUR");
532 assert!(fw.operating_leases_off_balance(), "BMF-Leasingerlasse");
533 assert!(
534 !fw.requires_development_capitalization(),
535 "§248(2) optional"
536 );
537 }
538
539 #[test]
540 fn test_german_gaap_settings() {
541 let settings = FrameworkSettings::german_gaap();
542 assert!(settings.validate().is_ok());
543 assert_eq!(settings.framework, AccountingFramework::GermanGaap);
544 assert!(!settings.use_lifo_inventory);
545 assert!(!settings.capitalize_development_costs);
546 assert!(!settings.use_ppe_revaluation);
547 assert!(settings.allow_impairment_reversal);
548 }
549
550 #[test]
551 fn test_german_gaap_lifo_validation_fails() {
552 let mut settings = FrameworkSettings::german_gaap();
553 settings.use_lifo_inventory = true;
554 assert!(matches!(
555 settings.validate(),
556 Err(FrameworkValidationError::LifoNotPermittedUnderIfrs)
557 ));
558 }
559
560 #[test]
561 fn test_german_gaap_serde_roundtrip() {
562 let framework = AccountingFramework::GermanGaap;
563 let json = serde_json::to_string(&framework).unwrap();
564 assert_eq!(json, "\"german_gaap\"");
565 let deserialized: AccountingFramework = serde_json::from_str(&json).unwrap();
566 assert_eq!(framework, deserialized);
567 }
568
569 #[test]
570 fn test_settings_validation_us_gaap() {
571 let settings = FrameworkSettings::us_gaap();
572 assert!(settings.validate().is_ok());
573 }
574
575 #[test]
576 fn test_settings_validation_ifrs() {
577 let settings = FrameworkSettings::ifrs();
578 assert!(settings.validate().is_ok());
579 }
580
581 #[test]
582 fn test_settings_validation_lifo_under_ifrs() {
583 let mut settings = FrameworkSettings::ifrs();
584 settings.use_lifo_inventory = true;
585 assert!(matches!(
586 settings.validate(),
587 Err(FrameworkValidationError::LifoNotPermittedUnderIfrs)
588 ));
589 }
590
591 #[test]
592 fn test_settings_validation_revaluation_under_us_gaap() {
593 let mut settings = FrameworkSettings::us_gaap();
594 settings.use_ppe_revaluation = true;
595 assert!(matches!(
596 settings.validate(),
597 Err(FrameworkValidationError::RevaluationNotPermittedUnderUsGaap)
598 ));
599 }
600
601 #[test]
602 fn test_common_differences() {
603 let differences = FrameworkDifference::common_differences();
604 assert!(!differences.is_empty());
605 assert!(differences.iter().any(|d| d.area == "Inventory Costing"));
606 }
607
608 #[test]
609 fn test_serde_roundtrip() {
610 let framework = AccountingFramework::Ifrs;
611 let json = serde_json::to_string(&framework).unwrap();
612 let deserialized: AccountingFramework = serde_json::from_str(&json).unwrap();
613 assert_eq!(framework, deserialized);
614
615 let settings = FrameworkSettings::ifrs();
616 let json = serde_json::to_string(&settings).unwrap();
617 let deserialized: FrameworkSettings = serde_json::from_str(&json).unwrap();
618 assert_eq!(settings.framework, deserialized.framework);
619 }
620}