datasynth_banking/labels/
relationship_labels.rs1use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use crate::models::{BankingCustomer, BeneficialOwner, CustomerRelationship};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum RelationshipType {
11 Family,
13 Employment,
15 BusinessPartner,
17 Vendor,
19 Customer,
21 BeneficialOwnership,
23 TransactionCounterparty,
25 MuleLink,
27 ShellLink,
29 Unknown,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct RelationshipLabel {
36 pub source_id: Uuid,
38 pub target_id: Uuid,
40 pub relationship_type: RelationshipType,
42 pub is_mule_link: bool,
44 pub is_shell_link: bool,
46 pub ownership_percent: Option<f64>,
48 pub transaction_count: u32,
50 pub transaction_volume: f64,
52 pub strength: f64,
54 pub case_id: Option<String>,
56 pub confidence: f64,
58}
59
60impl RelationshipLabel {
61 pub fn new(source_id: Uuid, target_id: Uuid, relationship_type: RelationshipType) -> Self {
63 Self {
64 source_id,
65 target_id,
66 relationship_type,
67 is_mule_link: false,
68 is_shell_link: false,
69 ownership_percent: None,
70 transaction_count: 0,
71 transaction_volume: 0.0,
72 strength: 0.5,
73 case_id: None,
74 confidence: 1.0,
75 }
76 }
77
78 pub fn as_mule_link(mut self) -> Self {
80 self.is_mule_link = true;
81 self.relationship_type = RelationshipType::MuleLink;
82 self
83 }
84
85 pub fn as_shell_link(mut self) -> Self {
87 self.is_shell_link = true;
88 self.relationship_type = RelationshipType::ShellLink;
89 self
90 }
91
92 pub fn with_ownership(mut self, percent: f64) -> Self {
94 self.ownership_percent = Some(percent);
95 self.relationship_type = RelationshipType::BeneficialOwnership;
96 self
97 }
98
99 pub fn with_transactions(mut self, count: u32, volume: f64) -> Self {
101 self.transaction_count = count;
102 self.transaction_volume = volume;
103 self.strength = (count as f64 / 100.0).min(1.0);
105 self
106 }
107
108 pub fn with_case(mut self, case_id: &str) -> Self {
110 self.case_id = Some(case_id.to_string());
111 self
112 }
113}
114
115pub struct RelationshipLabelExtractor;
117
118impl RelationshipLabelExtractor {
119 pub fn extract_from_customers(customers: &[BankingCustomer]) -> Vec<RelationshipLabel> {
121 let mut labels = Vec::new();
122
123 for customer in customers {
124 for rel in &customer.relationships {
126 let label = Self::from_customer_relationship(customer.customer_id, rel);
127 labels.push(label);
128 }
129
130 for bo in &customer.beneficial_owners {
132 let label = Self::from_beneficial_owner(customer.customer_id, bo);
133 labels.push(label);
134 }
135 }
136
137 labels
138 }
139
140 fn from_customer_relationship(
142 customer_id: Uuid,
143 relationship: &CustomerRelationship,
144 ) -> RelationshipLabel {
145 use crate::models::RelationshipType as CustRelType;
146
147 let rel_type = match relationship.relationship_type {
148 CustRelType::Spouse
149 | CustRelType::ParentChild
150 | CustRelType::Sibling
151 | CustRelType::Family => RelationshipType::Family,
152 CustRelType::Employment => RelationshipType::Employment,
153 CustRelType::BusinessPartner => RelationshipType::BusinessPartner,
154 CustRelType::AuthorizedSigner | CustRelType::JointAccountHolder => {
155 RelationshipType::Family
156 }
157 CustRelType::Beneficiary | CustRelType::TrustRelationship => {
158 RelationshipType::BeneficialOwnership
159 }
160 CustRelType::Guarantor | CustRelType::Attorney => RelationshipType::Unknown,
161 };
162
163 RelationshipLabel::new(customer_id, relationship.related_customer_id, rel_type)
164 }
165
166 fn from_beneficial_owner(entity_id: Uuid, bo: &BeneficialOwner) -> RelationshipLabel {
168 let ownership_pct: f64 = bo.ownership_percentage.try_into().unwrap_or(0.0);
169 let mut label =
170 RelationshipLabel::new(bo.ubo_id, entity_id, RelationshipType::BeneficialOwnership)
171 .with_ownership(ownership_pct);
172
173 if bo.is_hidden || bo.intermediary_entity.is_some() {
175 label = label.as_shell_link();
176 }
177
178 label
179 }
180
181 pub fn extract_from_transactions(
183 transactions: &[crate::models::BankTransaction],
184 ) -> Vec<RelationshipLabel> {
185 use std::collections::HashMap;
186
187 let mut pairs: HashMap<(Uuid, String), (u32, f64, bool)> = HashMap::new();
189
190 for txn in transactions {
191 let key = (txn.account_id, txn.counterparty.name.clone());
192 let entry = pairs.entry(key).or_insert((0, 0.0, false));
193 entry.0 += 1;
194 entry.1 += txn.amount.try_into().unwrap_or(0.0);
195 if txn.is_suspicious {
196 entry.2 = true;
197 }
198 }
199
200 pairs
201 .into_iter()
202 .filter(|(_, (count, _, _))| *count >= 2) .map(
204 |((account_id, _counterparty), (count, volume, suspicious))| {
205 let mut label = RelationshipLabel::new(
206 account_id,
207 Uuid::new_v4(), RelationshipType::TransactionCounterparty,
209 )
210 .with_transactions(count, volume);
211
212 if suspicious {
213 label.is_mule_link = true;
214 }
215
216 label
217 },
218 )
219 .collect()
220 }
221
222 pub fn summarize(labels: &[RelationshipLabel]) -> RelationshipLabelSummary {
224 let total = labels.len();
225 let mule_links = labels.iter().filter(|l| l.is_mule_link).count();
226 let shell_links = labels.iter().filter(|l| l.is_shell_link).count();
227 let ownership_links = labels
228 .iter()
229 .filter(|l| l.ownership_percent.is_some())
230 .count();
231
232 let mut by_type = std::collections::HashMap::new();
233 for label in labels {
234 *by_type.entry(label.relationship_type).or_insert(0) += 1;
235 }
236
237 RelationshipLabelSummary {
238 total_relationships: total,
239 mule_link_count: mule_links,
240 mule_link_rate: mule_links as f64 / total.max(1) as f64,
241 shell_link_count: shell_links,
242 shell_link_rate: shell_links as f64 / total.max(1) as f64,
243 ownership_link_count: ownership_links,
244 by_type,
245 }
246 }
247}
248
249#[derive(Debug, Clone)]
251pub struct RelationshipLabelSummary {
252 pub total_relationships: usize,
254 pub mule_link_count: usize,
256 pub mule_link_rate: f64,
258 pub shell_link_count: usize,
260 pub shell_link_rate: f64,
262 pub ownership_link_count: usize,
264 pub by_type: std::collections::HashMap<RelationshipType, usize>,
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_relationship_label() {
274 let source = Uuid::new_v4();
275 let target = Uuid::new_v4();
276
277 let label = RelationshipLabel::new(source, target, RelationshipType::Family);
278
279 assert_eq!(label.source_id, source);
280 assert_eq!(label.target_id, target);
281 assert!(!label.is_mule_link);
282 }
283
284 #[test]
285 fn test_mule_link() {
286 let source = Uuid::new_v4();
287 let target = Uuid::new_v4();
288
289 let label =
290 RelationshipLabel::new(source, target, RelationshipType::Unknown).as_mule_link();
291
292 assert!(label.is_mule_link);
293 assert_eq!(label.relationship_type, RelationshipType::MuleLink);
294 }
295
296 #[test]
297 fn test_ownership_label() {
298 let owner = Uuid::new_v4();
299 let entity = Uuid::new_v4();
300
301 let label =
302 RelationshipLabel::new(owner, entity, RelationshipType::Unknown).with_ownership(25.0);
303
304 assert_eq!(label.ownership_percent, Some(25.0));
305 assert_eq!(
306 label.relationship_type,
307 RelationshipType::BeneficialOwnership
308 );
309 }
310}