use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::framework::AccountingFramework;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FairValueMeasurement {
pub measurement_id: Uuid,
pub item_id: String,
pub item_description: String,
pub item_category: FairValueCategory,
pub hierarchy_level: FairValueHierarchyLevel,
pub valuation_technique: ValuationTechnique,
#[serde(with = "datasynth_core::serde_decimal")]
pub fair_value: Decimal,
#[serde(with = "datasynth_core::serde_decimal")]
pub carrying_amount: Decimal,
pub measurement_date: chrono::NaiveDate,
pub currency: String,
pub valuation_inputs: Vec<ValuationInput>,
pub measurement_type: MeasurementType,
pub framework: AccountingFramework,
pub sensitivity_analysis: Option<SensitivityAnalysis>,
}
impl FairValueMeasurement {
#[allow(clippy::too_many_arguments)]
pub fn new(
item_id: impl Into<String>,
item_description: impl Into<String>,
item_category: FairValueCategory,
hierarchy_level: FairValueHierarchyLevel,
fair_value: Decimal,
measurement_date: chrono::NaiveDate,
currency: impl Into<String>,
framework: AccountingFramework,
) -> Self {
Self {
measurement_id: Uuid::now_v7(),
item_id: item_id.into(),
item_description: item_description.into(),
item_category,
hierarchy_level,
valuation_technique: ValuationTechnique::default(),
fair_value,
carrying_amount: fair_value,
measurement_date,
currency: currency.into(),
valuation_inputs: Vec::new(),
measurement_type: MeasurementType::Recurring,
framework,
sensitivity_analysis: None,
}
}
pub fn add_input(&mut self, input: ValuationInput) {
self.valuation_inputs.push(input);
}
pub fn unrealized_gain_loss(&self) -> Decimal {
self.fair_value - self.carrying_amount
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum FairValueHierarchyLevel {
#[default]
Level1,
Level2,
Level3,
}
impl std::fmt::Display for FairValueHierarchyLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Level1 => write!(f, "Level 1"),
Self::Level2 => write!(f, "Level 2"),
Self::Level3 => write!(f, "Level 3"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum FairValueCategory {
#[default]
TradingSecurities,
AvailableForSale,
Derivatives,
InvestmentProperty,
BiologicalAssets,
PensionAssets,
ContingentConsideration,
ImpairedAssets,
Other,
}
impl std::fmt::Display for FairValueCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TradingSecurities => write!(f, "Trading Securities"),
Self::AvailableForSale => write!(f, "Available-for-Sale Securities"),
Self::Derivatives => write!(f, "Derivatives"),
Self::InvestmentProperty => write!(f, "Investment Property"),
Self::BiologicalAssets => write!(f, "Biological Assets"),
Self::PensionAssets => write!(f, "Pension Plan Assets"),
Self::ContingentConsideration => write!(f, "Contingent Consideration"),
Self::ImpairedAssets => write!(f, "Impaired Assets"),
Self::Other => write!(f, "Other"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ValuationTechnique {
#[default]
MarketApproach,
IncomeApproach,
CostApproach,
MultipleApproaches,
}
impl std::fmt::Display for ValuationTechnique {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MarketApproach => write!(f, "Market Approach"),
Self::IncomeApproach => write!(f, "Income Approach"),
Self::CostApproach => write!(f, "Cost Approach"),
Self::MultipleApproaches => write!(f, "Multiple Approaches"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum MeasurementType {
#[default]
Recurring,
NonRecurring,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationInput {
pub name: String,
#[serde(with = "datasynth_core::serde_decimal")]
pub value: Decimal,
pub unit: String,
pub observable: bool,
pub source: String,
}
impl ValuationInput {
pub fn new(
name: impl Into<String>,
value: Decimal,
unit: impl Into<String>,
observable: bool,
source: impl Into<String>,
) -> Self {
Self {
name: name.into(),
value,
unit: unit.into(),
observable,
source: source.into(),
}
}
pub fn discount_rate(rate: Decimal, source: impl Into<String>) -> Self {
Self::new("Discount Rate", rate, "%", true, source)
}
pub fn growth_rate(rate: Decimal, source: impl Into<String>) -> Self {
Self::new("Expected Growth Rate", rate, "%", false, source)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SensitivityAnalysis {
pub input_name: String,
#[serde(with = "decimal_tuple")]
pub input_range: (Decimal, Decimal),
#[serde(with = "datasynth_core::serde_decimal")]
pub fair_value_low: Decimal,
#[serde(with = "datasynth_core::serde_decimal")]
pub fair_value_high: Decimal,
pub correlated_inputs: Vec<String>,
}
mod decimal_tuple {
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(value: &(Decimal, Decimal), serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let tuple = (value.0.to_string(), value.1.to_string());
tuple.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<(Decimal, Decimal), D::Error>
where
D: Deserializer<'de>,
{
let tuple: (String, String) = Deserialize::deserialize(deserializer)?;
let low = tuple
.0
.parse()
.map_err(|_| serde::de::Error::custom("invalid decimal"))?;
let high = tuple
.1
.parse()
.map_err(|_| serde::de::Error::custom("invalid decimal"))?;
Ok((low, high))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FairValueHierarchySummary {
pub period_date: chrono::NaiveDate,
pub company_code: String,
#[serde(with = "datasynth_core::serde_decimal")]
pub level1_assets: Decimal,
#[serde(with = "datasynth_core::serde_decimal")]
pub level2_assets: Decimal,
#[serde(with = "datasynth_core::serde_decimal")]
pub level3_assets: Decimal,
#[serde(with = "datasynth_core::serde_decimal")]
pub level1_liabilities: Decimal,
#[serde(with = "datasynth_core::serde_decimal")]
pub level2_liabilities: Decimal,
#[serde(with = "datasynth_core::serde_decimal")]
pub level3_liabilities: Decimal,
pub framework: AccountingFramework,
}
impl FairValueHierarchySummary {
pub fn new(
period_date: chrono::NaiveDate,
company_code: impl Into<String>,
framework: AccountingFramework,
) -> Self {
Self {
period_date,
company_code: company_code.into(),
level1_assets: Decimal::ZERO,
level2_assets: Decimal::ZERO,
level3_assets: Decimal::ZERO,
level1_liabilities: Decimal::ZERO,
level2_liabilities: Decimal::ZERO,
level3_liabilities: Decimal::ZERO,
framework,
}
}
pub fn total_assets(&self) -> Decimal {
self.level1_assets + self.level2_assets + self.level3_assets
}
pub fn total_liabilities(&self) -> Decimal {
self.level1_liabilities + self.level2_liabilities + self.level3_liabilities
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_fair_value_measurement() {
let measurement = FairValueMeasurement::new(
"SEC001",
"ABC Corp Common Stock",
FairValueCategory::TradingSecurities,
FairValueHierarchyLevel::Level1,
dec!(50000),
chrono::NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
"USD",
AccountingFramework::UsGaap,
);
assert_eq!(measurement.fair_value, dec!(50000));
assert_eq!(measurement.hierarchy_level, FairValueHierarchyLevel::Level1);
}
#[test]
fn test_unrealized_gain_loss() {
let mut measurement = FairValueMeasurement::new(
"SEC001",
"XYZ Corp Stock",
FairValueCategory::TradingSecurities,
FairValueHierarchyLevel::Level1,
dec!(55000), chrono::NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
"USD",
AccountingFramework::UsGaap,
);
measurement.carrying_amount = dec!(50000);
assert_eq!(measurement.unrealized_gain_loss(), dec!(5000));
}
#[test]
fn test_hierarchy_summary() {
let mut summary = FairValueHierarchySummary::new(
chrono::NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
"1000",
AccountingFramework::UsGaap,
);
summary.level1_assets = dec!(100000);
summary.level2_assets = dec!(50000);
summary.level3_assets = dec!(25000);
assert_eq!(summary.total_assets(), dec!(175000));
}
}