use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CashGeneratingUnit {
pub cgu_id: String,
pub name: String,
pub member_entity_codes: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub segment_code: Option<String>,
}
impl CashGeneratingUnit {
pub fn new(
cgu_id: impl Into<String>,
name: impl Into<String>,
member_entity_codes: Vec<String>,
) -> Self {
Self {
cgu_id: cgu_id.into(),
name: name.into(),
member_entity_codes,
segment_code: None,
}
}
pub fn with_segment(mut self, segment_code: impl Into<String>) -> Self {
self.segment_code = Some(segment_code.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GoodwillAllocation {
pub cgu_id: String,
pub business_combination_id: String,
#[serde(with = "crate::serde_decimal")]
pub goodwill_amount: Decimal,
pub allocation_date: NaiveDate,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CguImpairmentTest {
pub cgu_id: String,
pub test_date: NaiveDate,
#[serde(with = "crate::serde_decimal")]
pub allocated_goodwill: Decimal,
#[serde(with = "crate::serde_decimal")]
pub other_carrying: Decimal,
#[serde(with = "crate::serde_decimal")]
pub fair_value_less_costs: Decimal,
#[serde(with = "crate::serde_decimal")]
pub value_in_use: Decimal,
pub currency: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CguImpairmentResult {
pub cgu_id: String,
pub test_date: NaiveDate,
#[serde(with = "crate::serde_decimal")]
pub carrying_total: Decimal,
#[serde(with = "crate::serde_decimal")]
pub recoverable_amount: Decimal,
#[serde(with = "crate::serde_decimal")]
pub impairment_loss_total: Decimal,
#[serde(with = "crate::serde_decimal")]
pub impairment_loss_to_goodwill: Decimal,
#[serde(with = "crate::serde_decimal")]
pub impairment_loss_to_other_assets: Decimal,
pub currency: String,
}
impl CguImpairmentTest {
pub fn run(&self) -> CguImpairmentResult {
let carrying_total = self.allocated_goodwill + self.other_carrying;
let recoverable_amount = self.fair_value_less_costs.max(self.value_in_use);
let impairment_loss_total = (carrying_total - recoverable_amount).max(Decimal::ZERO);
let impairment_loss_to_goodwill = impairment_loss_total.min(self.allocated_goodwill);
let impairment_loss_to_other_assets = impairment_loss_total - impairment_loss_to_goodwill;
CguImpairmentResult {
cgu_id: self.cgu_id.clone(),
test_date: self.test_date,
carrying_total,
recoverable_amount,
impairment_loss_total,
impairment_loss_to_goodwill,
impairment_loss_to_other_assets,
currency: self.currency.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn date() -> NaiveDate {
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()
}
fn impaired_test_sample() -> CguImpairmentTest {
CguImpairmentTest {
cgu_id: "CGU-EMEA".to_string(),
test_date: date(),
allocated_goodwill: dec!(100_000),
other_carrying: dec!(900_000),
fair_value_less_costs: dec!(750_000),
value_in_use: dec!(800_000),
currency: "EUR".to_string(),
}
}
#[test]
fn impaired_cgu_allocates_to_goodwill_first() {
let result = impaired_test_sample().run();
assert_eq!(result.carrying_total, dec!(1_000_000));
assert_eq!(result.recoverable_amount, dec!(800_000));
assert_eq!(result.impairment_loss_total, dec!(200_000));
assert_eq!(
result.impairment_loss_to_goodwill,
dec!(100_000),
"IAS 36 § 104 — goodwill takes the first hit, capped at allocated amount"
);
assert_eq!(
result.impairment_loss_to_other_assets,
dec!(100_000),
"residual flows pro-rata to other assets"
);
}
#[test]
fn recoverable_cgu_has_no_impairment() {
let mut test = impaired_test_sample();
test.value_in_use = dec!(1_500_000);
let result = test.run();
assert_eq!(result.recoverable_amount, dec!(1_500_000));
assert_eq!(result.impairment_loss_total, Decimal::ZERO);
assert_eq!(result.impairment_loss_to_goodwill, Decimal::ZERO);
assert_eq!(result.impairment_loss_to_other_assets, Decimal::ZERO);
}
#[test]
fn impairment_smaller_than_goodwill_only_hits_goodwill() {
let mut test = impaired_test_sample();
test.fair_value_less_costs = dec!(950_000);
test.value_in_use = dec!(900_000);
let result = test.run();
assert_eq!(result.impairment_loss_total, dec!(50_000));
assert_eq!(result.impairment_loss_to_goodwill, dec!(50_000));
assert_eq!(result.impairment_loss_to_other_assets, Decimal::ZERO);
}
#[test]
fn impairment_uses_higher_of_fv_and_viu() {
let mut test = impaired_test_sample();
test.fair_value_less_costs = dec!(700_000);
test.value_in_use = dec!(600_000);
let result = test.run();
assert_eq!(
result.recoverable_amount,
dec!(700_000),
"recoverable = max(FV, VIU) per IAS 36 § 18"
);
}
#[test]
fn cgu_with_no_allocated_goodwill_only_impairs_other_assets() {
let test = CguImpairmentTest {
cgu_id: "CGU-RND".to_string(),
test_date: date(),
allocated_goodwill: Decimal::ZERO,
other_carrying: dec!(500_000),
fair_value_less_costs: dec!(420_000),
value_in_use: dec!(400_000),
currency: "EUR".to_string(),
};
let result = test.run();
assert_eq!(result.impairment_loss_total, dec!(80_000));
assert_eq!(result.impairment_loss_to_goodwill, Decimal::ZERO);
assert_eq!(result.impairment_loss_to_other_assets, dec!(80_000));
}
#[test]
fn cgu_test_round_trips_via_serde() {
let test = impaired_test_sample();
let json = serde_json::to_string(&test).unwrap();
let back: CguImpairmentTest = serde_json::from_str(&json).unwrap();
assert_eq!(back, test);
let result = test.run();
let result_json = serde_json::to_string(&result).unwrap();
let result_back: CguImpairmentResult = serde_json::from_str(&result_json).unwrap();
assert_eq!(result_back, result);
}
#[test]
fn cgu_with_segment_carries_segment_code() {
let cgu = CashGeneratingUnit::new(
"CGU-EMEA",
"EMEA Consumer",
vec!["ACME_DE".to_string(), "ACME_FR".to_string()],
)
.with_segment("SEG-CONSUMER");
assert_eq!(cgu.segment_code.as_deref(), Some("SEG-CONSUMER"));
assert_eq!(cgu.member_entity_codes.len(), 2);
}
#[test]
fn goodwill_allocation_round_trips() {
let alloc = GoodwillAllocation {
cgu_id: "CGU-EMEA".to_string(),
business_combination_id: "BC-001".to_string(),
goodwill_amount: dec!(750_000),
allocation_date: date(),
};
let json = serde_json::to_string(&alloc).unwrap();
let back: GoodwillAllocation = serde_json::from_str(&json).unwrap();
assert_eq!(back, alloc);
}
}