1use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct IntercompanyRelationship {
11 pub relationship_id: String,
13 pub parent_company: String,
15 pub subsidiary_company: String,
17 pub ownership_percentage: Decimal,
19 pub consolidation_method: ConsolidationMethod,
21 pub transfer_pricing_policy: Option<String>,
23 pub effective_date: NaiveDate,
25 pub end_date: Option<NaiveDate>,
27 pub holding_type: HoldingType,
29 pub functional_currency: String,
31 pub requires_elimination: bool,
33 pub reporting_segment: Option<String>,
35}
36
37impl IntercompanyRelationship {
38 pub fn new(
40 relationship_id: String,
41 parent_company: String,
42 subsidiary_company: String,
43 ownership_percentage: Decimal,
44 effective_date: NaiveDate,
45 ) -> Self {
46 let consolidation_method = ConsolidationMethod::from_ownership(ownership_percentage);
47 let requires_elimination = consolidation_method != ConsolidationMethod::Equity;
48
49 Self {
50 relationship_id,
51 parent_company,
52 subsidiary_company,
53 ownership_percentage,
54 consolidation_method,
55 transfer_pricing_policy: None,
56 effective_date,
57 end_date: None,
58 holding_type: HoldingType::Direct,
59 functional_currency: "USD".to_string(),
60 requires_elimination,
61 reporting_segment: None,
62 }
63 }
64
65 pub fn is_active_on(&self, date: NaiveDate) -> bool {
67 date >= self.effective_date && self.end_date.map_or(true, |end| date <= end)
68 }
69
70 pub fn is_controlling(&self) -> bool {
72 self.ownership_percentage > Decimal::from(50)
73 }
74
75 pub fn has_significant_influence(&self) -> bool {
77 self.ownership_percentage >= Decimal::from(20)
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
83#[serde(rename_all = "snake_case")]
84pub enum ConsolidationMethod {
85 #[default]
87 Full,
88 Proportional,
90 Equity,
92 Cost,
94}
95
96impl ConsolidationMethod {
97 pub fn from_ownership(ownership_pct: Decimal) -> Self {
99 if ownership_pct > Decimal::from(50) {
100 Self::Full
101 } else if ownership_pct >= Decimal::from(20) {
102 Self::Equity
103 } else {
104 Self::Cost
105 }
106 }
107
108 pub fn requires_full_elimination(&self) -> bool {
110 matches!(self, Self::Full)
111 }
112
113 pub fn requires_proportional_elimination(&self) -> bool {
115 matches!(self, Self::Proportional)
116 }
117
118 pub fn as_str(&self) -> &'static str {
120 match self {
121 Self::Full => "Full",
122 Self::Proportional => "Proportional",
123 Self::Equity => "Equity",
124 Self::Cost => "Cost",
125 }
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
131#[serde(rename_all = "snake_case")]
132pub enum HoldingType {
133 #[default]
135 Direct,
136 Indirect,
138 Reciprocal,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct OwnershipStructure {
145 pub ultimate_parent: String,
147 pub relationships: Vec<IntercompanyRelationship>,
149 effective_ownership: HashMap<String, Decimal>,
151 subsidiaries_by_parent: HashMap<String, Vec<String>>,
153}
154
155impl OwnershipStructure {
156 pub fn new(ultimate_parent: String) -> Self {
158 Self {
159 ultimate_parent,
160 relationships: Vec::new(),
161 effective_ownership: HashMap::new(),
162 subsidiaries_by_parent: HashMap::new(),
163 }
164 }
165
166 pub fn add_relationship(&mut self, relationship: IntercompanyRelationship) {
168 self.subsidiaries_by_parent
170 .entry(relationship.parent_company.clone())
171 .or_default()
172 .push(relationship.subsidiary_company.clone());
173
174 self.relationships.push(relationship);
175
176 self.calculate_effective_ownership();
178 }
179
180 pub fn get_relationships_for_parent(&self, parent: &str) -> Vec<&IntercompanyRelationship> {
182 self.relationships
183 .iter()
184 .filter(|r| r.parent_company == parent)
185 .collect()
186 }
187
188 pub fn get_relationships_for_subsidiary(
190 &self,
191 subsidiary: &str,
192 ) -> Vec<&IntercompanyRelationship> {
193 self.relationships
194 .iter()
195 .filter(|r| r.subsidiary_company == subsidiary)
196 .collect()
197 }
198
199 pub fn get_direct_parent(&self, company: &str) -> Option<&str> {
201 self.relationships
202 .iter()
203 .find(|r| r.subsidiary_company == company && r.holding_type == HoldingType::Direct)
204 .map(|r| r.parent_company.as_str())
205 }
206
207 pub fn get_direct_subsidiaries(&self, parent: &str) -> Vec<&str> {
209 self.subsidiaries_by_parent
210 .get(parent)
211 .map(|subs| subs.iter().map(|s| s.as_str()).collect())
212 .unwrap_or_default()
213 }
214
215 pub fn get_all_companies(&self) -> Vec<&str> {
217 let mut companies: Vec<&str> = vec![self.ultimate_parent.as_str()];
218 for rel in &self.relationships {
219 if !companies.contains(&rel.subsidiary_company.as_str()) {
220 companies.push(rel.subsidiary_company.as_str());
221 }
222 }
223 companies
224 }
225
226 pub fn get_effective_ownership(&self, company: &str) -> Decimal {
228 if company == self.ultimate_parent {
229 Decimal::from(100)
230 } else {
231 self.effective_ownership
232 .get(company)
233 .copied()
234 .unwrap_or(Decimal::ZERO)
235 }
236 }
237
238 pub fn are_related(&self, company1: &str, company2: &str) -> bool {
240 if company1 == company2 {
241 return true;
242 }
243 let has1 =
245 company1 == self.ultimate_parent || self.effective_ownership.contains_key(company1);
246 let has2 =
247 company2 == self.ultimate_parent || self.effective_ownership.contains_key(company2);
248 has1 && has2
249 }
250
251 pub fn get_consolidation_method(&self, company: &str) -> Option<ConsolidationMethod> {
253 self.relationships
254 .iter()
255 .find(|r| r.subsidiary_company == company)
256 .map(|r| r.consolidation_method)
257 }
258
259 fn calculate_effective_ownership(&mut self) {
261 self.effective_ownership.clear();
262
263 let mut to_process: Vec<(String, Decimal)> = self
265 .get_direct_subsidiaries(&self.ultimate_parent)
266 .iter()
267 .filter_map(|sub| {
268 self.relationships
269 .iter()
270 .find(|r| {
271 r.parent_company == self.ultimate_parent && r.subsidiary_company == *sub
272 })
273 .map(|r| (sub.to_string(), r.ownership_percentage))
274 })
275 .collect();
276
277 while let Some((company, effective_pct)) = to_process.pop() {
279 self.effective_ownership
280 .insert(company.clone(), effective_pct);
281
282 for sub in self.get_direct_subsidiaries(&company) {
284 if let Some(rel) = self
285 .relationships
286 .iter()
287 .find(|r| r.parent_company == company && r.subsidiary_company == sub)
288 {
289 let sub_effective =
290 effective_pct * rel.ownership_percentage / Decimal::from(100);
291 to_process.push((sub.to_string(), sub_effective));
292 }
293 }
294 }
295 }
296
297 pub fn get_active_relationships(&self, date: NaiveDate) -> Vec<&IntercompanyRelationship> {
299 self.relationships
300 .iter()
301 .filter(|r| r.is_active_on(date))
302 .collect()
303 }
304
305 pub fn get_fully_consolidated_companies(&self) -> Vec<&str> {
307 self.relationships
308 .iter()
309 .filter(|r| r.consolidation_method == ConsolidationMethod::Full)
310 .map(|r| r.subsidiary_company.as_str())
311 .collect()
312 }
313
314 pub fn get_equity_method_companies(&self) -> Vec<&str> {
316 self.relationships
317 .iter()
318 .filter(|r| r.consolidation_method == ConsolidationMethod::Equity)
319 .map(|r| r.subsidiary_company.as_str())
320 .collect()
321 }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct IntercompanyAccountMapping {
327 pub relationship_id: String,
329 pub ic_receivable_account: String,
331 pub ic_payable_account: String,
333 pub ic_revenue_account: String,
335 pub ic_expense_account: String,
337 pub ic_investment_account: Option<String>,
339 pub ic_equity_account: Option<String>,
341}
342
343impl IntercompanyAccountMapping {
344 pub fn new_standard(relationship_id: String, company_code: &str) -> Self {
346 Self {
347 relationship_id,
348 ic_receivable_account: format!("1310{}", company_code),
349 ic_payable_account: format!("2110{}", company_code),
350 ic_revenue_account: format!("4100{}", company_code),
351 ic_expense_account: format!("5100{}", company_code),
352 ic_investment_account: Some(format!("1510{}", company_code)),
353 ic_equity_account: Some(format!("3100{}", company_code)),
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use rust_decimal_macros::dec;
362
363 #[test]
364 fn test_consolidation_method_from_ownership() {
365 assert_eq!(
366 ConsolidationMethod::from_ownership(dec!(100)),
367 ConsolidationMethod::Full
368 );
369 assert_eq!(
370 ConsolidationMethod::from_ownership(dec!(51)),
371 ConsolidationMethod::Full
372 );
373 assert_eq!(
374 ConsolidationMethod::from_ownership(dec!(50)),
375 ConsolidationMethod::Equity
376 );
377 assert_eq!(
378 ConsolidationMethod::from_ownership(dec!(20)),
379 ConsolidationMethod::Equity
380 );
381 assert_eq!(
382 ConsolidationMethod::from_ownership(dec!(19)),
383 ConsolidationMethod::Cost
384 );
385 }
386
387 #[test]
388 fn test_relationship_is_controlling() {
389 let rel = IntercompanyRelationship::new(
390 "REL001".to_string(),
391 "1000".to_string(),
392 "1100".to_string(),
393 dec!(100),
394 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
395 );
396 assert!(rel.is_controlling());
397
398 let rel2 = IntercompanyRelationship::new(
399 "REL002".to_string(),
400 "1000".to_string(),
401 "2000".to_string(),
402 dec!(30),
403 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
404 );
405 assert!(!rel2.is_controlling());
406 assert!(rel2.has_significant_influence());
407 }
408
409 #[test]
410 fn test_ownership_structure() {
411 let mut structure = OwnershipStructure::new("1000".to_string());
412
413 structure.add_relationship(IntercompanyRelationship::new(
414 "REL001".to_string(),
415 "1000".to_string(),
416 "1100".to_string(),
417 dec!(100),
418 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
419 ));
420
421 structure.add_relationship(IntercompanyRelationship::new(
422 "REL002".to_string(),
423 "1100".to_string(),
424 "1110".to_string(),
425 dec!(80),
426 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
427 ));
428
429 assert_eq!(structure.get_effective_ownership("1000"), dec!(100));
430 assert_eq!(structure.get_effective_ownership("1100"), dec!(100));
431 assert_eq!(structure.get_effective_ownership("1110"), dec!(80));
432
433 assert!(structure.are_related("1000", "1100"));
434 assert!(structure.are_related("1100", "1110"));
435
436 let subs = structure.get_direct_subsidiaries("1000");
437 assert_eq!(subs, vec!["1100"]);
438 }
439
440 #[test]
441 fn test_relationship_active_date() {
442 let mut rel = IntercompanyRelationship::new(
443 "REL001".to_string(),
444 "1000".to_string(),
445 "1100".to_string(),
446 dec!(100),
447 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
448 );
449 rel.end_date = Some(NaiveDate::from_ymd_opt(2023, 12, 31).unwrap());
450
451 assert!(rel.is_active_on(NaiveDate::from_ymd_opt(2022, 6, 15).unwrap()));
452 assert!(rel.is_active_on(NaiveDate::from_ymd_opt(2023, 12, 31).unwrap()));
453 assert!(!rel.is_active_on(NaiveDate::from_ymd_opt(2021, 12, 31).unwrap()));
454 assert!(!rel.is_active_on(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()));
455 }
456}