datasynth_eval/banking/
kyc_completeness.rs1use crate::error::EvalResult;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone)]
11pub struct KycProfileData {
12 pub profile_id: String,
14 pub has_name: bool,
16 pub has_dob: bool,
18 pub has_address: bool,
20 pub has_id_document: bool,
22 pub has_risk_rating: bool,
24 pub has_beneficial_owner: bool,
26 pub is_entity: bool,
28 pub is_verified: bool,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct KycCompletenessThresholds {
35 pub min_core_field_rate: f64,
37 pub min_beneficial_owner_rate: f64,
39 pub min_risk_rating_rate: f64,
41}
42
43impl Default for KycCompletenessThresholds {
44 fn default() -> Self {
45 Self {
46 min_core_field_rate: 0.95,
47 min_beneficial_owner_rate: 0.90,
48 min_risk_rating_rate: 0.95,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct KycCompletenessAnalysis {
56 pub core_field_rate: f64,
58 pub name_rate: f64,
60 pub dob_rate: f64,
62 pub address_rate: f64,
64 pub id_document_rate: f64,
66 pub risk_rating_rate: f64,
68 pub beneficial_owner_rate: f64,
70 pub verification_rate: f64,
72 pub total_profiles: usize,
74 pub passes: bool,
76 pub issues: Vec<String>,
78}
79
80pub struct KycCompletenessAnalyzer {
82 thresholds: KycCompletenessThresholds,
83}
84
85impl KycCompletenessAnalyzer {
86 pub fn new() -> Self {
88 Self {
89 thresholds: KycCompletenessThresholds::default(),
90 }
91 }
92
93 pub fn with_thresholds(thresholds: KycCompletenessThresholds) -> Self {
95 Self { thresholds }
96 }
97
98 pub fn analyze(&self, profiles: &[KycProfileData]) -> EvalResult<KycCompletenessAnalysis> {
100 let mut issues = Vec::new();
101 let total = profiles.len();
102
103 if total == 0 {
104 return Ok(KycCompletenessAnalysis {
105 core_field_rate: 1.0,
106 name_rate: 1.0,
107 dob_rate: 1.0,
108 address_rate: 1.0,
109 id_document_rate: 1.0,
110 risk_rating_rate: 1.0,
111 beneficial_owner_rate: 1.0,
112 verification_rate: 1.0,
113 total_profiles: 0,
114 passes: true,
115 issues: Vec::new(),
116 });
117 }
118
119 let name_count = profiles.iter().filter(|p| p.has_name).count();
120 let dob_count = profiles.iter().filter(|p| p.has_dob).count();
121 let address_count = profiles.iter().filter(|p| p.has_address).count();
122 let id_count = profiles.iter().filter(|p| p.has_id_document).count();
123 let risk_count = profiles.iter().filter(|p| p.has_risk_rating).count();
124 let verified_count = profiles.iter().filter(|p| p.is_verified).count();
125
126 let core_complete = profiles
127 .iter()
128 .filter(|p| p.has_name && p.has_dob && p.has_address && p.has_id_document)
129 .count();
130
131 let entities: Vec<&KycProfileData> = profiles.iter().filter(|p| p.is_entity).collect();
132 let bo_count = entities.iter().filter(|p| p.has_beneficial_owner).count();
133
134 let core_field_rate = core_complete as f64 / total as f64;
135 let name_rate = name_count as f64 / total as f64;
136 let dob_rate = dob_count as f64 / total as f64;
137 let address_rate = address_count as f64 / total as f64;
138 let id_document_rate = id_count as f64 / total as f64;
139 let risk_rating_rate = risk_count as f64 / total as f64;
140 let verification_rate = verified_count as f64 / total as f64;
141 let beneficial_owner_rate = if entities.is_empty() {
142 1.0
143 } else {
144 bo_count as f64 / entities.len() as f64
145 };
146
147 if core_field_rate < self.thresholds.min_core_field_rate {
148 issues.push(format!(
149 "Core field rate {:.3} < {:.3}",
150 core_field_rate, self.thresholds.min_core_field_rate
151 ));
152 }
153 if beneficial_owner_rate < self.thresholds.min_beneficial_owner_rate {
154 issues.push(format!(
155 "Beneficial owner rate {:.3} < {:.3}",
156 beneficial_owner_rate, self.thresholds.min_beneficial_owner_rate
157 ));
158 }
159 if risk_rating_rate < self.thresholds.min_risk_rating_rate {
160 issues.push(format!(
161 "Risk rating rate {:.3} < {:.3}",
162 risk_rating_rate, self.thresholds.min_risk_rating_rate
163 ));
164 }
165
166 let passes = issues.is_empty();
167
168 Ok(KycCompletenessAnalysis {
169 core_field_rate,
170 name_rate,
171 dob_rate,
172 address_rate,
173 id_document_rate,
174 risk_rating_rate,
175 beneficial_owner_rate,
176 verification_rate,
177 total_profiles: total,
178 passes,
179 issues,
180 })
181 }
182}
183
184impl Default for KycCompletenessAnalyzer {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190#[cfg(test)]
191#[allow(clippy::unwrap_used)]
192mod tests {
193 use super::*;
194
195 fn complete_profile() -> KycProfileData {
196 KycProfileData {
197 profile_id: "KYC001".to_string(),
198 has_name: true,
199 has_dob: true,
200 has_address: true,
201 has_id_document: true,
202 has_risk_rating: true,
203 has_beneficial_owner: true,
204 is_entity: true,
205 is_verified: true,
206 }
207 }
208
209 #[test]
210 fn test_complete_profiles() {
211 let analyzer = KycCompletenessAnalyzer::new();
212 let result = analyzer.analyze(&[complete_profile()]).unwrap();
213 assert!(result.passes);
214 assert_eq!(result.core_field_rate, 1.0);
215 }
216
217 #[test]
218 fn test_incomplete_profiles() {
219 let analyzer = KycCompletenessAnalyzer::new();
220 let mut profile = complete_profile();
221 profile.has_name = false;
222 profile.has_risk_rating = false;
223
224 let result = analyzer.analyze(&[profile]).unwrap();
225 assert!(!result.passes);
226 assert_eq!(result.core_field_rate, 0.0);
227 }
228
229 #[test]
230 fn test_empty() {
231 let analyzer = KycCompletenessAnalyzer::new();
232 let result = analyzer.analyze(&[]).unwrap();
233 assert!(result.passes);
234 }
235}