Skip to main content

datasynth_banking/labels/
relationship_labels.rs

1//! Relationship-level label generation.
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use crate::models::{BankingCustomer, BeneficialOwner, CustomerRelationship};
7
8/// Relationship type for labeling.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum RelationshipType {
11    /// Family relationship
12    Family,
13    /// Employer-employee relationship
14    Employment,
15    /// Business partner
16    BusinessPartner,
17    /// Vendor relationship
18    Vendor,
19    /// Customer relationship (B2B)
20    Customer,
21    /// Beneficial ownership
22    BeneficialOwnership,
23    /// Transaction counterparty
24    TransactionCounterparty,
25    /// Money mule link
26    MuleLink,
27    /// Shell company link
28    ShellLink,
29    /// Unknown/other
30    Unknown,
31}
32
33/// Relationship-level labels for ML training.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct RelationshipLabel {
36    /// Source entity ID
37    pub source_id: Uuid,
38    /// Target entity ID
39    pub target_id: Uuid,
40    /// Relationship type
41    pub relationship_type: RelationshipType,
42    /// Is this a mule network link?
43    pub is_mule_link: bool,
44    /// Is this a shell company link?
45    pub is_shell_link: bool,
46    /// Ownership percentage (for UBO edges)
47    pub ownership_percent: Option<f64>,
48    /// Number of transactions between entities
49    pub transaction_count: u32,
50    /// Total transaction volume
51    pub transaction_volume: f64,
52    /// Relationship strength score (0.0-1.0)
53    pub strength: f64,
54    /// Associated case ID
55    pub case_id: Option<String>,
56    /// Confidence score
57    pub confidence: f64,
58}
59
60impl RelationshipLabel {
61    /// Create a new relationship label.
62    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    /// Mark as mule link.
79    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    /// Mark as shell link.
86    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    /// Set ownership percentage.
93    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    /// Set transaction statistics.
100    pub fn with_transactions(mut self, count: u32, volume: f64) -> Self {
101        self.transaction_count = count;
102        self.transaction_volume = volume;
103        // Compute strength based on transaction frequency
104        self.strength = (count as f64 / 100.0).min(1.0);
105        self
106    }
107
108    /// Set case ID.
109    pub fn with_case(mut self, case_id: &str) -> Self {
110        self.case_id = Some(case_id.to_string());
111        self
112    }
113}
114
115/// Relationship label extractor.
116pub struct RelationshipLabelExtractor;
117
118impl RelationshipLabelExtractor {
119    /// Extract relationship labels from customers.
120    pub fn extract_from_customers(customers: &[BankingCustomer]) -> Vec<RelationshipLabel> {
121        let mut labels = Vec::new();
122
123        for customer in customers {
124            // Extract explicit relationships
125            for rel in &customer.relationships {
126                let label = Self::from_customer_relationship(customer.customer_id, rel);
127                labels.push(label);
128            }
129
130            // Extract beneficial ownership relationships
131            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    /// Create label from customer relationship.
141    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    /// Create label from beneficial owner.
167    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        // Check for shell company indicators (hidden ownership or indirect with intermediary)
174        if bo.is_hidden || bo.intermediary_entity.is_some() {
175            label = label.as_shell_link();
176        }
177
178        label
179    }
180
181    /// Extract transaction-based relationships.
182    pub fn extract_from_transactions(
183        transactions: &[crate::models::BankTransaction],
184    ) -> Vec<RelationshipLabel> {
185        use std::collections::HashMap;
186
187        // Group by account-counterparty pairs
188        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) // Only significant relationships
203            .map(
204                |((account_id, _counterparty), (count, volume, suspicious))| {
205                    let mut label = RelationshipLabel::new(
206                        account_id,
207                        Uuid::new_v4(), // Counterparty UUID would come from counterparty pool
208                        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    /// Get relationship label summary.
223    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/// Relationship label summary.
250#[derive(Debug, Clone)]
251pub struct RelationshipLabelSummary {
252    /// Total relationships
253    pub total_relationships: usize,
254    /// Number of mule links
255    pub mule_link_count: usize,
256    /// Mule link rate
257    pub mule_link_rate: f64,
258    /// Number of shell links
259    pub shell_link_count: usize,
260    /// Shell link rate
261    pub shell_link_rate: f64,
262    /// Number of ownership links
263    pub ownership_link_count: usize,
264    /// Counts by relationship type
265    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}