Skip to main content

datasynth_standards/accounting/
fair_value.rs

1//! Fair Value Measurement Models (ASC 820 / IFRS 13).
2//!
3//! Implements fair value measurement concepts for financial reporting:
4//!
5//! - Fair value hierarchy (Level 1, 2, 3 inputs)
6//! - Valuation techniques
7//! - Fair value disclosures
8//!
9//! Both ASC 820 and IFRS 13 are substantially converged, with
10//! largely consistent requirements for fair value measurement.
11
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16use crate::framework::AccountingFramework;
17
18/// Fair value measurement record.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct FairValueMeasurement {
21    /// Unique measurement identifier.
22    pub measurement_id: Uuid,
23
24    /// Asset or liability being measured.
25    pub item_id: String,
26
27    /// Description of the item.
28    pub item_description: String,
29
30    /// Category of measurement.
31    pub item_category: FairValueCategory,
32
33    /// Fair value hierarchy level.
34    pub hierarchy_level: FairValueHierarchyLevel,
35
36    /// Valuation technique used.
37    pub valuation_technique: ValuationTechnique,
38
39    /// Measured fair value.
40    #[serde(with = "rust_decimal::serde::str")]
41    pub fair_value: Decimal,
42
43    /// Carrying amount (if different from fair value).
44    #[serde(with = "rust_decimal::serde::str")]
45    pub carrying_amount: Decimal,
46
47    /// Measurement date.
48    pub measurement_date: chrono::NaiveDate,
49
50    /// Currency.
51    pub currency: String,
52
53    /// Key inputs used in valuation.
54    pub valuation_inputs: Vec<ValuationInput>,
55
56    /// Whether this is a recurring or non-recurring measurement.
57    pub measurement_type: MeasurementType,
58
59    /// Framework applied.
60    pub framework: AccountingFramework,
61
62    /// Sensitivity analysis (for Level 3 measurements).
63    pub sensitivity_analysis: Option<SensitivityAnalysis>,
64}
65
66impl FairValueMeasurement {
67    /// Create a new fair value measurement.
68    #[allow(clippy::too_many_arguments)]
69    pub fn new(
70        item_id: impl Into<String>,
71        item_description: impl Into<String>,
72        item_category: FairValueCategory,
73        hierarchy_level: FairValueHierarchyLevel,
74        fair_value: Decimal,
75        measurement_date: chrono::NaiveDate,
76        currency: impl Into<String>,
77        framework: AccountingFramework,
78    ) -> Self {
79        Self {
80            measurement_id: Uuid::now_v7(),
81            item_id: item_id.into(),
82            item_description: item_description.into(),
83            item_category,
84            hierarchy_level,
85            valuation_technique: ValuationTechnique::default(),
86            fair_value,
87            carrying_amount: fair_value,
88            measurement_date,
89            currency: currency.into(),
90            valuation_inputs: Vec::new(),
91            measurement_type: MeasurementType::Recurring,
92            framework,
93            sensitivity_analysis: None,
94        }
95    }
96
97    /// Add a valuation input.
98    pub fn add_input(&mut self, input: ValuationInput) {
99        self.valuation_inputs.push(input);
100    }
101
102    /// Calculate unrealized gain/loss.
103    pub fn unrealized_gain_loss(&self) -> Decimal {
104        self.fair_value - self.carrying_amount
105    }
106}
107
108/// Fair value hierarchy level.
109///
110/// The fair value hierarchy prioritizes the inputs to valuation techniques:
111/// - Level 1: Quoted prices in active markets (most reliable)
112/// - Level 2: Observable inputs other than Level 1 prices
113/// - Level 3: Unobservable inputs (requires more judgment)
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
115#[serde(rename_all = "snake_case")]
116pub enum FairValueHierarchyLevel {
117    /// Quoted prices in active markets for identical assets/liabilities.
118    ///
119    /// Examples: Exchange-traded securities, commodities with active markets.
120    #[default]
121    Level1,
122
123    /// Observable inputs other than Level 1 prices.
124    ///
125    /// Examples: Quoted prices for similar items, interest rates, yield curves.
126    Level2,
127
128    /// Unobservable inputs based on entity's assumptions.
129    ///
130    /// Examples: Discounted cash flow using internal projections,
131    /// privately held investments, complex derivatives.
132    Level3,
133}
134
135impl std::fmt::Display for FairValueHierarchyLevel {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        match self {
138            Self::Level1 => write!(f, "Level 1"),
139            Self::Level2 => write!(f, "Level 2"),
140            Self::Level3 => write!(f, "Level 3"),
141        }
142    }
143}
144
145/// Category of item being measured at fair value.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
147#[serde(rename_all = "snake_case")]
148pub enum FairValueCategory {
149    /// Trading securities.
150    #[default]
151    TradingSecurities,
152    /// Available-for-sale securities (US GAAP) / FVOCI (IFRS).
153    AvailableForSale,
154    /// Derivative financial instruments.
155    Derivatives,
156    /// Investment property (IFRS fair value model).
157    InvestmentProperty,
158    /// Biological assets.
159    BiologicalAssets,
160    /// Pension plan assets.
161    PensionAssets,
162    /// Contingent consideration (business combinations).
163    ContingentConsideration,
164    /// Impaired assets.
165    ImpairedAssets,
166    /// Other fair value items.
167    Other,
168}
169
170impl std::fmt::Display for FairValueCategory {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            Self::TradingSecurities => write!(f, "Trading Securities"),
174            Self::AvailableForSale => write!(f, "Available-for-Sale Securities"),
175            Self::Derivatives => write!(f, "Derivatives"),
176            Self::InvestmentProperty => write!(f, "Investment Property"),
177            Self::BiologicalAssets => write!(f, "Biological Assets"),
178            Self::PensionAssets => write!(f, "Pension Plan Assets"),
179            Self::ContingentConsideration => write!(f, "Contingent Consideration"),
180            Self::ImpairedAssets => write!(f, "Impaired Assets"),
181            Self::Other => write!(f, "Other"),
182        }
183    }
184}
185
186/// Valuation technique used in fair value measurement.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
188#[serde(rename_all = "snake_case")]
189pub enum ValuationTechnique {
190    /// Market approach - uses prices from market transactions.
191    #[default]
192    MarketApproach,
193    /// Income approach - converts future amounts to present value.
194    IncomeApproach,
195    /// Cost approach - current replacement cost.
196    CostApproach,
197    /// Combination of multiple approaches.
198    MultipleApproaches,
199}
200
201impl std::fmt::Display for ValuationTechnique {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        match self {
204            Self::MarketApproach => write!(f, "Market Approach"),
205            Self::IncomeApproach => write!(f, "Income Approach"),
206            Self::CostApproach => write!(f, "Cost Approach"),
207            Self::MultipleApproaches => write!(f, "Multiple Approaches"),
208        }
209    }
210}
211
212/// Type of fair value measurement.
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
214#[serde(rename_all = "snake_case")]
215pub enum MeasurementType {
216    /// Recurring measurement each reporting period.
217    #[default]
218    Recurring,
219    /// Non-recurring measurement (e.g., impairment).
220    NonRecurring,
221}
222
223/// Valuation input used in fair value measurement.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct ValuationInput {
226    /// Input name/description.
227    pub name: String,
228
229    /// Input value.
230    #[serde(with = "rust_decimal::serde::str")]
231    pub value: Decimal,
232
233    /// Unit of measurement.
234    pub unit: String,
235
236    /// Whether this is an observable or unobservable input.
237    pub observable: bool,
238
239    /// Source of the input.
240    pub source: String,
241}
242
243impl ValuationInput {
244    /// Create a new valuation input.
245    pub fn new(
246        name: impl Into<String>,
247        value: Decimal,
248        unit: impl Into<String>,
249        observable: bool,
250        source: impl Into<String>,
251    ) -> Self {
252        Self {
253            name: name.into(),
254            value,
255            unit: unit.into(),
256            observable,
257            source: source.into(),
258        }
259    }
260
261    /// Create a discount rate input.
262    pub fn discount_rate(rate: Decimal, source: impl Into<String>) -> Self {
263        Self::new("Discount Rate", rate, "%", true, source)
264    }
265
266    /// Create an expected growth rate input.
267    pub fn growth_rate(rate: Decimal, source: impl Into<String>) -> Self {
268        Self::new("Expected Growth Rate", rate, "%", false, source)
269    }
270}
271
272/// Sensitivity analysis for Level 3 measurements.
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct SensitivityAnalysis {
275    /// Primary unobservable input.
276    pub input_name: String,
277
278    /// Range tested (low, high).
279    #[serde(with = "decimal_tuple")]
280    pub input_range: (Decimal, Decimal),
281
282    /// Fair value at low end of range.
283    #[serde(with = "rust_decimal::serde::str")]
284    pub fair_value_low: Decimal,
285
286    /// Fair value at high end of range.
287    #[serde(with = "rust_decimal::serde::str")]
288    pub fair_value_high: Decimal,
289
290    /// Correlation with other inputs.
291    pub correlated_inputs: Vec<String>,
292}
293
294mod decimal_tuple {
295    use rust_decimal::Decimal;
296    use serde::{Deserialize, Deserializer, Serialize, Serializer};
297
298    pub fn serialize<S>(value: &(Decimal, Decimal), serializer: S) -> Result<S::Ok, S::Error>
299    where
300        S: Serializer,
301    {
302        let tuple = (value.0.to_string(), value.1.to_string());
303        tuple.serialize(serializer)
304    }
305
306    pub fn deserialize<'de, D>(deserializer: D) -> Result<(Decimal, Decimal), D::Error>
307    where
308        D: Deserializer<'de>,
309    {
310        let tuple: (String, String) = Deserialize::deserialize(deserializer)?;
311        let low = tuple
312            .0
313            .parse()
314            .map_err(|_| serde::de::Error::custom("invalid decimal"))?;
315        let high = tuple
316            .1
317            .parse()
318            .map_err(|_| serde::de::Error::custom("invalid decimal"))?;
319        Ok((low, high))
320    }
321}
322
323/// Fair value hierarchy summary for disclosure.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct FairValueHierarchySummary {
326    /// Reporting period end date.
327    pub period_date: chrono::NaiveDate,
328
329    /// Company code.
330    pub company_code: String,
331
332    /// Total Level 1 assets.
333    #[serde(with = "rust_decimal::serde::str")]
334    pub level1_assets: Decimal,
335
336    /// Total Level 2 assets.
337    #[serde(with = "rust_decimal::serde::str")]
338    pub level2_assets: Decimal,
339
340    /// Total Level 3 assets.
341    #[serde(with = "rust_decimal::serde::str")]
342    pub level3_assets: Decimal,
343
344    /// Total Level 1 liabilities.
345    #[serde(with = "rust_decimal::serde::str")]
346    pub level1_liabilities: Decimal,
347
348    /// Total Level 2 liabilities.
349    #[serde(with = "rust_decimal::serde::str")]
350    pub level2_liabilities: Decimal,
351
352    /// Total Level 3 liabilities.
353    #[serde(with = "rust_decimal::serde::str")]
354    pub level3_liabilities: Decimal,
355
356    /// Framework applied.
357    pub framework: AccountingFramework,
358}
359
360impl FairValueHierarchySummary {
361    /// Create a new summary.
362    pub fn new(
363        period_date: chrono::NaiveDate,
364        company_code: impl Into<String>,
365        framework: AccountingFramework,
366    ) -> Self {
367        Self {
368            period_date,
369            company_code: company_code.into(),
370            level1_assets: Decimal::ZERO,
371            level2_assets: Decimal::ZERO,
372            level3_assets: Decimal::ZERO,
373            level1_liabilities: Decimal::ZERO,
374            level2_liabilities: Decimal::ZERO,
375            level3_liabilities: Decimal::ZERO,
376            framework,
377        }
378    }
379
380    /// Total fair value assets.
381    pub fn total_assets(&self) -> Decimal {
382        self.level1_assets + self.level2_assets + self.level3_assets
383    }
384
385    /// Total fair value liabilities.
386    pub fn total_liabilities(&self) -> Decimal {
387        self.level1_liabilities + self.level2_liabilities + self.level3_liabilities
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use rust_decimal_macros::dec;
395
396    #[test]
397    fn test_fair_value_measurement() {
398        let measurement = FairValueMeasurement::new(
399            "SEC001",
400            "ABC Corp Common Stock",
401            FairValueCategory::TradingSecurities,
402            FairValueHierarchyLevel::Level1,
403            dec!(50000),
404            chrono::NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
405            "USD",
406            AccountingFramework::UsGaap,
407        );
408
409        assert_eq!(measurement.fair_value, dec!(50000));
410        assert_eq!(measurement.hierarchy_level, FairValueHierarchyLevel::Level1);
411    }
412
413    #[test]
414    fn test_unrealized_gain_loss() {
415        let mut measurement = FairValueMeasurement::new(
416            "SEC001",
417            "XYZ Corp Stock",
418            FairValueCategory::TradingSecurities,
419            FairValueHierarchyLevel::Level1,
420            dec!(55000), // Current fair value
421            chrono::NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
422            "USD",
423            AccountingFramework::UsGaap,
424        );
425        measurement.carrying_amount = dec!(50000); // Original cost
426
427        assert_eq!(measurement.unrealized_gain_loss(), dec!(5000));
428    }
429
430    #[test]
431    fn test_hierarchy_summary() {
432        let mut summary = FairValueHierarchySummary::new(
433            chrono::NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
434            "1000",
435            AccountingFramework::UsGaap,
436        );
437
438        summary.level1_assets = dec!(100000);
439        summary.level2_assets = dec!(50000);
440        summary.level3_assets = dec!(25000);
441
442        assert_eq!(summary.total_assets(), dec!(175000));
443    }
444}