datasynth_banking/models/
beneficial_owner.rs1use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct BeneficialOwner {
11 pub ubo_id: Uuid,
13 pub name: String,
15 pub date_of_birth: Option<NaiveDate>,
17 pub country_of_residence: String,
19 pub citizenship_country: Option<String>,
21 #[serde(with = "rust_decimal::serde::str")]
23 pub ownership_percentage: Decimal,
24 pub control_type: ControlType,
26 pub is_direct: bool,
28 pub intermediary_entity: Option<IntermediaryEntity>,
30 pub is_pep: bool,
32 pub is_sanctioned: bool,
34 pub verification_status: VerificationStatus,
36 pub verification_date: Option<NaiveDate>,
38 pub source_of_wealth: Option<String>,
40
41 pub is_true_ubo: bool,
44 pub is_hidden: bool,
46}
47
48impl BeneficialOwner {
49 pub fn new(ubo_id: Uuid, name: &str, country: &str, ownership_percentage: Decimal) -> Self {
51 Self {
52 ubo_id,
53 name: name.to_string(),
54 date_of_birth: None,
55 country_of_residence: country.to_string(),
56 citizenship_country: Some(country.to_string()),
57 ownership_percentage,
58 control_type: ControlType::OwnershipInterest,
59 is_direct: true,
60 intermediary_entity: None,
61 is_pep: false,
62 is_sanctioned: false,
63 verification_status: VerificationStatus::Verified,
64 verification_date: None,
65 source_of_wealth: None,
66 is_true_ubo: true,
67 is_hidden: false,
68 }
69 }
70
71 pub fn with_intermediary(mut self, intermediary: IntermediaryEntity) -> Self {
73 self.is_direct = false;
74 self.intermediary_entity = Some(intermediary);
75 self
76 }
77
78 pub fn as_pep(mut self) -> Self {
80 self.is_pep = true;
81 self
82 }
83
84 pub fn as_nominee(mut self) -> Self {
86 self.is_true_ubo = false;
87 self
88 }
89
90 pub fn as_hidden(mut self) -> Self {
92 self.is_hidden = true;
93 self.is_true_ubo = false;
94 self
95 }
96
97 pub fn calculate_risk_score(&self) -> u8 {
99 let mut score = 0.0;
100
101 let ownership_f64: f64 = self.ownership_percentage.try_into().unwrap_or(0.0);
103 if ownership_f64 >= 25.0 {
104 score += 20.0;
105 } else if ownership_f64 >= 10.0 {
106 score += 10.0;
107 }
108
109 score += self.control_type.risk_weight() * 10.0;
111
112 if !self.is_direct {
114 score += 15.0;
115 }
116
117 if self.is_pep {
119 score += 25.0;
120 }
121
122 if self.is_sanctioned {
124 score += 50.0;
125 }
126
127 if self.verification_status != VerificationStatus::Verified {
129 score += 10.0;
130 }
131
132 if self.is_hidden {
134 score += 30.0;
135 }
136
137 score.min(100.0) as u8
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
143#[serde(rename_all = "snake_case")]
144pub enum ControlType {
145 #[default]
147 OwnershipInterest,
148 VotingRights,
150 BoardControl,
152 ManagementControl,
154 ContractualControl,
156 FamilyRelationship,
158 TrustArrangement,
160 NomineeArrangement,
162}
163
164impl ControlType {
165 pub fn risk_weight(&self) -> f64 {
167 match self {
168 Self::OwnershipInterest => 1.0,
169 Self::VotingRights => 1.1,
170 Self::BoardControl | Self::ManagementControl => 1.2,
171 Self::ContractualControl => 1.4,
172 Self::FamilyRelationship => 1.3,
173 Self::TrustArrangement => 1.6,
174 Self::NomineeArrangement => 2.0,
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
181#[serde(rename_all = "snake_case")]
182pub enum VerificationStatus {
183 #[default]
185 Verified,
186 PartiallyVerified,
188 Pending,
190 UnableToVerify,
192 Expired,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct IntermediaryEntity {
199 pub entity_id: Uuid,
201 pub name: String,
203 pub entity_type: IntermediaryType,
205 pub jurisdiction: String,
207 #[serde(with = "rust_decimal::serde::str")]
209 pub ownership_percentage: Decimal,
210 pub is_shell: bool,
212 pub registration_number: Option<String>,
214}
215
216impl IntermediaryEntity {
217 pub fn new(
219 entity_id: Uuid,
220 name: &str,
221 entity_type: IntermediaryType,
222 jurisdiction: &str,
223 ownership_percentage: Decimal,
224 ) -> Self {
225 Self {
226 entity_id,
227 name: name.to_string(),
228 entity_type,
229 jurisdiction: jurisdiction.to_string(),
230 ownership_percentage,
231 is_shell: false,
232 registration_number: None,
233 }
234 }
235
236 pub fn as_shell(mut self) -> Self {
238 self.is_shell = true;
239 self
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum IntermediaryType {
247 HoldingCompany,
249 SPV,
251 Trust,
253 Foundation,
255 LimitedPartnership,
257 LLC,
259 Other,
261}
262
263impl IntermediaryType {
264 pub fn risk_weight(&self) -> f64 {
266 match self {
267 Self::HoldingCompany => 1.2,
268 Self::SPV => 1.5,
269 Self::Trust => 1.6,
270 Self::Foundation => 1.5,
271 Self::LimitedPartnership => 1.3,
272 Self::LLC => 1.2,
273 Self::Other => 1.4,
274 }
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct OwnershipChain {
281 pub ultimate_owner: BeneficialOwner,
283 pub intermediaries: Vec<IntermediaryEntity>,
285 pub total_layers: u8,
287 #[serde(with = "rust_decimal::serde::str")]
289 pub effective_ownership: Decimal,
290}
291
292impl OwnershipChain {
293 pub fn new(owner: BeneficialOwner) -> Self {
295 let effective = owner.ownership_percentage;
296 Self {
297 ultimate_owner: owner,
298 intermediaries: Vec::new(),
299 total_layers: 1,
300 effective_ownership: effective,
301 }
302 }
303
304 pub fn add_intermediary(&mut self, intermediary: IntermediaryEntity) {
306 let intermediary_pct: f64 = intermediary
308 .ownership_percentage
309 .try_into()
310 .unwrap_or(100.0);
311 let current_effective: f64 = self.effective_ownership.try_into().unwrap_or(0.0);
312 self.effective_ownership =
313 Decimal::from_f64_retain(current_effective * intermediary_pct / 100.0)
314 .unwrap_or(Decimal::ZERO);
315
316 self.intermediaries.push(intermediary);
317 self.total_layers += 1;
318 }
319
320 pub fn complexity_score(&self) -> u8 {
322 let mut score = (self.total_layers as f64 - 1.0) * 10.0;
323
324 for intermediary in &self.intermediaries {
325 score += intermediary.entity_type.risk_weight() * 5.0;
326 if intermediary.is_shell {
327 score += 20.0;
328 }
329 }
330
331 if !self.ultimate_owner.is_true_ubo {
332 score += 30.0;
333 }
334
335 score.min(100.0) as u8
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn test_beneficial_owner() {
345 let owner = BeneficialOwner::new(Uuid::new_v4(), "John Doe", "US", Decimal::from(50));
346 assert!(owner.is_true_ubo);
347 assert!(owner.is_direct);
348 }
349
350 #[test]
351 fn test_ownership_chain() {
352 let owner = BeneficialOwner::new(Uuid::new_v4(), "John Doe", "US", Decimal::from(100));
353 let mut chain = OwnershipChain::new(owner);
354
355 let holding = IntermediaryEntity::new(
356 Uuid::new_v4(),
357 "Holding Co Ltd",
358 IntermediaryType::HoldingCompany,
359 "KY",
360 Decimal::from(80),
361 );
362 chain.add_intermediary(holding);
363
364 assert_eq!(chain.total_layers, 2);
365 assert!(chain.effective_ownership < Decimal::from(100));
366 }
367
368 #[test]
369 fn test_risk_scoring() {
370 let base_owner = BeneficialOwner::new(Uuid::new_v4(), "Jane Doe", "US", Decimal::from(30));
371 let base_score = base_owner.calculate_risk_score();
372
373 let pep_owner =
374 BeneficialOwner::new(Uuid::new_v4(), "Minister Smith", "US", Decimal::from(30))
375 .as_pep();
376 let pep_score = pep_owner.calculate_risk_score();
377
378 assert!(pep_score > base_score);
379 }
380}