use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use super::group_structure::NciMeasurementMethod;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OwnershipChangeType {
ControlGained,
ControlIncreased,
ControlDecreased,
ControlLost,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OwnershipChangeEvent {
pub entity_code: String,
pub parent_entity_code: String,
pub event_type: OwnershipChangeType,
pub effective_date: NaiveDate,
#[serde(with = "crate::serde_decimal")]
pub ownership_percent_before: Decimal,
#[serde(with = "crate::serde_decimal")]
pub ownership_percent_after: Decimal,
#[serde(default, with = "crate::serde_decimal::option")]
pub previously_held_interest_carrying: Option<Decimal>,
#[serde(default, with = "crate::serde_decimal::option")]
pub previously_held_interest_fair_value: Option<Decimal>,
#[serde(with = "crate::serde_decimal")]
pub consideration_paid_or_received: Decimal,
#[serde(default, with = "crate::serde_decimal::option")]
pub acquisition_date_nci_fair_value: Option<Decimal>,
#[serde(default)]
pub nci_measurement_method: NciMeasurementMethod,
pub currency: String,
}
impl OwnershipChangeEvent {
pub fn p_and_l_gain_or_loss(&self) -> Option<Decimal> {
match self.event_type {
OwnershipChangeType::ControlGained | OwnershipChangeType::ControlLost => {
let carrying = self.previously_held_interest_carrying?;
let fv = self.previously_held_interest_fair_value?;
Some(fv - carrying)
}
OwnershipChangeType::ControlIncreased | OwnershipChangeType::ControlDecreased => None,
}
}
pub fn triggers_pl_remeasurement(&self) -> bool {
matches!(
self.event_type,
OwnershipChangeType::ControlGained | OwnershipChangeType::ControlLost
)
}
pub fn triggers_method_transition(&self) -> bool {
matches!(
self.event_type,
OwnershipChangeType::ControlGained | OwnershipChangeType::ControlLost
)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn date() -> NaiveDate {
NaiveDate::from_ymd_opt(2024, 7, 1).unwrap()
}
fn control_gained_sample() -> OwnershipChangeEvent {
OwnershipChangeEvent {
entity_code: "SUB1".to_string(),
parent_entity_code: "PARENT".to_string(),
event_type: OwnershipChangeType::ControlGained,
effective_date: date(),
ownership_percent_before: dec!(0.30),
ownership_percent_after: dec!(0.80),
previously_held_interest_carrying: Some(dec!(100_000)),
previously_held_interest_fair_value: Some(dec!(130_000)),
consideration_paid_or_received: dec!(900_000),
acquisition_date_nci_fair_value: Some(dec!(310_000)),
nci_measurement_method: NciMeasurementMethod::FullGoodwill,
currency: "EUR".to_string(),
}
}
#[test]
fn control_gained_recognises_remeasurement_gain() {
let ev = control_gained_sample();
assert_eq!(
ev.p_and_l_gain_or_loss(),
Some(dec!(30_000)),
"IFRS 3.42 gain = FV (130k) − carrying (100k)"
);
assert!(ev.triggers_pl_remeasurement());
assert!(ev.triggers_method_transition());
}
#[test]
fn control_lost_with_loss_in_p_and_l() {
let ev = OwnershipChangeEvent {
entity_code: "SUB1".to_string(),
parent_entity_code: "PARENT".to_string(),
event_type: OwnershipChangeType::ControlLost,
effective_date: date(),
ownership_percent_before: dec!(0.80),
ownership_percent_after: dec!(0.30),
previously_held_interest_carrying: Some(dec!(250_000)),
previously_held_interest_fair_value: Some(dec!(200_000)),
consideration_paid_or_received: dec!(-600_000), acquisition_date_nci_fair_value: None,
nci_measurement_method: NciMeasurementMethod::Proportionate,
currency: "EUR".to_string(),
};
assert_eq!(ev.p_and_l_gain_or_loss(), Some(dec!(-50_000)));
assert!(ev.triggers_pl_remeasurement());
assert!(ev.triggers_method_transition());
}
#[test]
fn control_increased_is_equity_transaction_no_p_and_l() {
let ev = OwnershipChangeEvent {
entity_code: "SUB1".to_string(),
parent_entity_code: "PARENT".to_string(),
event_type: OwnershipChangeType::ControlIncreased,
effective_date: date(),
ownership_percent_before: dec!(0.60),
ownership_percent_after: dec!(0.80),
previously_held_interest_carrying: Some(dec!(500_000)),
previously_held_interest_fair_value: Some(dec!(550_000)),
consideration_paid_or_received: dec!(200_000),
acquisition_date_nci_fair_value: None,
nci_measurement_method: NciMeasurementMethod::Proportionate,
currency: "EUR".to_string(),
};
assert_eq!(
ev.p_and_l_gain_or_loss(),
None,
"IFRS 10.23 — equity transaction, no P&L gain/loss"
);
assert!(!ev.triggers_pl_remeasurement());
assert!(!ev.triggers_method_transition());
}
#[test]
fn control_decreased_is_equity_transaction_no_p_and_l() {
let ev = OwnershipChangeEvent {
entity_code: "SUB1".to_string(),
parent_entity_code: "PARENT".to_string(),
event_type: OwnershipChangeType::ControlDecreased,
effective_date: date(),
ownership_percent_before: dec!(0.80),
ownership_percent_after: dec!(0.55),
previously_held_interest_carrying: Some(dec!(800_000)),
previously_held_interest_fair_value: Some(dec!(700_000)),
consideration_paid_or_received: dec!(-300_000),
acquisition_date_nci_fair_value: None,
nci_measurement_method: NciMeasurementMethod::Proportionate,
currency: "EUR".to_string(),
};
assert_eq!(ev.p_and_l_gain_or_loss(), None);
assert!(!ev.triggers_pl_remeasurement());
assert!(!ev.triggers_method_transition());
}
#[test]
fn p_and_l_returns_none_when_carrying_or_fv_missing() {
let mut ev = control_gained_sample();
ev.previously_held_interest_carrying = None;
assert_eq!(ev.p_and_l_gain_or_loss(), None);
let mut ev = control_gained_sample();
ev.previously_held_interest_fair_value = None;
assert_eq!(ev.p_and_l_gain_or_loss(), None);
}
#[test]
fn ownership_change_event_round_trips_via_serde() {
let ev = control_gained_sample();
let json = serde_json::to_string(&ev).unwrap();
let back: OwnershipChangeEvent = serde_json::from_str(&json).unwrap();
assert_eq!(back.entity_code, "SUB1");
assert_eq!(back.event_type, OwnershipChangeType::ControlGained);
assert_eq!(back.previously_held_interest_carrying, Some(dec!(100_000)));
assert_eq!(
back.previously_held_interest_fair_value,
Some(dec!(130_000))
);
assert_eq!(back.acquisition_date_nci_fair_value, Some(dec!(310_000)));
assert_eq!(
back.nci_measurement_method,
NciMeasurementMethod::FullGoodwill
);
}
}