1use std::collections::HashMap;
8
9use rust_decimal::Decimal;
10
11use datasynth_core::models::intercompany::IntercompanyRelationship;
12use datasynth_core::models::Company;
13
14use crate::models::{CompanyNode, EdgeType, Graph, GraphEdge, GraphType, NodeId, OwnershipEdge};
15
16#[derive(Debug, Clone)]
18pub struct EntityGraphConfig {
19 pub include_intercompany_edges: bool,
21 pub compute_consolidation_weights: bool,
23 pub min_ownership_percent: Decimal,
25 pub include_indirect_ownership: bool,
27}
28
29impl Default for EntityGraphConfig {
30 fn default() -> Self {
31 Self {
32 include_intercompany_edges: true,
33 compute_consolidation_weights: true,
34 min_ownership_percent: Decimal::ZERO,
35 include_indirect_ownership: true,
36 }
37 }
38}
39
40pub struct EntityGraphBuilder {
42 config: EntityGraphConfig,
43 graph: Graph,
44 company_nodes: HashMap<String, NodeId>,
46 ownership_edges: Vec<(String, String, Decimal)>,
48}
49
50impl EntityGraphBuilder {
51 pub fn new(config: EntityGraphConfig) -> Self {
53 Self {
54 config,
55 graph: Graph::new("entity_network", GraphType::EntityRelationship),
56 company_nodes: HashMap::new(),
57 ownership_edges: Vec::new(),
58 }
59 }
60
61 pub fn add_companies(&mut self, companies: &[Company]) {
63 for company in companies {
64 self.get_or_create_company_node(company);
65 }
66 }
67
68 pub fn add_ownership_relationships(&mut self, relationships: &[IntercompanyRelationship]) {
70 for rel in relationships {
71 if rel.ownership_percentage < self.config.min_ownership_percent {
72 continue;
73 }
74
75 let parent_id = self.ensure_company_node(&rel.parent_company, &rel.parent_company);
76 let subsidiary_id =
77 self.ensure_company_node(&rel.subsidiary_company, &rel.subsidiary_company);
78
79 self.ownership_edges.push((
81 rel.parent_company.clone(),
82 rel.subsidiary_company.clone(),
83 rel.ownership_percentage,
84 ));
85
86 let mut edge = OwnershipEdge::new(
88 0,
89 parent_id,
90 subsidiary_id,
91 rel.ownership_percentage,
92 rel.effective_date,
93 );
94 edge.parent_code = rel.parent_company.clone();
95 edge.subsidiary_code = rel.subsidiary_company.clone();
96 edge.consolidation_method = rel.consolidation_method.as_str().to_string();
97 edge.compute_features();
98
99 self.graph.add_edge(edge.edge);
100 }
101 }
102
103 pub fn add_intercompany_edge(
105 &mut self,
106 from_company: &str,
107 to_company: &str,
108 amount: Decimal,
109 transaction_type: &str,
110 ) {
111 if !self.config.include_intercompany_edges {
112 return;
113 }
114
115 let from_id = self.ensure_company_node(from_company, from_company);
116 let to_id = self.ensure_company_node(to_company, to_company);
117
118 let amount_f64: f64 = amount.try_into().unwrap_or(0.0);
119 let edge = GraphEdge::new(0, from_id, to_id, EdgeType::Intercompany)
120 .with_weight(amount_f64)
121 .with_feature((amount_f64.abs() + 1.0).ln())
122 .with_feature(Self::encode_transaction_type(transaction_type));
123
124 self.graph.add_edge(edge);
125 }
126
127 fn get_or_create_company_node(&mut self, company: &Company) -> NodeId {
129 if let Some(&id) = self.company_nodes.get(&company.company_code) {
130 return id;
131 }
132
133 let mut company_node = CompanyNode::new(
134 0,
135 company.company_code.clone(),
136 company.company_name.clone(),
137 );
138 company_node.country = company.country.clone();
139 company_node.currency = company.local_currency.clone();
140 company_node.is_parent = company.is_parent;
141 company_node.parent_code = company.parent_company.clone();
142 company_node.ownership_percent = company.ownership_percentage;
143 company_node.compute_features();
144
145 let id = self.graph.add_node(company_node.node);
146 self.company_nodes.insert(company.company_code.clone(), id);
147 id
148 }
149
150 fn ensure_company_node(&mut self, company_code: &str, company_name: &str) -> NodeId {
152 if let Some(&id) = self.company_nodes.get(company_code) {
153 return id;
154 }
155
156 let mut company_node =
157 CompanyNode::new(0, company_code.to_string(), company_name.to_string());
158 company_node.compute_features();
159
160 let id = self.graph.add_node(company_node.node);
161 self.company_nodes.insert(company_code.to_string(), id);
162 id
163 }
164
165 fn encode_transaction_type(transaction_type: &str) -> f64 {
167 match transaction_type {
168 "GoodsSale" => 1.0,
169 "ServiceProvided" => 2.0,
170 "Loan" => 3.0,
171 "Dividend" => 4.0,
172 "ManagementFee" => 5.0,
173 "Royalty" => 6.0,
174 "CostSharing" => 7.0,
175 _ => 0.0,
176 }
177 }
178
179 fn compute_indirect_ownership(&self) -> HashMap<(String, String), Decimal> {
181 let mut indirect: HashMap<(String, String), Decimal> = HashMap::new();
182
183 for (parent, subsidiary, pct) in &self.ownership_edges {
185 indirect.insert((parent.clone(), subsidiary.clone()), *pct);
186 }
187
188 let mut changed = true;
190 let max_iterations = 10;
191 let mut iteration = 0;
192
193 while changed && iteration < max_iterations {
194 changed = false;
195 iteration += 1;
196
197 let current_indirect: Vec<_> = indirect.iter().map(|(k, v)| (k.clone(), *v)).collect();
198
199 for ((parent, subsidiary), pct) in ¤t_indirect {
200 for (child_parent, child_sub, child_pct) in &self.ownership_edges {
202 if child_parent == subsidiary {
203 let key = (parent.clone(), child_sub.clone());
204 let indirect_pct = *pct * *child_pct / Decimal::ONE_HUNDRED;
205
206 if let Some(existing) = indirect.get(&key) {
207 if indirect_pct > *existing {
208 indirect.insert(key, indirect_pct);
209 changed = true;
210 }
211 } else {
212 indirect.insert(key, indirect_pct);
213 changed = true;
214 }
215 }
216 }
217 }
218 }
219
220 indirect
221 }
222
223 pub fn add_indirect_ownership_edges(&mut self) {
225 if !self.config.include_indirect_ownership {
226 return;
227 }
228
229 let indirect = self.compute_indirect_ownership();
230
231 let direct: std::collections::HashSet<_> = self
233 .ownership_edges
234 .iter()
235 .map(|(p, s, _)| (p.clone(), s.clone()))
236 .collect();
237
238 for ((parent, subsidiary), pct) in indirect {
240 if direct.contains(&(parent.clone(), subsidiary.clone())) {
241 continue; }
243
244 if pct < self.config.min_ownership_percent {
245 continue;
246 }
247
248 if let (Some(&parent_id), Some(&sub_id)) = (
249 self.company_nodes.get(&parent),
250 self.company_nodes.get(&subsidiary),
251 ) {
252 let pct_f64: f64 = pct.try_into().unwrap_or(0.0);
253 let edge = GraphEdge::new(0, parent_id, sub_id, EdgeType::Ownership)
254 .with_weight(pct_f64)
255 .with_feature(pct_f64 / 100.0)
256 .with_feature(1.0); self.graph.add_edge(edge);
259 }
260 }
261 }
262
263 pub fn build(mut self) -> Graph {
265 if self.config.include_indirect_ownership {
267 self.add_indirect_ownership_edges();
268 }
269
270 self.graph.compute_statistics();
271 self.graph
272 }
273
274 pub fn company_node_map(&self) -> &HashMap<String, NodeId> {
276 &self.company_nodes
277 }
278}
279
280#[derive(Debug, Clone)]
282pub struct OwnershipHierarchy {
283 pub root: String,
285 pub children: Vec<OwnershipHierarchyNode>,
287}
288
289#[derive(Debug, Clone)]
291pub struct OwnershipHierarchyNode {
292 pub company_code: String,
294 pub direct_ownership: Decimal,
296 pub effective_ownership: Decimal,
298 pub children: Vec<OwnershipHierarchyNode>,
300}
301
302impl OwnershipHierarchy {
303 pub fn from_relationships(root: &str, relationships: &[IntercompanyRelationship]) -> Self {
305 let children = Self::build_children(root, Decimal::ONE_HUNDRED, relationships);
306 Self {
307 root: root.to_string(),
308 children,
309 }
310 }
311
312 fn build_children(
313 parent: &str,
314 parent_effective: Decimal,
315 relationships: &[IntercompanyRelationship],
316 ) -> Vec<OwnershipHierarchyNode> {
317 let mut children = Vec::new();
318
319 for rel in relationships {
320 if rel.parent_company == parent {
321 let effective = parent_effective * rel.ownership_percentage / Decimal::ONE_HUNDRED;
322 let grandchildren =
323 Self::build_children(&rel.subsidiary_company, effective, relationships);
324
325 children.push(OwnershipHierarchyNode {
326 company_code: rel.subsidiary_company.clone(),
327 direct_ownership: rel.ownership_percentage,
328 effective_ownership: effective,
329 children: grandchildren,
330 });
331 }
332 }
333
334 children
335 }
336
337 pub fn all_companies(&self) -> Vec<(String, Decimal)> {
339 let mut result = vec![(self.root.clone(), Decimal::ONE_HUNDRED)];
340 Self::collect_companies(&self.children, &mut result);
341 result
342 }
343
344 fn collect_companies(nodes: &[OwnershipHierarchyNode], result: &mut Vec<(String, Decimal)>) {
345 for node in nodes {
346 result.push((node.company_code.clone(), node.effective_ownership));
347 Self::collect_companies(&node.children, result);
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use chrono::NaiveDate;
356 use datasynth_core::models::intercompany::ConsolidationMethod;
357 use rust_decimal_macros::dec;
358
359 fn create_test_relationship(
360 parent: &str,
361 subsidiary: &str,
362 pct: Decimal,
363 ) -> IntercompanyRelationship {
364 IntercompanyRelationship {
365 relationship_id: format!("REL-{}-{}", parent, subsidiary),
366 parent_company: parent.to_string(),
367 subsidiary_company: subsidiary.to_string(),
368 ownership_percentage: pct,
369 consolidation_method: ConsolidationMethod::Full,
370 effective_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
371 end_date: None,
372 transfer_pricing_policy: None,
373 holding_type: datasynth_core::models::intercompany::HoldingType::Direct,
374 functional_currency: "USD".to_string(),
375 requires_elimination: true,
376 reporting_segment: None,
377 }
378 }
379
380 #[test]
381 fn test_entity_graph() {
382 let mut builder = EntityGraphBuilder::new(EntityGraphConfig::default());
383
384 let relationships = vec![
385 create_test_relationship("1000", "1100", dec!(100)),
386 create_test_relationship("1000", "1200", dec!(100)),
387 create_test_relationship("1100", "1110", dec!(80)),
388 ];
389
390 builder.add_ownership_relationships(&relationships);
391
392 let graph = builder.build();
393
394 assert_eq!(graph.node_count(), 4); assert!(graph.edge_count() >= 3); }
397
398 #[test]
399 fn test_ownership_hierarchy() {
400 let relationships = vec![
401 create_test_relationship("HQ", "US", dec!(100)),
402 create_test_relationship("HQ", "EU", dec!(100)),
403 create_test_relationship("US", "US-WEST", dec!(100)),
404 create_test_relationship("EU", "DE", dec!(80)),
405 ];
406
407 let hierarchy = OwnershipHierarchy::from_relationships("HQ", &relationships);
408
409 assert_eq!(hierarchy.root, "HQ");
410 assert_eq!(hierarchy.children.len(), 2);
411
412 let all = hierarchy.all_companies();
413 assert_eq!(all.len(), 5);
414
415 let de = all.iter().find(|(c, _)| c == "DE").unwrap();
417 assert_eq!(de.1, dec!(80));
418 }
419
420 #[test]
421 fn test_indirect_ownership() {
422 let config = EntityGraphConfig {
423 include_indirect_ownership: true,
424 ..Default::default()
425 };
426 let mut builder = EntityGraphBuilder::new(config);
427
428 let relationships = vec![
429 create_test_relationship("A", "B", dec!(100)),
430 create_test_relationship("B", "C", dec!(50)),
431 ];
432
433 builder.add_ownership_relationships(&relationships);
434 let graph = builder.build();
435
436 assert_eq!(graph.node_count(), 3);
438 }
439}