datasynth_banking/models/
kyc_profile.rs1use datasynth_core::models::banking::{
4 CashIntensity, CountryExposure, FrequencyBand, SourceOfFunds, SourceOfWealth, TurnoverBand,
5};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct KycProfile {
11 pub declared_purpose: String,
13 pub expected_monthly_turnover: TurnoverBand,
15 pub expected_transaction_frequency: FrequencyBand,
17 pub expected_categories: Vec<ExpectedCategory>,
19 pub source_of_funds: SourceOfFunds,
21 pub source_of_wealth: Option<SourceOfWealth>,
23 pub geographic_exposure: Vec<CountryExposure>,
25 pub cash_intensity: CashIntensity,
27 pub beneficial_owner_complexity: u8,
29 pub international_rate: f64,
31 pub large_transaction_rate: f64,
33 pub large_transaction_threshold: u64,
35 pub completeness_score: f64,
37
38 pub true_source_of_funds: Option<SourceOfFunds>,
41 pub true_turnover: Option<TurnoverBand>,
43 pub is_truthful: bool,
45}
46
47impl Default for KycProfile {
48 fn default() -> Self {
49 Self {
50 declared_purpose: "Personal banking".to_string(),
51 expected_monthly_turnover: TurnoverBand::default(),
52 expected_transaction_frequency: FrequencyBand::default(),
53 expected_categories: Vec::new(),
54 source_of_funds: SourceOfFunds::Employment,
55 source_of_wealth: None,
56 geographic_exposure: Vec::new(),
57 cash_intensity: CashIntensity::default(),
58 beneficial_owner_complexity: 0,
59 international_rate: 0.05,
60 large_transaction_rate: 0.02,
61 large_transaction_threshold: 10_000,
62 completeness_score: 1.0,
63 true_source_of_funds: None,
64 true_turnover: None,
65 is_truthful: true,
66 }
67 }
68}
69
70impl KycProfile {
71 pub fn new(purpose: &str, source_of_funds: SourceOfFunds) -> Self {
73 Self {
74 declared_purpose: purpose.to_string(),
75 source_of_funds,
76 ..Default::default()
77 }
78 }
79
80 pub fn retail_standard() -> Self {
82 Self {
83 declared_purpose: "Personal checking and savings".to_string(),
84 expected_monthly_turnover: TurnoverBand::Low,
85 expected_transaction_frequency: FrequencyBand::Medium,
86 source_of_funds: SourceOfFunds::Employment,
87 cash_intensity: CashIntensity::Low,
88 ..Default::default()
89 }
90 }
91
92 pub fn high_net_worth() -> Self {
94 Self {
95 declared_purpose: "Wealth management and investment".to_string(),
96 expected_monthly_turnover: TurnoverBand::VeryHigh,
97 expected_transaction_frequency: FrequencyBand::High,
98 source_of_funds: SourceOfFunds::Investments,
99 source_of_wealth: Some(SourceOfWealth::BusinessOwnership),
100 cash_intensity: CashIntensity::VeryLow,
101 international_rate: 0.20,
102 large_transaction_rate: 0.15,
103 large_transaction_threshold: 50_000,
104 ..Default::default()
105 }
106 }
107
108 pub fn small_business() -> Self {
110 Self {
111 declared_purpose: "Business operations".to_string(),
112 expected_monthly_turnover: TurnoverBand::Medium,
113 expected_transaction_frequency: FrequencyBand::High,
114 source_of_funds: SourceOfFunds::SelfEmployment,
115 cash_intensity: CashIntensity::Moderate,
116 large_transaction_rate: 0.05,
117 large_transaction_threshold: 25_000,
118 ..Default::default()
119 }
120 }
121
122 pub fn cash_intensive_business() -> Self {
124 Self {
125 declared_purpose: "Retail business operations".to_string(),
126 expected_monthly_turnover: TurnoverBand::High,
127 expected_transaction_frequency: FrequencyBand::VeryHigh,
128 source_of_funds: SourceOfFunds::SelfEmployment,
129 cash_intensity: CashIntensity::VeryHigh,
130 large_transaction_rate: 0.01,
131 large_transaction_threshold: 10_000,
132 ..Default::default()
133 }
134 }
135
136 pub fn with_turnover(mut self, turnover: TurnoverBand) -> Self {
138 self.expected_monthly_turnover = turnover;
139 self
140 }
141
142 pub fn with_frequency(mut self, frequency: FrequencyBand) -> Self {
144 self.expected_transaction_frequency = frequency;
145 self
146 }
147
148 pub fn with_expected_category(mut self, category: ExpectedCategory) -> Self {
150 self.expected_categories.push(category);
151 self
152 }
153
154 pub fn with_country_exposure(mut self, exposure: CountryExposure) -> Self {
156 self.geographic_exposure.push(exposure);
157 self
158 }
159
160 pub fn with_cash_intensity(mut self, intensity: CashIntensity) -> Self {
162 self.cash_intensity = intensity;
163 self
164 }
165
166 pub fn with_deception(
168 mut self,
169 true_source: SourceOfFunds,
170 true_turnover: Option<TurnoverBand>,
171 ) -> Self {
172 self.true_source_of_funds = Some(true_source);
173 self.true_turnover = true_turnover;
174 self.is_truthful = false;
175 self
176 }
177
178 pub fn calculate_risk_score(&self) -> u8 {
180 let mut score = 0.0;
181
182 score += self.source_of_funds.risk_weight() * 15.0;
184
185 let (_, max_turnover) = self.expected_monthly_turnover.range();
187 if max_turnover > 100_000 {
188 score += 15.0;
189 } else if max_turnover > 25_000 {
190 score += 10.0;
191 } else if max_turnover > 5_000 {
192 score += 5.0;
193 }
194
195 score += self.cash_intensity.risk_weight() * 10.0;
197
198 score += self.international_rate * 20.0;
200
201 score += (self.beneficial_owner_complexity as f64) * 2.0;
203
204 if !self.is_truthful {
206 score += 25.0;
207 }
208
209 score += (1.0 - self.completeness_score) * 10.0;
211
212 score.min(100.0) as u8
213 }
214
215 pub fn is_within_expected_turnover(&self, actual_monthly: u64) -> bool {
217 let (min, max) = self.expected_monthly_turnover.range();
218 actual_monthly >= min && actual_monthly <= max * 2 }
220
221 pub fn is_within_expected_frequency(&self, actual_count: u32) -> bool {
223 let (min, max) = self.expected_transaction_frequency.range();
224 actual_count >= min / 2 && actual_count <= max * 2
225 }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ExpectedCategory {
231 pub category: String,
233 pub expected_percentage: f64,
235 pub tolerance: f64,
237}
238
239impl ExpectedCategory {
240 pub fn new(category: &str, percentage: f64) -> Self {
242 Self {
243 category: category.to_string(),
244 expected_percentage: percentage,
245 tolerance: 0.1, }
247 }
248
249 pub fn matches(&self, actual_percentage: f64) -> bool {
251 (actual_percentage - self.expected_percentage).abs() <= self.tolerance
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_kyc_profile_default() {
261 let profile = KycProfile::default();
262 assert!(profile.is_truthful);
263 assert_eq!(profile.source_of_funds, SourceOfFunds::Employment);
264 }
265
266 #[test]
267 fn test_kyc_profile_presets() {
268 let retail = KycProfile::retail_standard();
269 assert_eq!(retail.expected_monthly_turnover, TurnoverBand::Low);
270
271 let hnw = KycProfile::high_net_worth();
272 assert_eq!(hnw.expected_monthly_turnover, TurnoverBand::VeryHigh);
273 assert!(hnw.source_of_wealth.is_some());
274 }
275
276 #[test]
277 fn test_deceptive_profile() {
278 let profile = KycProfile::retail_standard()
279 .with_deception(SourceOfFunds::CryptoAssets, Some(TurnoverBand::VeryHigh));
280
281 assert!(!profile.is_truthful);
282 assert!(profile.true_source_of_funds.is_some());
283
284 let base_score = KycProfile::retail_standard().calculate_risk_score();
285 let deceptive_score = profile.calculate_risk_score();
286 assert!(deceptive_score > base_score);
287 }
288
289 #[test]
290 fn test_turnover_check() {
291 let profile = KycProfile::default().with_turnover(TurnoverBand::Medium);
292 assert!(profile.is_within_expected_turnover(10_000));
294 assert!(profile.is_within_expected_turnover(40_000)); assert!(!profile.is_within_expected_turnover(100_000));
296 }
297}