Skip to main content

datasynth_eval/banking/
kyc_completeness.rs

1//! KYC profile completeness evaluator.
2//!
3//! Validates that KYC profiles have required fields populated
4//! and beneficial owner coverage meets standards.
5
6use crate::error::EvalResult;
7use serde::{Deserialize, Serialize};
8
9/// KYC profile data for validation.
10#[derive(Debug, Clone)]
11pub struct KycProfileData {
12    /// Profile identifier.
13    pub profile_id: String,
14    /// Whether customer name is populated.
15    pub has_name: bool,
16    /// Whether date of birth / incorporation date is populated.
17    pub has_dob: bool,
18    /// Whether address is populated.
19    pub has_address: bool,
20    /// Whether ID document is populated.
21    pub has_id_document: bool,
22    /// Whether risk rating is assigned.
23    pub has_risk_rating: bool,
24    /// Whether beneficial owner information is populated (for entities).
25    pub has_beneficial_owner: bool,
26    /// Whether the profile is for an entity (vs individual).
27    pub is_entity: bool,
28    /// Whether the profile has been reviewed/verified.
29    pub is_verified: bool,
30}
31
32/// Thresholds for KYC completeness.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct KycCompletenessThresholds {
35    /// Minimum rate for core fields (name, DOB, address, ID).
36    pub min_core_field_rate: f64,
37    /// Minimum beneficial owner coverage for entities.
38    pub min_beneficial_owner_rate: f64,
39    /// Minimum risk rating coverage.
40    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/// Results of KYC completeness analysis.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct KycCompletenessAnalysis {
56    /// Core field completeness rate (name + DOB + address + ID all present).
57    pub core_field_rate: f64,
58    /// Individual field rates.
59    pub name_rate: f64,
60    /// DOB/incorporation date rate.
61    pub dob_rate: f64,
62    /// Address rate.
63    pub address_rate: f64,
64    /// ID document rate.
65    pub id_document_rate: f64,
66    /// Risk rating coverage.
67    pub risk_rating_rate: f64,
68    /// Beneficial owner rate for entities.
69    pub beneficial_owner_rate: f64,
70    /// Verification rate.
71    pub verification_rate: f64,
72    /// Total profiles evaluated.
73    pub total_profiles: usize,
74    /// Overall pass/fail.
75    pub passes: bool,
76    /// Issues found.
77    pub issues: Vec<String>,
78}
79
80/// Analyzer for KYC completeness.
81pub struct KycCompletenessAnalyzer {
82    thresholds: KycCompletenessThresholds,
83}
84
85impl KycCompletenessAnalyzer {
86    /// Create a new analyzer with default thresholds.
87    pub fn new() -> Self {
88        Self {
89            thresholds: KycCompletenessThresholds::default(),
90        }
91    }
92
93    /// Create with custom thresholds.
94    pub fn with_thresholds(thresholds: KycCompletenessThresholds) -> Self {
95        Self { thresholds }
96    }
97
98    /// Analyze KYC profiles.
99    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}