1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//! Business Combination models (IFRS 3 / ASC 805).
//!
//! Provides data structures for:
//! - Acquisition consideration (cash, shares, contingent)
//! - Purchase price allocation (PPA) with fair value adjustments
//! - Goodwill computation
//! - Day 1 journal entries and subsequent amortization of acquired intangibles
use crate::models::intercompany::NciMeasurementMethod;
use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
/// A business combination transaction representing an acquisition accounted for
/// under IFRS 3 (acquisition method) or ASC 805.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BusinessCombination {
/// Unique identifier for this acquisition
pub id: String,
/// Company code of the acquirer entity
pub acquirer_entity: String,
/// Name of the acquiree (target company)
pub acquiree_name: String,
/// Entity code of the acquiree (the subsidiary in the group's
/// consolidation graph after the acquisition). When set, the
/// acquisition can be matched to the corresponding `ManifestEntity`
/// in the v5.x group manifest so the NCI rollforward can pick up
/// the IFRS 3.19 measurement choice automatically. Optional —
/// `None` means the acquisition has no consolidated counterpart
/// (asset purchase, fully owned, etc.).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub acquiree_entity_code: Option<String>,
/// Date control was obtained (acquisition date)
pub acquisition_date: NaiveDate,
/// Total consideration paid or transferred
pub consideration: AcquisitionConsideration,
/// Purchase price allocation at acquisition date
pub purchase_price_allocation: AcquisitionPpa,
/// Goodwill recognised (consideration minus net identifiable assets at FV).
/// Zero when consideration < net identifiable assets (bargain purchase).
#[serde(with = "crate::serde_decimal")]
pub goodwill: Decimal,
/// IFRS 3 § 19 / ASC 805-30-30-1 acquisition-date NCI measurement
/// method. Defaults to `Proportionate` (matches the v5.0–v5.1
/// behaviour where every acquisition was effectively measured on
/// the proportionate basis). US GAAP requires `FullGoodwill`;
/// IFRS allows either on a per-acquisition basis.
#[serde(default)]
pub nci_measurement_method: NciMeasurementMethod,
/// IFRS 3 § 19(a) acquisition-date NCI fair value. Required when
/// `nci_measurement_method == FullGoodwill`; optional otherwise
/// (carried for disclosure purposes under either method). When
/// supplied this becomes the period-1 opening NCI for the
/// acquired subsidiary in the group consolidation rollforward.
#[serde(default, with = "crate::serde_decimal::option")]
pub acquisition_date_nci_fair_value: Option<Decimal>,
/// Accounting framework applied: "IFRS" or "US_GAAP"
pub framework: String,
}
impl BusinessCombination {
/// Resolve the acquisition-date NCI fair value to feed into the
/// group consolidation rollforward. Returns `Some(fv)` when the
/// caller has supplied one; `None` otherwise — in which case the
/// rollforward falls back to the v5.0–v5.1 proportionate-basis
/// behaviour (opening NCI = 0, grows via share of profit).
///
/// Use from `run_aggregate` when wiring per-acquisition NCI
/// measurement choices into the period-1 opening of the
/// `compute_nci_rollforward` call.
pub fn nci_opening_fair_value(&self) -> Option<Decimal> {
self.acquisition_date_nci_fair_value
}
}
/// Consideration transferred in a business combination.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcquisitionConsideration {
/// Cash and cash equivalents paid
#[serde(with = "crate::serde_decimal")]
pub cash: Decimal,
/// Fair value of equity instruments issued by the acquirer
#[serde(default, with = "crate::serde_decimal::option")]
pub shares_issued_value: Option<Decimal>,
/// Fair value of contingent consideration (earn-out) at acquisition date
#[serde(default, with = "crate::serde_decimal::option")]
pub contingent_consideration: Option<Decimal>,
/// Total consideration (sum of cash + shares + contingent)
#[serde(with = "crate::serde_decimal")]
pub total: Decimal,
}
/// Purchase price allocation mapping the consideration to identifiable
/// net assets at fair value, with the residual as goodwill.
///
/// Named `AcquisitionPpa` (not `PurchasePriceAllocation`) to avoid
/// a name collision with the same-named struct in `organizational_event`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcquisitionPpa {
/// Identifiable assets acquired at fair value
pub identifiable_assets: Vec<AcquisitionFvAdjustment>,
/// Identifiable liabilities assumed at fair value
pub identifiable_liabilities: Vec<AcquisitionFvAdjustment>,
/// Net identifiable assets at fair value
/// = sum(asset FVs) - sum(liability FVs)
#[serde(with = "crate::serde_decimal")]
pub net_identifiable_assets_fv: Decimal,
}
/// A single asset or liability line within the purchase price allocation,
/// showing book value, fair value step-up, and useful life for intangibles.
///
/// Named `AcquisitionFvAdjustment` to avoid a name collision with
/// `FairValueAdjustment` in `organizational_event`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcquisitionFvAdjustment {
/// Description of the asset or liability (e.g. "Customer Relationships")
pub asset_or_liability: String,
/// Carrying amount in the acquiree's books at acquisition date
#[serde(with = "crate::serde_decimal")]
pub book_value: Decimal,
/// Fair value assigned in the PPA
#[serde(with = "crate::serde_decimal")]
pub fair_value: Decimal,
/// Step-up amount (fair_value - book_value; may be negative for liabilities)
#[serde(with = "crate::serde_decimal")]
pub step_up: Decimal,
/// Useful life in years for finite-lived intangibles; None for PP&E and indefinite-lived assets
#[serde(default)]
pub useful_life_years: Option<u32>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn sample_bc() -> BusinessCombination {
BusinessCombination {
id: "BC-001".to_string(),
acquirer_entity: "PARENT".to_string(),
acquiree_name: "Acme Sub".to_string(),
acquiree_entity_code: Some("SUB1".to_string()),
acquisition_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
consideration: AcquisitionConsideration {
cash: dec!(1_000_000),
shares_issued_value: None,
contingent_consideration: None,
total: dec!(1_000_000),
},
purchase_price_allocation: AcquisitionPpa {
identifiable_assets: Vec::new(),
identifiable_liabilities: Vec::new(),
net_identifiable_assets_fv: dec!(800_000),
},
goodwill: dec!(200_000),
nci_measurement_method: NciMeasurementMethod::Proportionate,
acquisition_date_nci_fair_value: None,
framework: "IFRS".to_string(),
}
}
#[test]
fn nci_opening_fair_value_returns_none_under_proportionate() {
let bc = sample_bc();
assert!(bc.nci_opening_fair_value().is_none());
}
#[test]
fn nci_opening_fair_value_returns_supplied_amount() {
let mut bc = sample_bc();
bc.nci_measurement_method = NciMeasurementMethod::FullGoodwill;
bc.acquisition_date_nci_fair_value = Some(dec!(280_000));
assert_eq!(bc.nci_opening_fair_value(), Some(dec!(280_000)));
}
#[test]
fn business_combination_round_trips_with_new_fields() {
let mut bc = sample_bc();
bc.nci_measurement_method = NciMeasurementMethod::FullGoodwill;
bc.acquisition_date_nci_fair_value = Some(dec!(280_000));
let json = serde_json::to_string(&bc).unwrap();
let back: BusinessCombination = serde_json::from_str(&json).unwrap();
assert_eq!(back.acquiree_entity_code.as_deref(), Some("SUB1"));
assert_eq!(
back.nci_measurement_method,
NciMeasurementMethod::FullGoodwill
);
assert_eq!(back.acquisition_date_nci_fair_value, Some(dec!(280_000)));
}
#[test]
fn legacy_archives_default_to_proportionate_and_no_fair_value() {
// v5.0 / v5.1 archives lack the three new fields entirely.
// `#[serde(default)]` must let them deserialise cleanly to
// the Proportionate default + None fair value + no acquiree
// entity code.
let legacy_json = r#"{
"id": "BC-LEGACY",
"acquirer_entity": "PARENT",
"acquiree_name": "Old Sub",
"acquisition_date": "2024-01-01",
"consideration": {
"cash": "1000000",
"shares_issued_value": null,
"contingent_consideration": null,
"total": "1000000"
},
"purchase_price_allocation": {
"identifiable_assets": [],
"identifiable_liabilities": [],
"net_identifiable_assets_fv": "800000"
},
"goodwill": "200000",
"framework": "IFRS"
}"#;
let bc: BusinessCombination = serde_json::from_str(legacy_json).unwrap();
assert!(bc.acquiree_entity_code.is_none());
assert_eq!(
bc.nci_measurement_method,
NciMeasurementMethod::Proportionate
);
assert!(bc.acquisition_date_nci_fair_value.is_none());
}
}