datasynth_core/models/intercompany/group_structure.rs
1//! Group structure ownership models for consolidated financial reporting.
2//!
3//! This module provides models for capturing parent-subsidiary relationships,
4//! ownership percentages, and consolidation methods. It feeds into ISA 600
5//! (component auditor scope), consolidated financial statements, and NCI
6//! (non-controlling interest) calculations.
7
8use chrono::NaiveDate;
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12/// Complete ownership/consolidation structure for a corporate group.
13///
14/// Captures the parent entity and all subsidiaries and associates, with their
15/// respective ownership percentages and consolidation methods.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct GroupStructure {
18 /// Code of the ultimate parent entity in the group.
19 pub parent_entity: String,
20 /// Subsidiary relationships (>50% owned or otherwise controlled entities).
21 pub subsidiaries: Vec<SubsidiaryRelationship>,
22 /// Associate relationships (20–50% owned entities, significant influence).
23 pub associates: Vec<AssociateRelationship>,
24}
25
26impl GroupStructure {
27 /// Create a new group structure with the given parent entity.
28 pub fn new(parent_entity: String) -> Self {
29 Self {
30 parent_entity,
31 subsidiaries: Vec::new(),
32 associates: Vec::new(),
33 }
34 }
35
36 /// Add a subsidiary relationship.
37 pub fn add_subsidiary(&mut self, subsidiary: SubsidiaryRelationship) {
38 self.subsidiaries.push(subsidiary);
39 }
40
41 /// Add an associate relationship.
42 pub fn add_associate(&mut self, associate: AssociateRelationship) {
43 self.associates.push(associate);
44 }
45
46 /// Return the total number of entities in the group (parent + subs + associates).
47 pub fn entity_count(&self) -> usize {
48 1 + self.subsidiaries.len() + self.associates.len()
49 }
50}
51
52/// Relationship between the group parent and a subsidiary entity.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct SubsidiaryRelationship {
55 /// Entity code of the subsidiary.
56 pub entity_code: String,
57 /// Percentage of shares held by the parent (0–100).
58 pub ownership_percentage: Decimal,
59 /// Percentage of voting rights held by the parent (0–100).
60 pub voting_rights_percentage: Decimal,
61 /// Accounting consolidation method applied to this subsidiary.
62 pub consolidation_method: GroupConsolidationMethod,
63 /// Date the parent acquired control of this subsidiary.
64 pub acquisition_date: Option<NaiveDate>,
65 /// Non-controlling interest percentage (= 100 − ownership_percentage).
66 pub nci_percentage: Decimal,
67 /// Functional currency code of the subsidiary (e.g. "USD", "EUR").
68 pub functional_currency: String,
69}
70
71impl SubsidiaryRelationship {
72 /// Create a fully-owned (100 %) subsidiary with full consolidation.
73 pub fn new_full(entity_code: String, functional_currency: String) -> Self {
74 Self {
75 entity_code,
76 ownership_percentage: Decimal::from(100),
77 voting_rights_percentage: Decimal::from(100),
78 consolidation_method: GroupConsolidationMethod::FullConsolidation,
79 acquisition_date: None,
80 nci_percentage: Decimal::ZERO,
81 functional_currency,
82 }
83 }
84
85 /// Create a subsidiary with a specified ownership percentage.
86 ///
87 /// The consolidation method and NCI are derived automatically from the
88 /// ownership percentage using IFRS 10 / IAS 28 thresholds.
89 pub fn new_with_ownership(
90 entity_code: String,
91 ownership_percentage: Decimal,
92 functional_currency: String,
93 acquisition_date: Option<NaiveDate>,
94 ) -> Self {
95 let consolidation_method = GroupConsolidationMethod::from_ownership(ownership_percentage);
96 let nci_percentage = Decimal::from(100) - ownership_percentage;
97 Self {
98 entity_code,
99 ownership_percentage,
100 voting_rights_percentage: ownership_percentage,
101 consolidation_method,
102 acquisition_date,
103 nci_percentage,
104 functional_currency,
105 }
106 }
107}
108
109/// Consolidation method applied to a subsidiary or investee.
110///
111/// Distinct from the existing [`super::ConsolidationMethod`] in that it uses
112/// IFRS-aligned terminology and adds a `FairValue` option for FVTPL investments.
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum GroupConsolidationMethod {
116 /// Full line-by-line consolidation (IFRS 10, >50 % ownership / control).
117 FullConsolidation,
118 /// Equity method (IAS 28, 20–50 % ownership, significant influence).
119 EquityMethod,
120 /// Fair value through profit or loss (<20 % ownership, no influence).
121 FairValue,
122}
123
124impl GroupConsolidationMethod {
125 /// Derive the consolidation method from the ownership percentage.
126 ///
127 /// Uses standard IFRS 10 / IAS 28 thresholds:
128 /// - > 50 % → FullConsolidation
129 /// - 20–50 % → EquityMethod
130 /// - < 20 % → FairValue
131 pub fn from_ownership(ownership_pct: Decimal) -> Self {
132 if ownership_pct > Decimal::from(50) {
133 Self::FullConsolidation
134 } else if ownership_pct >= Decimal::from(20) {
135 Self::EquityMethod
136 } else {
137 Self::FairValue
138 }
139 }
140}
141
142/// Relationship between the group parent and an associate entity.
143///
144/// Associates are accounted for under the equity method (IAS 28).
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct AssociateRelationship {
147 /// Entity code of the associate.
148 pub entity_code: String,
149 /// Percentage of shares held by the investor (typically 20–50 %).
150 pub ownership_percentage: Decimal,
151 /// Share of the associate's profit/(loss) recognised in the period.
152 pub equity_pickup: Decimal,
153}
154
155impl AssociateRelationship {
156 /// Create a new associate relationship with zero equity pickup.
157 pub fn new(entity_code: String, ownership_percentage: Decimal) -> Self {
158 Self {
159 entity_code,
160 ownership_percentage,
161 equity_pickup: Decimal::ZERO,
162 }
163 }
164}
165
166// ---------------------------------------------------------------------------
167// NCI Measurement
168// ---------------------------------------------------------------------------
169
170/// Non-controlling interest measurement for a subsidiary.
171///
172/// Captures the NCI share of net assets and current-period profit/loss,
173/// computed from the subsidiary's `nci_percentage` in [`SubsidiaryRelationship`].
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct NciMeasurement {
176 /// Entity code of the subsidiary carrying an NCI.
177 pub entity_code: String,
178 /// NCI percentage (= 100 − parent ownership percentage).
179 #[serde(with = "rust_decimal::serde::str")]
180 pub nci_percentage: Decimal,
181 /// NCI share of the subsidiary's net assets at period-end.
182 #[serde(with = "rust_decimal::serde::str")]
183 pub nci_share_net_assets: Decimal,
184 /// NCI share of the subsidiary's net income/(loss) for the period.
185 #[serde(with = "rust_decimal::serde::str")]
186 pub nci_share_profit: Decimal,
187 /// Total NCI recognised in the consolidated balance sheet
188 /// (opening NCI + share of profit − NCI dividends).
189 #[serde(with = "rust_decimal::serde::str")]
190 pub total_nci: Decimal,
191}
192
193impl NciMeasurement {
194 /// Compute NCI measurement from subsidiary inputs.
195 ///
196 /// # Arguments
197 /// * `entity_code` — entity code of the subsidiary.
198 /// * `nci_percentage` — NCI percentage (0–100).
199 /// * `net_assets` — subsidiary net assets at period-end (before NCI split).
200 /// * `net_income` — subsidiary net income/(loss) for the period.
201 pub fn compute(
202 entity_code: String,
203 nci_percentage: Decimal,
204 net_assets: Decimal,
205 net_income: Decimal,
206 ) -> Self {
207 let hundred = Decimal::from(100);
208 let nci_pct_fraction = nci_percentage / hundred;
209 let nci_share_net_assets = net_assets * nci_pct_fraction;
210 let nci_share_profit = net_income * nci_pct_fraction;
211 // Simplified total NCI = share of net assets (already includes accumulated earnings).
212 let total_nci = nci_share_net_assets;
213
214 Self {
215 entity_code,
216 nci_percentage,
217 nci_share_net_assets,
218 nci_share_profit,
219 total_nci,
220 }
221 }
222}
223
224#[cfg(test)]
225#[allow(clippy::unwrap_used)]
226mod tests {
227 use super::*;
228 use rust_decimal_macros::dec;
229
230 #[test]
231 fn test_group_consolidation_method_from_ownership() {
232 assert_eq!(
233 GroupConsolidationMethod::from_ownership(dec!(100)),
234 GroupConsolidationMethod::FullConsolidation
235 );
236 assert_eq!(
237 GroupConsolidationMethod::from_ownership(dec!(51)),
238 GroupConsolidationMethod::FullConsolidation
239 );
240 assert_eq!(
241 GroupConsolidationMethod::from_ownership(dec!(50)),
242 GroupConsolidationMethod::EquityMethod
243 );
244 assert_eq!(
245 GroupConsolidationMethod::from_ownership(dec!(20)),
246 GroupConsolidationMethod::EquityMethod
247 );
248 assert_eq!(
249 GroupConsolidationMethod::from_ownership(dec!(19)),
250 GroupConsolidationMethod::FairValue
251 );
252 assert_eq!(
253 GroupConsolidationMethod::from_ownership(dec!(0)),
254 GroupConsolidationMethod::FairValue
255 );
256 }
257}