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)]
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}