datasynth_generators/sourcing/
qualification_generator.rs1use chrono::NaiveDate;
4use datasynth_config::schema::QualificationConfig;
5use datasynth_core::models::sourcing::{
6 QualificationScore, QualificationStatus, SupplierCertification, SupplierQualification,
7};
8use datasynth_core::uuid_factory::{DeterministicUuidFactory, GeneratorType};
9use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11
12pub struct QualificationGenerator {
14 rng: ChaCha8Rng,
15 uuid_factory: DeterministicUuidFactory,
16 config: QualificationConfig,
17}
18
19impl QualificationGenerator {
20 pub fn new(seed: u64) -> Self {
22 Self {
23 rng: ChaCha8Rng::seed_from_u64(seed),
24 uuid_factory: DeterministicUuidFactory::new(seed, GeneratorType::SourcingProject),
25 config: QualificationConfig::default(),
26 }
27 }
28
29 pub fn with_config(seed: u64, config: QualificationConfig) -> Self {
31 Self {
32 rng: ChaCha8Rng::seed_from_u64(seed),
33 uuid_factory: DeterministicUuidFactory::new(seed, GeneratorType::SourcingProject),
34 config,
35 }
36 }
37
38 pub fn generate(
40 &mut self,
41 company_code: &str,
42 vendor_ids: &[String],
43 sourcing_project_id: Option<&str>,
44 evaluator_id: &str,
45 qualification_date: NaiveDate,
46 ) -> Vec<SupplierQualification> {
47 let mut qualifications = Vec::new();
48
49 for vendor_id in vendor_ids {
50 let criteria = vec![
51 ("Financial Stability", self.config.financial_weight, 60.0),
52 ("Quality Management", self.config.quality_weight, 65.0),
53 ("Delivery Performance", self.config.delivery_weight, 60.0),
54 ("Compliance", self.config.compliance_weight, 70.0),
55 ];
56
57 let mut scores = Vec::new();
58 let mut weighted_total = 0.0;
59 let mut all_mandatory_passed = true;
60
61 for (name, weight, min_score) in &criteria {
62 let score = self.rng.gen_range(40.0..=100.0);
63 let passed = score >= *min_score;
64 if !passed {
65 all_mandatory_passed = false;
66 }
67 weighted_total += score * weight;
68 scores.push(QualificationScore {
69 criterion_name: name.to_string(),
70 score,
71 passed,
72 comments: None,
73 });
74 }
75
76 let status = if !all_mandatory_passed {
77 QualificationStatus::Disqualified
78 } else if weighted_total >= 75.0 {
79 QualificationStatus::Qualified
80 } else if weighted_total >= 60.0 {
81 QualificationStatus::ConditionallyQualified
82 } else {
83 QualificationStatus::Disqualified
84 };
85
86 let valid_until = if matches!(
87 status,
88 QualificationStatus::Qualified | QualificationStatus::ConditionallyQualified
89 ) {
90 Some(qualification_date + chrono::Duration::days(self.config.validity_days as i64))
91 } else {
92 None
93 };
94
95 qualifications.push(SupplierQualification {
96 qualification_id: self.uuid_factory.next().to_string(),
97 vendor_id: vendor_id.clone(),
98 sourcing_project_id: sourcing_project_id.map(|s| s.to_string()),
99 company_code: company_code.to_string(),
100 status,
101 start_date: qualification_date - chrono::Duration::days(14),
102 completion_date: Some(qualification_date),
103 valid_until,
104 scores,
105 overall_score: weighted_total,
106 evaluator_id: evaluator_id.to_string(),
107 certifications: Vec::new(),
108 conditions: if matches!(status, QualificationStatus::ConditionallyQualified) {
109 Some("Improvement plan required within 90 days".to_string())
110 } else {
111 None
112 },
113 });
114 }
115
116 qualifications
117 }
118
119 pub fn generate_certifications(
121 &mut self,
122 vendor_id: &str,
123 base_date: NaiveDate,
124 ) -> Vec<SupplierCertification> {
125 let cert_types = [
126 ("ISO 9001", "TUV Rheinland"),
127 ("ISO 14001", "Bureau Veritas"),
128 ("SOC 2 Type II", "Deloitte"),
129 ("ISO 27001", "BSI Group"),
130 ];
131
132 let count = self.rng.gen_range(0..=3);
133 let mut certs = Vec::new();
134
135 for &(cert_type, issuer) in cert_types.iter().take(count) {
136 let issue_date = base_date - chrono::Duration::days(self.rng.gen_range(30..=730));
137 let expiry_date = issue_date + chrono::Duration::days(365 * 3);
138
139 certs.push(SupplierCertification {
140 certification_id: self.uuid_factory.next().to_string(),
141 vendor_id: vendor_id.to_string(),
142 certification_type: cert_type.to_string(),
143 issuing_body: issuer.to_string(),
144 issue_date,
145 expiry_date,
146 is_valid: expiry_date >= base_date,
147 });
148 }
149
150 certs
151 }
152}
153
154#[cfg(test)]
155#[allow(clippy::unwrap_used)]
156mod tests {
157 use super::*;
158
159 fn test_vendor_ids() -> Vec<String> {
160 vec!["V001".to_string(), "V002".to_string(), "V003".to_string()]
161 }
162
163 #[test]
164 fn test_basic_generation() {
165 let mut gen = QualificationGenerator::new(42);
166 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
167 let results = gen.generate("C001", &test_vendor_ids(), Some("SP-001"), "EVAL-01", date);
168
169 assert_eq!(results.len(), 3);
170 for qual in &results {
171 assert_eq!(qual.company_code, "C001");
172 assert!(!qual.qualification_id.is_empty());
173 assert!(!qual.vendor_id.is_empty());
174 assert_eq!(qual.evaluator_id, "EVAL-01");
175 assert_eq!(qual.sourcing_project_id.as_deref(), Some("SP-001"));
176 assert!(!qual.scores.is_empty());
177 assert_eq!(qual.scores.len(), 4); assert!(qual.overall_score > 0.0);
179 }
180 }
181
182 #[test]
183 fn test_deterministic() {
184 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
185 let vendors = test_vendor_ids();
186
187 let mut gen1 = QualificationGenerator::new(42);
188 let mut gen2 = QualificationGenerator::new(42);
189
190 let r1 = gen1.generate("C001", &vendors, Some("SP-001"), "EVAL-01", date);
191 let r2 = gen2.generate("C001", &vendors, Some("SP-001"), "EVAL-01", date);
192
193 assert_eq!(r1.len(), r2.len());
194 for (a, b) in r1.iter().zip(r2.iter()) {
195 assert_eq!(a.qualification_id, b.qualification_id);
196 assert_eq!(a.vendor_id, b.vendor_id);
197 assert_eq!(a.overall_score, b.overall_score);
198 assert_eq!(a.status, b.status);
199 }
200 }
201
202 #[test]
203 fn test_field_constraints() {
204 let mut gen = QualificationGenerator::new(99);
205 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
206 let results = gen.generate("C001", &test_vendor_ids(), None, "EVAL-01", date);
207
208 for qual in &results {
209 for score in &qual.scores {
211 assert!(score.score >= 40.0 && score.score <= 100.0);
212 assert!(!score.criterion_name.is_empty());
213 }
214
215 match qual.status {
217 QualificationStatus::Qualified | QualificationStatus::ConditionallyQualified => {
218 assert!(qual.valid_until.is_some());
219 }
220 QualificationStatus::Disqualified => {
221 assert!(qual.valid_until.is_none());
222 }
223 _ => {}
224 }
225
226 assert_eq!(qual.start_date, date - chrono::Duration::days(14));
228 assert_eq!(qual.completion_date, Some(date));
229 }
230 }
231
232 #[test]
233 fn test_generate_certifications() {
234 let mut gen = QualificationGenerator::new(42);
235 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
236 let certs = gen.generate_certifications("V001", date);
237
238 assert!(certs.len() <= 3);
240 for cert in &certs {
241 assert_eq!(cert.vendor_id, "V001");
242 assert!(!cert.certification_id.is_empty());
243 assert!(!cert.certification_type.is_empty());
244 assert!(!cert.issuing_body.is_empty());
245 assert!(cert.expiry_date > cert.issue_date);
246 }
247 }
248}