use chrono::NaiveDate;
use rust_decimal::prelude::*;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::framework::AccountingFramework;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImpairmentTest {
pub test_id: Uuid,
pub company_code: String,
pub asset_id: String,
pub asset_description: String,
pub asset_type: ImpairmentAssetType,
pub test_date: NaiveDate,
#[serde(with = "rust_decimal::serde::str")]
pub carrying_amount: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub recoverable_amount: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub fair_value_less_costs: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub value_in_use: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub impairment_loss: Decimal,
pub impairment_indicators: Vec<ImpairmentIndicator>,
pub test_result: ImpairmentTestResult,
pub framework: AccountingFramework,
#[serde(default, with = "rust_decimal::serde::str_option")]
pub undiscounted_cash_flows: Option<Decimal>,
#[serde(with = "rust_decimal::serde::str")]
pub discount_rate: Decimal,
pub cash_flow_projections: Vec<CashFlowProjection>,
pub journal_entry_id: Option<Uuid>,
}
impl ImpairmentTest {
pub fn new(
company_code: impl Into<String>,
asset_id: impl Into<String>,
asset_description: impl Into<String>,
asset_type: ImpairmentAssetType,
test_date: NaiveDate,
carrying_amount: Decimal,
framework: AccountingFramework,
) -> Self {
Self {
test_id: Uuid::now_v7(),
company_code: company_code.into(),
asset_id: asset_id.into(),
asset_description: asset_description.into(),
asset_type,
test_date,
carrying_amount,
recoverable_amount: Decimal::ZERO,
fair_value_less_costs: Decimal::ZERO,
value_in_use: Decimal::ZERO,
impairment_loss: Decimal::ZERO,
impairment_indicators: Vec::new(),
test_result: ImpairmentTestResult::NotImpaired,
framework,
undiscounted_cash_flows: None,
discount_rate: Decimal::ZERO,
cash_flow_projections: Vec::new(),
journal_entry_id: None,
}
}
pub fn add_indicator(&mut self, indicator: ImpairmentIndicator) {
self.impairment_indicators.push(indicator);
}
pub fn perform_test(&mut self) {
match self.framework {
AccountingFramework::UsGaap => self.perform_us_gaap_test(),
AccountingFramework::Ifrs
| AccountingFramework::DualReporting
| AccountingFramework::FrenchGaap
| AccountingFramework::GermanGaap => self.perform_ifrs_test(),
}
}
fn perform_us_gaap_test(&mut self) {
if let Some(undiscounted) = self.undiscounted_cash_flows {
if self.carrying_amount <= undiscounted {
self.test_result = ImpairmentTestResult::NotImpaired;
self.impairment_loss = Decimal::ZERO;
return;
}
}
self.recoverable_amount = self.fair_value_less_costs;
self.impairment_loss = (self.carrying_amount - self.recoverable_amount).max(Decimal::ZERO);
self.test_result = if self.impairment_loss > Decimal::ZERO {
ImpairmentTestResult::Impaired
} else {
ImpairmentTestResult::NotImpaired
};
}
fn perform_ifrs_test(&mut self) {
self.recoverable_amount = self.fair_value_less_costs.max(self.value_in_use);
self.impairment_loss = (self.carrying_amount - self.recoverable_amount).max(Decimal::ZERO);
self.test_result = if self.impairment_loss > Decimal::ZERO {
ImpairmentTestResult::Impaired
} else {
ImpairmentTestResult::NotImpaired
};
}
pub fn calculate_value_in_use(&mut self) {
let mut viu = Decimal::ZERO;
for projection in &self.cash_flow_projections {
let discount_factor = Decimal::ONE
/ (Decimal::ONE + self.discount_rate).powd(Decimal::from(projection.year as i64));
viu += projection.net_cash_flow * discount_factor;
}
self.value_in_use = viu;
}
pub fn calculate_undiscounted_cash_flows(&mut self) {
let undiscounted: Decimal = self
.cash_flow_projections
.iter()
.map(|p| p.net_cash_flow)
.sum();
self.undiscounted_cash_flows = Some(undiscounted);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ImpairmentAssetType {
#[default]
PropertyPlantEquipment,
IntangibleFinite,
IntangibleIndefinite,
Goodwill,
RightOfUseAsset,
EquityInvestment,
CashGeneratingUnit,
}
impl std::fmt::Display for ImpairmentAssetType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PropertyPlantEquipment => write!(f, "Property, Plant & Equipment"),
Self::IntangibleFinite => write!(f, "Intangible Asset (Finite Life)"),
Self::IntangibleIndefinite => write!(f, "Intangible Asset (Indefinite Life)"),
Self::Goodwill => write!(f, "Goodwill"),
Self::RightOfUseAsset => write!(f, "Right-of-Use Asset"),
Self::EquityInvestment => write!(f, "Equity Method Investment"),
Self::CashGeneratingUnit => write!(f, "Cash-Generating Unit"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImpairmentIndicator {
MarketValueDecline,
AdverseEnvironmentChanges,
InterestRateIncrease,
MarketCapBelowBookValue,
ObsolescenceOrDamage,
AdverseUseChanges,
OperatingLosses,
DiscontinuationPlans,
EarlyDisposal,
WorsePerformance,
KeyPersonnelLoss,
MajorCustomerLoss,
CompetitionIncrease,
RegulatoryChanges,
AnnualTest,
}
impl std::fmt::Display for ImpairmentIndicator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MarketValueDecline => write!(f, "Market Value Decline"),
Self::AdverseEnvironmentChanges => write!(f, "Adverse Environment Changes"),
Self::InterestRateIncrease => write!(f, "Interest Rate Increase"),
Self::MarketCapBelowBookValue => write!(f, "Market Cap Below Book Value"),
Self::ObsolescenceOrDamage => write!(f, "Obsolescence or Damage"),
Self::AdverseUseChanges => write!(f, "Adverse Use Changes"),
Self::OperatingLosses => write!(f, "Operating Losses"),
Self::DiscontinuationPlans => write!(f, "Discontinuation Plans"),
Self::EarlyDisposal => write!(f, "Early Disposal"),
Self::WorsePerformance => write!(f, "Worse Performance"),
Self::KeyPersonnelLoss => write!(f, "Key Personnel Loss"),
Self::MajorCustomerLoss => write!(f, "Major Customer Loss"),
Self::CompetitionIncrease => write!(f, "Competition Increase"),
Self::RegulatoryChanges => write!(f, "Regulatory Changes"),
Self::AnnualTest => write!(f, "Annual Test"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ImpairmentTestResult {
#[default]
NotImpaired,
Impaired,
ReversalRecognized,
}
impl std::fmt::Display for ImpairmentTestResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotImpaired => write!(f, "Not Impaired"),
Self::Impaired => write!(f, "Impaired"),
Self::ReversalRecognized => write!(f, "Reversal Recognized"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CashFlowProjection {
pub year: u32,
#[serde(with = "rust_decimal::serde::str")]
pub revenue: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub operating_expenses: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub capital_expenditures: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub net_cash_flow: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub growth_rate: Decimal,
pub is_terminal_value: bool,
}
impl CashFlowProjection {
pub fn new(year: u32, revenue: Decimal, operating_expenses: Decimal) -> Self {
let net_cash_flow = revenue - operating_expenses;
Self {
year,
revenue,
operating_expenses,
capital_expenditures: Decimal::ZERO,
net_cash_flow,
growth_rate: Decimal::ZERO,
is_terminal_value: false,
}
}
pub fn calculate_net_cash_flow(&mut self) {
self.net_cash_flow = self.revenue - self.operating_expenses - self.capital_expenditures;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImpairmentReversal {
pub reversal_id: Uuid,
pub original_test_id: Uuid,
pub asset_id: String,
pub reversal_date: NaiveDate,
#[serde(with = "rust_decimal::serde::str")]
pub carrying_amount_before: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub reversal_amount: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub carrying_amount_after: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub maximum_carrying_amount: Decimal,
pub reversal_reason: String,
pub journal_entry_id: Option<Uuid>,
}
impl ImpairmentReversal {
pub fn is_valid(&self, asset_type: ImpairmentAssetType) -> bool {
if asset_type == ImpairmentAssetType::Goodwill {
return false;
}
self.carrying_amount_after <= self.maximum_carrying_amount
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_impairment_test_creation() {
let test = ImpairmentTest::new(
"1000",
"FA001",
"Manufacturing Equipment",
ImpairmentAssetType::PropertyPlantEquipment,
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
dec!(500000),
AccountingFramework::UsGaap,
);
assert_eq!(test.carrying_amount, dec!(500000));
assert_eq!(test.test_result, ImpairmentTestResult::NotImpaired);
}
#[test]
fn test_us_gaap_impairment_no_impairment() {
let mut test = ImpairmentTest::new(
"1000",
"FA001",
"Manufacturing Equipment",
ImpairmentAssetType::PropertyPlantEquipment,
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
dec!(500000),
AccountingFramework::UsGaap,
);
test.undiscounted_cash_flows = Some(dec!(600000));
test.fair_value_less_costs = dec!(450000);
test.perform_test();
assert_eq!(test.test_result, ImpairmentTestResult::NotImpaired);
assert_eq!(test.impairment_loss, dec!(0));
}
#[test]
fn test_us_gaap_impairment_recognized() {
let mut test = ImpairmentTest::new(
"1000",
"FA001",
"Manufacturing Equipment",
ImpairmentAssetType::PropertyPlantEquipment,
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
dec!(500000),
AccountingFramework::UsGaap,
);
test.undiscounted_cash_flows = Some(dec!(400000));
test.fair_value_less_costs = dec!(350000);
test.perform_test();
assert_eq!(test.test_result, ImpairmentTestResult::Impaired);
assert_eq!(test.impairment_loss, dec!(150000)); }
#[test]
fn test_ifrs_impairment() {
let mut test = ImpairmentTest::new(
"1000",
"FA001",
"Manufacturing Equipment",
ImpairmentAssetType::PropertyPlantEquipment,
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
dec!(500000),
AccountingFramework::Ifrs,
);
test.fair_value_less_costs = dec!(350000);
test.value_in_use = dec!(380000);
test.perform_test();
assert_eq!(test.recoverable_amount, dec!(380000));
assert_eq!(test.impairment_loss, dec!(120000)); assert_eq!(test.test_result, ImpairmentTestResult::Impaired);
}
#[test]
fn test_german_gaap_impairment() {
let mut test = ImpairmentTest::new(
"DE01",
"FA001",
"Industriemaschine",
ImpairmentAssetType::PropertyPlantEquipment,
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
dec!(500000),
AccountingFramework::GermanGaap,
);
test.fair_value_less_costs = dec!(300000);
test.value_in_use = dec!(350000);
test.perform_test();
assert_eq!(test.recoverable_amount, dec!(350000));
assert_eq!(test.impairment_loss, dec!(150000));
assert_eq!(test.test_result, ImpairmentTestResult::Impaired);
assert!(AccountingFramework::GermanGaap.allows_impairment_reversal());
}
#[test]
fn test_german_gaap_goodwill_impairment() {
let mut test = ImpairmentTest::new(
"DE01",
"GW001",
"Geschäftswert",
ImpairmentAssetType::Goodwill,
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
dec!(200000),
AccountingFramework::GermanGaap,
);
test.fair_value_less_costs = dec!(150000);
test.value_in_use = dec!(160000);
test.perform_test();
assert_eq!(test.recoverable_amount, dec!(160000));
assert_eq!(test.impairment_loss, dec!(40000));
}
#[test]
fn test_value_in_use_calculation() {
let mut test = ImpairmentTest::new(
"1000",
"FA001",
"Manufacturing Equipment",
ImpairmentAssetType::PropertyPlantEquipment,
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
dec!(500000),
AccountingFramework::Ifrs,
);
test.discount_rate = dec!(0.10);
test.cash_flow_projections = vec![
CashFlowProjection::new(1, dec!(200000), dec!(100000)),
CashFlowProjection::new(2, dec!(200000), dec!(100000)),
CashFlowProjection::new(3, dec!(200000), dec!(100000)),
];
test.calculate_value_in_use();
assert!(test.value_in_use > dec!(240000));
assert!(test.value_in_use < dec!(260000));
}
}