1use chrono::NaiveDate;
4use datasynth_core::utils::seeded_rng;
5use rand::Rng;
6use rand_chacha::ChaCha8Rng;
7use rust_decimal::Decimal;
8use rust_decimal_macros::dec;
9
10use datasynth_core::models::process_evolution::{
11 ApprovalWorkflowChangeConfig, ControlEnhancementConfig, PolicyCategory, PolicyChangeConfig,
12 ProcessAutomationConfig, ProcessEvolutionEvent, ProcessEvolutionType, RolloutCurve,
13 ThresholdChange, WorkflowType,
14};
15
16#[derive(Debug, Clone)]
20pub struct ProcEvoGeneratorConfig {
21 pub type_weights: [f64; 4],
23 pub events_per_year: f64,
25}
26
27impl Default for ProcEvoGeneratorConfig {
28 fn default() -> Self {
29 Self {
30 type_weights: [0.25, 0.30, 0.25, 0.20],
31 events_per_year: 4.0,
32 }
33 }
34}
35
36pub struct ProcessEvolutionGenerator {
42 rng: ChaCha8Rng,
43 config: ProcEvoGeneratorConfig,
44 event_counter: usize,
45}
46
47const SEED_DISCRIMINATOR: u64 = 0xAE_0C;
50
51impl ProcessEvolutionGenerator {
52 pub fn new(seed: u64) -> Self {
54 Self {
55 rng: seeded_rng(seed, SEED_DISCRIMINATOR),
56 config: ProcEvoGeneratorConfig::default(),
57 event_counter: 0,
58 }
59 }
60
61 pub fn with_config(seed: u64, config: ProcEvoGeneratorConfig) -> Self {
63 Self {
64 rng: seeded_rng(seed, SEED_DISCRIMINATOR),
65 config,
66 event_counter: 0,
67 }
68 }
69
70 pub fn generate_events(
74 &mut self,
75 start_date: NaiveDate,
76 end_date: NaiveDate,
77 ) -> Vec<ProcessEvolutionEvent> {
78 let total_days = (end_date - start_date).num_days().max(1) as f64;
79 let total_years = total_days / 365.25;
80 let expected_count = (self.config.events_per_year * total_years).round() as usize;
81 let count = expected_count.max(1);
82
83 let mut events = Vec::with_capacity(count);
84
85 for _ in 0..count {
86 self.event_counter += 1;
87 let days_offset = self.rng.random_range(0..total_days as i64);
88 let effective_date = start_date + chrono::Duration::days(days_offset);
89
90 let event = self.build_event(effective_date);
91 events.push(event);
92 }
93
94 events.sort_by_key(|e| e.effective_date);
95 events
96 }
97
98 fn pick_event_type_index(&mut self) -> usize {
100 let weights = &self.config.type_weights;
101 let total: f64 = weights.iter().sum();
102 let mut r: f64 = self.rng.random_range(0.0..total);
103
104 for (i, &w) in weights.iter().enumerate() {
105 r -= w;
106 if r <= 0.0 {
107 return i;
108 }
109 }
110 0
111 }
112
113 fn build_event(&mut self, effective_date: NaiveDate) -> ProcessEvolutionEvent {
116 let event_id = format!("PROC-EVT-{:06}", self.event_counter);
117 let type_idx = self.pick_event_type_index();
118
119 let event_type = match type_idx {
120 0 => self.build_workflow_change(),
121 1 => self.build_automation(),
122 2 => self.build_policy_change(),
123 _ => self.build_control_enhancement(),
124 };
125
126 let description = match &event_type {
127 ProcessEvolutionType::ApprovalWorkflowChange(c) => {
128 Some(format!("Workflow change from {:?} to {:?}", c.from, c.to))
129 }
130 ProcessEvolutionType::ProcessAutomation(c) => {
131 Some(format!("Automation of {} process", c.process_name))
132 }
133 ProcessEvolutionType::PolicyChange(c) => {
134 Some(format!("Policy change in {:?} category", c.category))
135 }
136 ProcessEvolutionType::ControlEnhancement(c) => {
137 Some(format!("Enhancement of control {}", c.control_id))
138 }
139 };
140
141 let tags = vec![format!("type:{}", event_type.type_name())];
142
143 ProcessEvolutionEvent {
144 event_id,
145 event_type,
146 effective_date,
147 description,
148 tags,
149 }
150 }
151
152 fn build_workflow_change(&mut self) -> ProcessEvolutionType {
157 let all_types = [
158 WorkflowType::SingleApprover,
159 WorkflowType::DualApproval,
160 WorkflowType::MultiLevel,
161 WorkflowType::Automated,
162 WorkflowType::Matrix,
163 WorkflowType::Parallel,
164 ];
165
166 let from_idx = self.rng.random_range(0..all_types.len());
167 let mut to_idx = self.rng.random_range(0..all_types.len());
168 while to_idx == from_idx {
170 to_idx = self.rng.random_range(0..all_types.len());
171 }
172
173 let from = all_types[from_idx];
174 let to = all_types[to_idx];
175 let time_delta = to.processing_time_multiplier() / from.processing_time_multiplier();
176 let error_rate_impact = self.rng.random_range(0.01..0.04);
177 let transition_months = self.rng.random_range(2..6_u32);
178
179 let threshold_changes = if self.rng.random_bool(0.4) {
180 let old_val = Decimal::from(self.rng.random_range(5000..10000_i64));
181 let new_val = Decimal::from(self.rng.random_range(10000..25000_i64));
182 vec![ThresholdChange {
183 category: "amount".to_string(),
184 old_threshold: old_val,
185 new_threshold: new_val,
186 }]
187 } else {
188 Vec::new()
189 };
190
191 ProcessEvolutionType::ApprovalWorkflowChange(ApprovalWorkflowChangeConfig {
192 from,
193 to,
194 time_delta,
195 error_rate_impact,
196 transition_months,
197 threshold_changes,
198 })
199 }
200
201 fn build_automation(&mut self) -> ProcessEvolutionType {
202 let process_names = [
203 "three_way_match",
204 "invoice_processing",
205 "expense_approval",
206 "bank_reconciliation",
207 "period_close_checklist",
208 ];
209 let idx = self.rng.random_range(0..process_names.len());
210 let process_name = process_names[idx].to_string();
211
212 let manual_rate_before = self.rng.random_range(0.60..0.90);
213 let manual_rate_after = self.rng.random_range(0.05..0.25);
214 let error_rate_before = self.rng.random_range(0.03..0.08);
215 let error_rate_after = self.rng.random_range(0.005..0.02);
216 let processing_time_reduction = self.rng.random_range(0.20..0.50);
217 let rollout_months = self.rng.random_range(3..12_u32);
218
219 let curves = [
220 RolloutCurve::Linear,
221 RolloutCurve::SCurve,
222 RolloutCurve::Exponential,
223 RolloutCurve::Step,
224 ];
225 let curve_idx = self.rng.random_range(0..curves.len());
226 let rollout_curve = curves[curve_idx];
227
228 ProcessEvolutionType::ProcessAutomation(ProcessAutomationConfig {
229 process_name,
230 manual_rate_before,
231 manual_rate_after,
232 error_rate_before,
233 error_rate_after,
234 processing_time_reduction,
235 rollout_months,
236 rollout_curve,
237 affected_transaction_types: Vec::new(),
238 })
239 }
240
241 fn build_policy_change(&mut self) -> ProcessEvolutionType {
242 let categories = [
243 PolicyCategory::ApprovalThreshold,
244 PolicyCategory::ExpensePolicy,
245 PolicyCategory::TravelPolicy,
246 PolicyCategory::ProcurementPolicy,
247 PolicyCategory::CreditPolicy,
248 PolicyCategory::InventoryPolicy,
249 PolicyCategory::DocumentationRequirement,
250 PolicyCategory::Other,
251 ];
252 let cat_idx = self.rng.random_range(0..categories.len());
253 let category = categories[cat_idx];
254
255 let (old_value, new_value) = match category {
257 PolicyCategory::ApprovalThreshold
258 | PolicyCategory::ExpensePolicy
259 | PolicyCategory::CreditPolicy => {
260 let old = Decimal::from(self.rng.random_range(1000..10000_i64));
261 let new = Decimal::from(self.rng.random_range(5000..20000_i64));
262 (Some(old), Some(new))
263 }
264 PolicyCategory::InventoryPolicy | PolicyCategory::ProcurementPolicy => {
265 let old = Decimal::from(self.rng.random_range(100..500_i64));
266 let new = Decimal::from(self.rng.random_range(200..1000_i64));
267 (Some(old), Some(new))
268 }
269 _ => (None, None),
270 };
271
272 let transition_error_rate = self.rng.random_range(0.02..0.06);
273 let transition_months = self.rng.random_range(2..6_u32);
274
275 ProcessEvolutionType::PolicyChange(PolicyChangeConfig {
276 category,
277 description: Some(format!("Updated {} policy", category.code())),
278 old_value,
279 new_value,
280 transition_error_rate,
281 transition_months,
282 affected_controls: Vec::new(),
283 })
284 }
285
286 fn build_control_enhancement(&mut self) -> ProcessEvolutionType {
287 let control_id = format!("C-{:03}", self.event_counter);
288 let error_reduction = self.rng.random_range(0.01..0.05);
289 let processing_time_impact = self.rng.random_range(1.02..1.15);
290 let implementation_months = self.rng.random_range(1..4_u32);
291
292 let tolerance_change = if self.rng.random_bool(0.5) {
293 let old_tol = dec!(100) + Decimal::from(self.rng.random_range(0..400_i64));
294 let new_tol = dec!(50) + Decimal::from(self.rng.random_range(0..200_i64));
295 Some(datasynth_core::models::process_evolution::ToleranceChange {
296 old_tolerance: old_tol,
297 new_tolerance: new_tol,
298 tolerance_type: datasynth_core::models::process_evolution::ToleranceType::Absolute,
299 })
300 } else {
301 None
302 };
303
304 ProcessEvolutionType::ControlEnhancement(ControlEnhancementConfig {
305 control_id,
306 description: Some(format!(
307 "Enhanced control with {:.1}% error reduction",
308 error_reduction * 100.0
309 )),
310 tolerance_change,
311 error_reduction,
312 processing_time_impact,
313 implementation_months,
314 additional_evidence: Vec::new(),
315 })
316 }
317}
318
319#[cfg(test)]
320#[allow(clippy::unwrap_used)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_deterministic_generation() {
326 let mut gen1 = ProcessEvolutionGenerator::new(42);
327 let mut gen2 = ProcessEvolutionGenerator::new(42);
328 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
329 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
330
331 let events1 = gen1.generate_events(start, end);
332 let events2 = gen2.generate_events(start, end);
333
334 assert_eq!(events1.len(), events2.len());
335 for (e1, e2) in events1.iter().zip(events2.iter()) {
336 assert_eq!(e1.event_id, e2.event_id);
337 assert_eq!(e1.effective_date, e2.effective_date);
338 assert_eq!(e1.event_type.type_name(), e2.event_type.type_name());
339 }
340 }
341
342 #[test]
343 fn test_events_sorted_by_date() {
344 let mut gen = ProcessEvolutionGenerator::new(42);
345 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
346 let end = NaiveDate::from_ymd_opt(2025, 12, 31).unwrap();
347
348 let events = gen.generate_events(start, end);
349 for w in events.windows(2) {
350 assert!(w[0].effective_date <= w[1].effective_date);
351 }
352 }
353
354 #[test]
355 fn test_all_event_types_generated() {
356 let config = ProcEvoGeneratorConfig {
357 type_weights: [1.0, 1.0, 1.0, 1.0],
358 events_per_year: 100.0,
359 };
360 let mut gen = ProcessEvolutionGenerator::with_config(42, config);
361 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
362 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
363
364 let events = gen.generate_events(start, end);
365
366 let has_workflow = events.iter().any(|e| {
367 matches!(
368 e.event_type,
369 ProcessEvolutionType::ApprovalWorkflowChange(_)
370 )
371 });
372 let has_automation = events
373 .iter()
374 .any(|e| matches!(e.event_type, ProcessEvolutionType::ProcessAutomation(_)));
375 let has_policy = events
376 .iter()
377 .any(|e| matches!(e.event_type, ProcessEvolutionType::PolicyChange(_)));
378 let has_control = events
379 .iter()
380 .any(|e| matches!(e.event_type, ProcessEvolutionType::ControlEnhancement(_)));
381
382 assert!(has_workflow, "should generate workflow changes");
383 assert!(has_automation, "should generate automation events");
384 assert!(has_policy, "should generate policy changes");
385 assert!(has_control, "should generate control enhancements");
386 }
387
388 #[test]
389 fn test_events_within_date_range() {
390 let mut gen = ProcessEvolutionGenerator::new(42);
391 let start = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
392 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
393
394 let events = gen.generate_events(start, end);
395 for e in &events {
396 assert!(e.effective_date >= start, "event date before start");
397 assert!(e.effective_date <= end, "event date after end");
398 }
399 }
400
401 #[test]
402 fn test_workflow_change_valid_transitions() {
403 let config = ProcEvoGeneratorConfig {
404 type_weights: [1.0, 0.0, 0.0, 0.0],
405 events_per_year: 50.0,
406 };
407 let mut gen = ProcessEvolutionGenerator::with_config(42, config);
408 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
409 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
410
411 let events = gen.generate_events(start, end);
412 for e in &events {
413 if let ProcessEvolutionType::ApprovalWorkflowChange(ref c) = e.event_type {
414 assert_ne!(c.from, c.to, "from and to workflow types must differ");
415 } else {
416 panic!("expected only workflow change events");
417 }
418 }
419 }
420
421 #[test]
422 fn test_automation_config_populated() {
423 let config = ProcEvoGeneratorConfig {
424 type_weights: [0.0, 1.0, 0.0, 0.0],
425 events_per_year: 20.0,
426 };
427 let mut gen = ProcessEvolutionGenerator::with_config(42, config);
428 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
429 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
430
431 let events = gen.generate_events(start, end);
432 for e in &events {
433 if let ProcessEvolutionType::ProcessAutomation(ref c) = e.event_type {
434 assert!(
435 !c.process_name.is_empty(),
436 "process_name should not be empty"
437 );
438 assert!(
439 c.manual_rate_before >= 0.60 && c.manual_rate_before <= 0.90,
440 "manual_rate_before out of range: {}",
441 c.manual_rate_before
442 );
443 assert!(
444 c.manual_rate_after >= 0.05 && c.manual_rate_after <= 0.25,
445 "manual_rate_after out of range: {}",
446 c.manual_rate_after
447 );
448 assert!(
449 c.manual_rate_before > c.manual_rate_after,
450 "manual_rate_before should exceed manual_rate_after"
451 );
452 } else {
453 panic!("expected only automation events");
454 }
455 }
456 }
457
458 #[test]
459 fn test_s_curve_automation_progression() {
460 let config = ProcEvoGeneratorConfig {
461 type_weights: [0.0, 1.0, 0.0, 0.0],
462 events_per_year: 20.0,
463 };
464 let mut gen = ProcessEvolutionGenerator::with_config(42, config);
465 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
466 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
467
468 let events = gen.generate_events(start, end);
469
470 let s_curve_event = events.iter().find(|e| {
472 if let ProcessEvolutionType::ProcessAutomation(ref c) = e.event_type {
473 c.rollout_curve == RolloutCurve::SCurve
474 } else {
475 false
476 }
477 });
478
479 if let Some(evt) = s_curve_event {
482 if let ProcessEvolutionType::ProcessAutomation(ref c) = evt.event_type {
483 let rate_0 = c.automation_rate_at_progress(0.0);
484 let rate_25 = c.automation_rate_at_progress(0.25);
485 let rate_50 = c.automation_rate_at_progress(0.5);
486 let rate_75 = c.automation_rate_at_progress(0.75);
487 let rate_100 = c.automation_rate_at_progress(1.0);
488
489 assert!(rate_0 <= rate_25, "rate should increase from 0% to 25%");
491 assert!(rate_25 <= rate_50, "rate should increase from 25% to 50%");
492 assert!(rate_50 <= rate_75, "rate should increase from 50% to 75%");
493 assert!(rate_75 <= rate_100, "rate should increase from 75% to 100%");
494
495 let delta_first_quarter = rate_25 - rate_0;
497 let delta_second_quarter = rate_50 - rate_25;
498 let delta_last_quarter = rate_100 - rate_75;
499
500 assert!(
501 delta_second_quarter > delta_first_quarter,
502 "S-curve: middle growth ({}) should exceed early growth ({})",
503 delta_second_quarter,
504 delta_first_quarter
505 );
506 assert!(
507 delta_second_quarter > delta_last_quarter,
508 "S-curve: middle growth ({}) should exceed late growth ({})",
509 delta_second_quarter,
510 delta_last_quarter
511 );
512 }
513 } else {
514 let manual_config = ProcessAutomationConfig {
516 manual_rate_before: 0.80,
517 manual_rate_after: 0.10,
518 rollout_curve: RolloutCurve::SCurve,
519 ..Default::default()
520 };
521 let rate_0 = manual_config.automation_rate_at_progress(0.0);
522 let rate_50 = manual_config.automation_rate_at_progress(0.5);
523 let rate_100 = manual_config.automation_rate_at_progress(1.0);
524 assert!(rate_0 < rate_50);
525 assert!(rate_50 < rate_100);
526 }
527 }
528}