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)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_framework_defaults() {
384 let framework = AccountingFramework::default();
385 assert_eq!(framework, AccountingFramework::UsGaap);
386 }
387
388 #[test]
389 fn test_framework_standards() {
390 assert_eq!(AccountingFramework::UsGaap.revenue_standard(), "ASC 606");
391 assert_eq!(AccountingFramework::Ifrs.revenue_standard(), "IFRS 15");
392 assert_eq!(AccountingFramework::UsGaap.lease_standard(), "ASC 842");
393 assert_eq!(AccountingFramework::Ifrs.lease_standard(), "IFRS 16");
394 }
395
396 #[test]
397 fn test_framework_features() {
398 assert!(AccountingFramework::UsGaap.allows_lifo());
399 assert!(!AccountingFramework::Ifrs.allows_lifo());
400
401 assert!(!AccountingFramework::UsGaap.allows_ppe_revaluation());
402 assert!(AccountingFramework::Ifrs.allows_ppe_revaluation());
403
404 assert!(!AccountingFramework::UsGaap.allows_impairment_reversal());
405 assert!(AccountingFramework::Ifrs.allows_impairment_reversal());
406 }
407
408 #[test]
409 fn test_settings_validation_us_gaap() {
410 let settings = FrameworkSettings::us_gaap();
411 assert!(settings.validate().is_ok());
412 }
413
414 #[test]
415 fn test_settings_validation_ifrs() {
416 let settings = FrameworkSettings::ifrs();
417 assert!(settings.validate().is_ok());
418 }
419
420 #[test]
421 fn test_settings_validation_lifo_under_ifrs() {
422 let mut settings = FrameworkSettings::ifrs();
423 settings.use_lifo_inventory = true;
424 assert!(matches!(
425 settings.validate(),
426 Err(FrameworkValidationError::LifoNotPermittedUnderIfrs)
427 ));
428 }
429
430 #[test]
431 fn test_settings_validation_revaluation_under_us_gaap() {
432 let mut settings = FrameworkSettings::us_gaap();
433 settings.use_ppe_revaluation = true;
434 assert!(matches!(
435 settings.validate(),
436 Err(FrameworkValidationError::RevaluationNotPermittedUnderUsGaap)
437 ));
438 }
439
440 #[test]
441 fn test_common_differences() {
442 let differences = FrameworkDifference::common_differences();
443 assert!(!differences.is_empty());
444 assert!(differences.iter().any(|d| d.area == "Inventory Costing"));
445 }
446
447 #[test]
448 fn test_serde_roundtrip() {
449 let framework = AccountingFramework::Ifrs;
450 let json = serde_json::to_string(&framework).unwrap();
451 let deserialized: AccountingFramework = serde_json::from_str(&json).unwrap();
452 assert_eq!(framework, deserialized);
453
454 let settings = FrameworkSettings::ifrs();
455 let json = serde_json::to_string(&settings).unwrap();
456 let deserialized: FrameworkSettings = serde_json::from_str(&json).unwrap();
457 assert_eq!(settings.framework, deserialized.framework);
458 }
459}