1use chrono::NaiveDate;
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "type", rename_all = "snake_case")]
16pub enum ProcessEvolutionType {
17 ApprovalWorkflowChange(ApprovalWorkflowChangeConfig),
19 ProcessAutomation(ProcessAutomationConfig),
21 PolicyChange(PolicyChangeConfig),
23 ControlEnhancement(ControlEnhancementConfig),
25}
26
27impl ProcessEvolutionType {
28 pub fn type_name(&self) -> &'static str {
30 match self {
31 Self::ApprovalWorkflowChange(_) => "approval_workflow_change",
32 Self::ProcessAutomation(_) => "process_automation",
33 Self::PolicyChange(_) => "policy_change",
34 Self::ControlEnhancement(_) => "control_enhancement",
35 }
36 }
37
38 pub fn processing_time_factor(&self) -> f64 {
40 match self {
41 Self::ApprovalWorkflowChange(c) => c.time_delta,
42 Self::ProcessAutomation(c) => c.processing_time_reduction,
43 Self::PolicyChange(_) => 1.0, Self::ControlEnhancement(c) => c.processing_time_impact,
45 }
46 }
47
48 pub fn error_rate_impact(&self) -> f64 {
50 match self {
51 Self::ApprovalWorkflowChange(c) => c.error_rate_impact,
52 Self::ProcessAutomation(c) => c.error_rate_after - c.error_rate_before,
53 Self::PolicyChange(c) => c.transition_error_rate,
54 Self::ControlEnhancement(c) => -c.error_reduction, }
56 }
57
58 pub fn transition_months(&self) -> u32 {
60 match self {
61 Self::ApprovalWorkflowChange(c) => c.transition_months,
62 Self::ProcessAutomation(c) => c.rollout_months,
63 Self::PolicyChange(c) => c.transition_months,
64 Self::ControlEnhancement(c) => c.implementation_months,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
71#[serde(rename_all = "snake_case")]
72pub enum WorkflowType {
73 #[default]
75 SingleApprover,
76 DualApproval,
78 MultiLevel,
80 Automated,
82 Matrix,
84 Parallel,
86}
87
88impl WorkflowType {
89 pub fn processing_time_multiplier(&self) -> f64 {
91 match self {
92 Self::SingleApprover => 1.0,
93 Self::DualApproval => 1.5,
94 Self::MultiLevel => 2.5,
95 Self::Automated => 0.2,
96 Self::Matrix => 2.0,
97 Self::Parallel => 1.2,
98 }
99 }
100
101 pub fn error_detection_rate(&self) -> f64 {
103 match self {
104 Self::SingleApprover => 0.70,
105 Self::DualApproval => 0.85,
106 Self::MultiLevel => 0.90,
107 Self::Automated => 0.95,
108 Self::Matrix => 0.88,
109 Self::Parallel => 0.82,
110 }
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ApprovalWorkflowChangeConfig {
117 pub from: WorkflowType,
119 pub to: WorkflowType,
121 #[serde(default = "default_time_delta")]
123 pub time_delta: f64,
124 #[serde(default = "default_workflow_error_impact")]
126 pub error_rate_impact: f64,
127 #[serde(default = "default_workflow_transition")]
129 pub transition_months: u32,
130 #[serde(default)]
132 pub threshold_changes: Vec<ThresholdChange>,
133}
134
135fn default_time_delta() -> f64 {
136 1.0
137}
138
139fn default_workflow_error_impact() -> f64 {
140 0.02
141}
142
143fn default_workflow_transition() -> u32 {
144 3
145}
146
147impl Default for ApprovalWorkflowChangeConfig {
148 fn default() -> Self {
149 Self {
150 from: WorkflowType::SingleApprover,
151 to: WorkflowType::DualApproval,
152 time_delta: 1.5,
153 error_rate_impact: 0.02,
154 transition_months: 3,
155 threshold_changes: Vec::new(),
156 }
157 }
158}
159
160impl ApprovalWorkflowChangeConfig {
161 pub fn calculated_time_delta(&self) -> f64 {
163 self.to.processing_time_multiplier() / self.from.processing_time_multiplier()
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct ThresholdChange {
170 pub category: String,
172 #[serde(with = "rust_decimal::serde::str")]
174 pub old_threshold: Decimal,
175 #[serde(with = "rust_decimal::serde::str")]
177 pub new_threshold: Decimal,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ProcessAutomationConfig {
183 pub process_name: String,
185 #[serde(default = "default_manual_rate_before")]
187 pub manual_rate_before: f64,
188 #[serde(default = "default_manual_rate_after")]
190 pub manual_rate_after: f64,
191 #[serde(default = "default_error_rate_before")]
193 pub error_rate_before: f64,
194 #[serde(default = "default_error_rate_after")]
196 pub error_rate_after: f64,
197 #[serde(default = "default_processing_reduction")]
199 pub processing_time_reduction: f64,
200 #[serde(default = "default_rollout_months")]
202 pub rollout_months: u32,
203 #[serde(default)]
205 pub rollout_curve: RolloutCurve,
206 #[serde(default)]
208 pub affected_transaction_types: Vec<String>,
209}
210
211fn default_manual_rate_before() -> f64 {
212 0.80
213}
214
215fn default_manual_rate_after() -> f64 {
216 0.15
217}
218
219fn default_error_rate_before() -> f64 {
220 0.05
221}
222
223fn default_error_rate_after() -> f64 {
224 0.01
225}
226
227fn default_processing_reduction() -> f64 {
228 0.30
229}
230
231fn default_rollout_months() -> u32 {
232 6
233}
234
235impl Default for ProcessAutomationConfig {
236 fn default() -> Self {
237 Self {
238 process_name: "three_way_match".to_string(),
239 manual_rate_before: 0.80,
240 manual_rate_after: 0.15,
241 error_rate_before: 0.05,
242 error_rate_after: 0.01,
243 processing_time_reduction: 0.30,
244 rollout_months: 6,
245 rollout_curve: RolloutCurve::SCurve,
246 affected_transaction_types: Vec::new(),
247 }
248 }
249}
250
251impl ProcessAutomationConfig {
252 pub fn automation_rate_at_progress(&self, progress: f64) -> f64 {
254 let target_automation = 1.0 - self.manual_rate_after;
255 let starting_automation = 1.0 - self.manual_rate_before;
256 let range = target_automation - starting_automation;
257
258 match self.rollout_curve {
259 RolloutCurve::Linear => starting_automation + range * progress,
260 RolloutCurve::SCurve => {
261 let steepness = 8.0;
263 let midpoint = 0.5;
264 let s_value = 1.0 / (1.0 + (-steepness * (progress - midpoint)).exp());
265 starting_automation + range * s_value
266 }
267 RolloutCurve::Exponential => {
268 starting_automation + range * (1.0 - (-3.0 * progress).exp())
270 }
271 RolloutCurve::Step => {
272 if progress >= 1.0 {
273 target_automation
274 } else {
275 starting_automation
276 }
277 }
278 }
279 }
280
281 pub fn error_rate_at_progress(&self, progress: f64) -> f64 {
283 let automation_progress = self.automation_rate_at_progress(progress);
285 let target_automation = 1.0 - self.manual_rate_after;
286 let starting_automation = 1.0 - self.manual_rate_before;
287
288 if (target_automation - starting_automation).abs() < 0.001 {
289 return self.error_rate_before;
290 }
291
292 let automation_fraction =
293 (automation_progress - starting_automation) / (target_automation - starting_automation);
294 self.error_rate_before
295 + (self.error_rate_after - self.error_rate_before) * automation_fraction
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
301#[serde(rename_all = "snake_case")]
302pub enum RolloutCurve {
303 Linear,
305 #[default]
307 SCurve,
308 Exponential,
310 Step,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct PolicyChangeConfig {
317 pub category: PolicyCategory,
319 #[serde(default)]
321 pub description: Option<String>,
322 #[serde(default, with = "rust_decimal::serde::str_option")]
324 pub old_value: Option<Decimal>,
325 #[serde(default, with = "rust_decimal::serde::str_option")]
327 pub new_value: Option<Decimal>,
328 #[serde(default = "default_policy_transition_error")]
330 pub transition_error_rate: f64,
331 #[serde(default = "default_policy_transition")]
333 pub transition_months: u32,
334 #[serde(default)]
336 pub affected_controls: Vec<String>,
337}
338
339fn default_policy_transition_error() -> f64 {
340 0.03
341}
342
343fn default_policy_transition() -> u32 {
344 3
345}
346
347impl Default for PolicyChangeConfig {
348 fn default() -> Self {
349 Self {
350 category: PolicyCategory::ApprovalThreshold,
351 description: None,
352 old_value: None,
353 new_value: None,
354 transition_error_rate: 0.03,
355 transition_months: 3,
356 affected_controls: Vec::new(),
357 }
358 }
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
363#[serde(rename_all = "snake_case")]
364pub enum PolicyCategory {
365 #[default]
367 ApprovalThreshold,
368 ExpensePolicy,
370 TravelPolicy,
372 ProcurementPolicy,
374 CreditPolicy,
376 InventoryPolicy,
378 DocumentationRequirement,
380 Other,
382}
383
384impl PolicyCategory {
385 pub fn code(&self) -> &'static str {
387 match self {
388 Self::ApprovalThreshold => "APPR",
389 Self::ExpensePolicy => "EXPS",
390 Self::TravelPolicy => "TRVL",
391 Self::ProcurementPolicy => "PROC",
392 Self::CreditPolicy => "CRED",
393 Self::InventoryPolicy => "INVT",
394 Self::DocumentationRequirement => "DOCS",
395 Self::Other => "OTHR",
396 }
397 }
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct ControlEnhancementConfig {
403 pub control_id: String,
405 #[serde(default)]
407 pub description: Option<String>,
408 #[serde(default)]
410 pub tolerance_change: Option<ToleranceChange>,
411 #[serde(default = "default_error_reduction")]
413 pub error_reduction: f64,
414 #[serde(default = "default_processing_impact")]
416 pub processing_time_impact: f64,
417 #[serde(default = "default_implementation_months")]
419 pub implementation_months: u32,
420 #[serde(default)]
422 pub additional_evidence: Vec<String>,
423}
424
425fn default_error_reduction() -> f64 {
426 0.02
427}
428
429fn default_processing_impact() -> f64 {
430 1.05
431}
432
433fn default_implementation_months() -> u32 {
434 2
435}
436
437impl Default for ControlEnhancementConfig {
438 fn default() -> Self {
439 Self {
440 control_id: String::new(),
441 description: None,
442 tolerance_change: None,
443 error_reduction: 0.02,
444 processing_time_impact: 1.05,
445 implementation_months: 2,
446 additional_evidence: Vec::new(),
447 }
448 }
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct ToleranceChange {
454 #[serde(with = "rust_decimal::serde::str")]
456 pub old_tolerance: Decimal,
457 #[serde(with = "rust_decimal::serde::str")]
459 pub new_tolerance: Decimal,
460 #[serde(default)]
462 pub tolerance_type: ToleranceType,
463}
464
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
467#[serde(rename_all = "snake_case")]
468pub enum ToleranceType {
469 #[default]
471 Absolute,
472 Percentage,
474 Count,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct ProcessEvolutionEvent {
481 pub event_id: String,
483 pub event_type: ProcessEvolutionType,
485 pub effective_date: NaiveDate,
487 #[serde(default)]
489 pub description: Option<String>,
490 #[serde(default)]
492 pub tags: Vec<String>,
493}
494
495impl ProcessEvolutionEvent {
496 pub fn new(
498 event_id: impl Into<String>,
499 event_type: ProcessEvolutionType,
500 effective_date: NaiveDate,
501 ) -> Self {
502 Self {
503 event_id: event_id.into(),
504 event_type,
505 effective_date,
506 description: None,
507 tags: Vec::new(),
508 }
509 }
510
511 pub fn is_active_at(&self, date: NaiveDate) -> bool {
513 if date < self.effective_date {
514 return false;
515 }
516 let transition_months = self.event_type.transition_months();
517 let end_date = self.effective_date + chrono::Duration::days(transition_months as i64 * 30);
518 date <= end_date
519 }
520
521 pub fn progress_at(&self, date: NaiveDate) -> f64 {
523 if date < self.effective_date {
524 return 0.0;
525 }
526 let transition_months = self.event_type.transition_months();
527 if transition_months == 0 {
528 return 1.0;
529 }
530 let days_elapsed = (date - self.effective_date).num_days() as f64;
531 let total_days = transition_months as f64 * 30.0;
532 (days_elapsed / total_days).min(1.0)
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn test_workflow_type_multipliers() {
542 assert!((WorkflowType::SingleApprover.processing_time_multiplier() - 1.0).abs() < 0.001);
543 assert!(WorkflowType::DualApproval.processing_time_multiplier() > 1.0);
544 assert!(WorkflowType::Automated.processing_time_multiplier() < 1.0);
545 }
546
547 #[test]
548 fn test_process_automation_s_curve() {
549 let config = ProcessAutomationConfig {
550 manual_rate_before: 0.80,
551 manual_rate_after: 0.20,
552 ..Default::default()
553 };
554
555 let start = config.automation_rate_at_progress(0.0);
556 let mid = config.automation_rate_at_progress(0.5);
557 let end = config.automation_rate_at_progress(1.0);
558
559 assert!(start < mid);
561 assert!(mid < end);
562 assert!((end - 0.80).abs() < 0.02); }
564
565 #[test]
566 fn test_process_evolution_event_progress() {
567 let config = ProcessAutomationConfig {
568 rollout_months: 6,
569 ..Default::default()
570 };
571
572 let event = ProcessEvolutionEvent::new(
573 "AUTO-001",
574 ProcessEvolutionType::ProcessAutomation(config),
575 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
576 );
577
578 assert!(!event.is_active_at(NaiveDate::from_ymd_opt(2023, 12, 1).unwrap()));
580
581 let during = NaiveDate::from_ymd_opt(2024, 4, 1).unwrap();
583 assert!(event.is_active_at(during));
584 let progress = event.progress_at(during);
585 assert!(progress > 0.4 && progress < 0.6);
586
587 let after = NaiveDate::from_ymd_opt(2024, 12, 1).unwrap();
589 assert!(!event.is_active_at(after));
590 assert!((event.progress_at(after) - 1.0).abs() < 0.001);
591 }
592
593 #[test]
594 fn test_approval_workflow_time_delta() {
595 let config = ApprovalWorkflowChangeConfig {
596 from: WorkflowType::SingleApprover,
597 to: WorkflowType::Automated,
598 ..Default::default()
599 };
600
601 let calculated = config.calculated_time_delta();
602 assert!(calculated < 1.0); }
604}