Skip to main content

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}