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
45impl AccountingFramework {
46 pub fn revenue_standard(&self) -> &'static str {
48 match self {
49 Self::UsGaap => "ASC 606",
50 Self::Ifrs => "IFRS 15",
51 Self::DualReporting => "ASC 606 / IFRS 15",
52 }
53 }
54
55 pub fn lease_standard(&self) -> &'static str {
57 match self {
58 Self::UsGaap => "ASC 842",
59 Self::Ifrs => "IFRS 16",
60 Self::DualReporting => "ASC 842 / IFRS 16",
61 }
62 }
63
64 pub fn fair_value_standard(&self) -> &'static str {
66 match self {
67 Self::UsGaap => "ASC 820",
68 Self::Ifrs => "IFRS 13",
69 Self::DualReporting => "ASC 820 / IFRS 13",
70 }
71 }
72
73 pub fn impairment_standard(&self) -> &'static str {
75 match self {
76 Self::UsGaap => "ASC 360",
77 Self::Ifrs => "IAS 36",
78 Self::DualReporting => "ASC 360 / IAS 36",
79 }
80 }
81
82 pub fn allows_lifo(&self) -> bool {
84 matches!(self, Self::UsGaap)
85 }
86
87 pub fn requires_development_capitalization(&self) -> bool {
89 matches!(self, Self::Ifrs | Self::DualReporting)
90 }
91
92 pub fn allows_ppe_revaluation(&self) -> bool {
94 matches!(self, Self::Ifrs | Self::DualReporting)
95 }
96
97 pub fn allows_impairment_reversal(&self) -> bool {
99 matches!(self, Self::Ifrs | Self::DualReporting)
100 }
101
102 pub fn uses_brightline_lease_tests(&self) -> bool {
104 matches!(self, Self::UsGaap)
105 }
106}
107
108impl std::fmt::Display for AccountingFramework {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 Self::UsGaap => write!(f, "US GAAP"),
112 Self::Ifrs => write!(f, "IFRS"),
113 Self::DualReporting => write!(f, "Dual Reporting (US GAAP & IFRS)"),
114 }
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct FrameworkSettings {
124 pub framework: AccountingFramework,
126
127 #[serde(default)]
131 pub use_lifo_inventory: bool,
132
133 #[serde(default)]
138 pub capitalize_development_costs: bool,
139
140 #[serde(default)]
145 pub use_ppe_revaluation: bool,
146
147 #[serde(default)]
152 pub allow_impairment_reversal: bool,
153
154 #[serde(default = "default_lease_term_threshold")]
159 pub lease_term_threshold: f64,
160
161 #[serde(default = "default_lease_pv_threshold")]
166 pub lease_pv_threshold: f64,
167
168 #[serde(default = "default_incremental_borrowing_rate")]
170 pub default_incremental_borrowing_rate: f64,
171
172 #[serde(default = "default_variable_consideration_constraint")]
178 pub variable_consideration_constraint: f64,
179}
180
181fn default_lease_term_threshold() -> f64 {
182 0.75
183}
184
185fn default_lease_pv_threshold() -> f64 {
186 0.90
187}
188
189fn default_incremental_borrowing_rate() -> f64 {
190 0.05
191}
192
193fn default_variable_consideration_constraint() -> f64 {
194 0.80
195}
196
197impl Default for FrameworkSettings {
198 fn default() -> Self {
199 Self {
200 framework: AccountingFramework::default(),
201 use_lifo_inventory: false,
202 capitalize_development_costs: false,
203 use_ppe_revaluation: false,
204 allow_impairment_reversal: false,
205 lease_term_threshold: default_lease_term_threshold(),
206 lease_pv_threshold: default_lease_pv_threshold(),
207 default_incremental_borrowing_rate: default_incremental_borrowing_rate(),
208 variable_consideration_constraint: default_variable_consideration_constraint(),
209 }
210 }
211}
212
213impl FrameworkSettings {
214 pub fn us_gaap() -> Self {
216 Self {
217 framework: AccountingFramework::UsGaap,
218 use_lifo_inventory: false, capitalize_development_costs: false,
220 use_ppe_revaluation: false,
221 allow_impairment_reversal: false,
222 ..Default::default()
223 }
224 }
225
226 pub fn ifrs() -> Self {
228 Self {
229 framework: AccountingFramework::Ifrs,
230 use_lifo_inventory: false, capitalize_development_costs: true, use_ppe_revaluation: false, allow_impairment_reversal: true, ..Default::default()
235 }
236 }
237
238 pub fn dual_reporting() -> Self {
240 Self {
241 framework: AccountingFramework::DualReporting,
242 use_lifo_inventory: false,
243 capitalize_development_costs: true,
244 use_ppe_revaluation: false,
245 allow_impairment_reversal: true,
246 ..Default::default()
247 }
248 }
249
250 pub fn validate(&self) -> Result<(), FrameworkValidationError> {
252 if self.use_lifo_inventory && self.framework == AccountingFramework::Ifrs {
254 return Err(FrameworkValidationError::LifoNotPermittedUnderIfrs);
255 }
256
257 if self.use_ppe_revaluation && self.framework == AccountingFramework::UsGaap {
259 return Err(FrameworkValidationError::RevaluationNotPermittedUnderUsGaap);
260 }
261
262 if self.allow_impairment_reversal && self.framework == AccountingFramework::UsGaap {
264 return Err(FrameworkValidationError::ImpairmentReversalNotPermittedUnderUsGaap);
265 }
266
267 if !(0.0..=1.0).contains(&self.lease_term_threshold) {
269 return Err(FrameworkValidationError::InvalidThreshold(
270 "lease_term_threshold".to_string(),
271 ));
272 }
273
274 if !(0.0..=1.0).contains(&self.lease_pv_threshold) {
275 return Err(FrameworkValidationError::InvalidThreshold(
276 "lease_pv_threshold".to_string(),
277 ));
278 }
279
280 Ok(())
281 }
282}
283
284#[derive(Debug, Clone, thiserror::Error)]
286pub enum FrameworkValidationError {
287 #[error("LIFO inventory costing is not permitted under IFRS")]
288 LifoNotPermittedUnderIfrs,
289
290 #[error("PPE revaluation above cost is not permitted under US GAAP")]
291 RevaluationNotPermittedUnderUsGaap,
292
293 #[error("Reversal of impairment losses is not permitted under US GAAP")]
294 ImpairmentReversalNotPermittedUnderUsGaap,
295
296 #[error("Invalid threshold value for {0}: must be between 0.0 and 1.0")]
297 InvalidThreshold(String),
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct FrameworkDifference {
303 pub area: String,
305
306 pub us_gaap_treatment: String,
308
309 pub ifrs_treatment: String,
311
312 pub typically_material: bool,
314
315 pub us_gaap_reference: String,
317
318 pub ifrs_reference: String,
320}
321
322impl FrameworkDifference {
323 pub fn common_differences() -> Vec<Self> {
325 vec![
326 Self {
327 area: "Inventory Costing".to_string(),
328 us_gaap_treatment: "LIFO, FIFO, and weighted average permitted".to_string(),
329 ifrs_treatment: "LIFO prohibited; FIFO and weighted average permitted".to_string(),
330 typically_material: true,
331 us_gaap_reference: "ASC 330".to_string(),
332 ifrs_reference: "IAS 2".to_string(),
333 },
334 Self {
335 area: "Development Costs".to_string(),
336 us_gaap_treatment: "Generally expensed as incurred".to_string(),
337 ifrs_treatment: "Capitalized when specified criteria are met".to_string(),
338 typically_material: true,
339 us_gaap_reference: "ASC 730".to_string(),
340 ifrs_reference: "IAS 38".to_string(),
341 },
342 Self {
343 area: "Property, Plant & Equipment".to_string(),
344 us_gaap_treatment: "Cost model only; no revaluation above cost".to_string(),
345 ifrs_treatment: "Cost model or revaluation model permitted".to_string(),
346 typically_material: true,
347 us_gaap_reference: "ASC 360".to_string(),
348 ifrs_reference: "IAS 16".to_string(),
349 },
350 Self {
351 area: "Impairment Reversal".to_string(),
352 us_gaap_treatment: "Not permitted for most assets".to_string(),
353 ifrs_treatment: "Permitted except for goodwill".to_string(),
354 typically_material: true,
355 us_gaap_reference: "ASC 360".to_string(),
356 ifrs_reference: "IAS 36".to_string(),
357 },
358 Self {
359 area: "Lease Classification".to_string(),
360 us_gaap_treatment: "Bright-line tests (75% term, 90% PV)".to_string(),
361 ifrs_treatment: "Principles-based; transfer of risks and rewards".to_string(),
362 typically_material: false,
363 us_gaap_reference: "ASC 842".to_string(),
364 ifrs_reference: "IFRS 16".to_string(),
365 },
366 Self {
367 area: "Contingent Liabilities".to_string(),
368 us_gaap_treatment: "Recognized when probable (>75%) and estimable".to_string(),
369 ifrs_treatment: "Recognized when probable (>50%) and estimable".to_string(),
370 typically_material: true,
371 us_gaap_reference: "ASC 450".to_string(),
372 ifrs_reference: "IAS 37".to_string(),
373 },
374 ]
375 }
376}
377
378#[cfg(test)]
379#[allow(clippy::unwrap_used)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_framework_defaults() {
385 let framework = AccountingFramework::default();
386 assert_eq!(framework, AccountingFramework::UsGaap);
387 }
388
389 #[test]
390 fn test_framework_standards() {
391 assert_eq!(AccountingFramework::UsGaap.revenue_standard(), "ASC 606");
392 assert_eq!(AccountingFramework::Ifrs.revenue_standard(), "IFRS 15");
393 assert_eq!(AccountingFramework::UsGaap.lease_standard(), "ASC 842");
394 assert_eq!(AccountingFramework::Ifrs.lease_standard(), "IFRS 16");
395 }
396
397 #[test]
398 fn test_framework_features() {
399 assert!(AccountingFramework::UsGaap.allows_lifo());
400 assert!(!AccountingFramework::Ifrs.allows_lifo());
401
402 assert!(!AccountingFramework::UsGaap.allows_ppe_revaluation());
403 assert!(AccountingFramework::Ifrs.allows_ppe_revaluation());
404
405 assert!(!AccountingFramework::UsGaap.allows_impairment_reversal());
406 assert!(AccountingFramework::Ifrs.allows_impairment_reversal());
407 }
408
409 #[test]
410 fn test_settings_validation_us_gaap() {
411 let settings = FrameworkSettings::us_gaap();
412 assert!(settings.validate().is_ok());
413 }
414
415 #[test]
416 fn test_settings_validation_ifrs() {
417 let settings = FrameworkSettings::ifrs();
418 assert!(settings.validate().is_ok());
419 }
420
421 #[test]
422 fn test_settings_validation_lifo_under_ifrs() {
423 let mut settings = FrameworkSettings::ifrs();
424 settings.use_lifo_inventory = true;
425 assert!(matches!(
426 settings.validate(),
427 Err(FrameworkValidationError::LifoNotPermittedUnderIfrs)
428 ));
429 }
430
431 #[test]
432 fn test_settings_validation_revaluation_under_us_gaap() {
433 let mut settings = FrameworkSettings::us_gaap();
434 settings.use_ppe_revaluation = true;
435 assert!(matches!(
436 settings.validate(),
437 Err(FrameworkValidationError::RevaluationNotPermittedUnderUsGaap)
438 ));
439 }
440
441 #[test]
442 fn test_common_differences() {
443 let differences = FrameworkDifference::common_differences();
444 assert!(!differences.is_empty());
445 assert!(differences.iter().any(|d| d.area == "Inventory Costing"));
446 }
447
448 #[test]
449 fn test_serde_roundtrip() {
450 let framework = AccountingFramework::Ifrs;
451 let json = serde_json::to_string(&framework).unwrap();
452 let deserialized: AccountingFramework = serde_json::from_str(&json).unwrap();
453 assert_eq!(framework, deserialized);
454
455 let settings = FrameworkSettings::ifrs();
456 let json = serde_json::to_string(&settings).unwrap();
457 let deserialized: FrameworkSettings = serde_json::from_str(&json).unwrap();
458 assert_eq!(settings.framework, deserialized.framework);
459 }
460}