Skip to main content

datasynth_core/models/
process_evolution.rs

1//! Process evolution models for pattern drift simulation.
2//!
3//! Provides comprehensive process change modeling including:
4//! - Approval workflow changes
5//! - Process automation transitions
6//! - Policy changes
7//! - Control enhancements
8
9use chrono::NaiveDate;
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12
13/// Process evolution event type with associated configuration.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "type", rename_all = "snake_case")]
16pub enum ProcessEvolutionType {
17    /// Change in approval workflow.
18    ApprovalWorkflowChange(ApprovalWorkflowChangeConfig),
19    /// Automation of a previously manual process.
20    ProcessAutomation(ProcessAutomationConfig),
21    /// Policy change affecting processes.
22    PolicyChange(PolicyChangeConfig),
23    /// Enhancement to existing controls.
24    ControlEnhancement(ControlEnhancementConfig),
25}
26
27impl ProcessEvolutionType {
28    /// Get the event type name.
29    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    /// Get the processing time impact factor.
39    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, // No direct impact
44            Self::ControlEnhancement(c) => c.processing_time_impact,
45        }
46    }
47
48    /// Get the error rate impact.
49    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, // Negative because it reduces errors
55        }
56    }
57
58    /// Get the transition duration in months.
59    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/// Workflow type for approval processes.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
71#[serde(rename_all = "snake_case")]
72pub enum WorkflowType {
73    /// Single approver workflow.
74    #[default]
75    SingleApprover,
76    /// Dual approval (maker-checker).
77    DualApproval,
78    /// Multi-level approval chain.
79    MultiLevel,
80    /// Automated approval with rules.
81    Automated,
82    /// Matrix approval (multiple dimensions).
83    Matrix,
84    /// Parallel approval (multiple concurrent).
85    Parallel,
86}
87
88impl WorkflowType {
89    /// Get typical processing time multiplier relative to single approver.
90    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    /// Get typical error detection rate improvement.
102    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/// Configuration for approval workflow change.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ApprovalWorkflowChangeConfig {
117    /// Previous workflow type.
118    pub from: WorkflowType,
119    /// New workflow type.
120    pub to: WorkflowType,
121    /// Processing time change factor (e.g., 0.8 = 20% faster).
122    #[serde(default = "default_time_delta")]
123    pub time_delta: f64,
124    /// Impact on error rate during transition.
125    #[serde(default = "default_workflow_error_impact")]
126    pub error_rate_impact: f64,
127    /// Number of months for transition.
128    #[serde(default = "default_workflow_transition")]
129    pub transition_months: u32,
130    /// Threshold changes associated with the workflow change.
131    #[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    /// Calculate the time delta from workflow types if not explicitly set.
162    pub fn calculated_time_delta(&self) -> f64 {
163        self.to.processing_time_multiplier() / self.from.processing_time_multiplier()
164    }
165}
166
167/// Threshold change details.
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct ThresholdChange {
170    /// Threshold category (e.g., "amount", "risk_level").
171    pub category: String,
172    /// Old threshold value.
173    #[serde(with = "rust_decimal::serde::str")]
174    pub old_threshold: Decimal,
175    /// New threshold value.
176    #[serde(with = "rust_decimal::serde::str")]
177    pub new_threshold: Decimal,
178}
179
180/// Configuration for process automation.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ProcessAutomationConfig {
183    /// Name of the process being automated.
184    pub process_name: String,
185    /// Manual processing rate before automation (0.0 to 1.0).
186    #[serde(default = "default_manual_rate_before")]
187    pub manual_rate_before: f64,
188    /// Manual processing rate after automation (0.0 to 1.0).
189    #[serde(default = "default_manual_rate_after")]
190    pub manual_rate_after: f64,
191    /// Error rate before automation.
192    #[serde(default = "default_error_rate_before")]
193    pub error_rate_before: f64,
194    /// Error rate after automation.
195    #[serde(default = "default_error_rate_after")]
196    pub error_rate_after: f64,
197    /// Processing time reduction factor (e.g., 0.3 = 70% faster).
198    #[serde(default = "default_processing_reduction")]
199    pub processing_time_reduction: f64,
200    /// Number of months for rollout.
201    #[serde(default = "default_rollout_months")]
202    pub rollout_months: u32,
203    /// Automation rollout curve type.
204    #[serde(default)]
205    pub rollout_curve: RolloutCurve,
206    /// Affected transaction types.
207    #[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    /// Calculate automation rate at a given point in rollout (0.0 to 1.0 progress).
253    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                // Logistic S-curve
262                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                // Exponential approach to target
269                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    /// Calculate error rate at a given point in rollout.
282    pub fn error_rate_at_progress(&self, progress: f64) -> f64 {
283        // Error rate typically follows the automation rate
284        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/// Rollout curve type for automation adoption.
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
301#[serde(rename_all = "snake_case")]
302pub enum RolloutCurve {
303    /// Linear adoption.
304    Linear,
305    /// S-curve adoption (slow start, fast middle, slow end).
306    #[default]
307    SCurve,
308    /// Exponential adoption (fast start, slowing).
309    Exponential,
310    /// Step function (immediate switch).
311    Step,
312}
313
314/// Configuration for policy change.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct PolicyChangeConfig {
317    /// Policy category affected.
318    pub category: PolicyCategory,
319    /// Description of the change.
320    #[serde(default)]
321    pub description: Option<String>,
322    /// Old policy value (for threshold-based policies).
323    #[serde(default, with = "rust_decimal::serde::str_option")]
324    pub old_value: Option<Decimal>,
325    /// New policy value (for threshold-based policies).
326    #[serde(default, with = "rust_decimal::serde::str_option")]
327    pub new_value: Option<Decimal>,
328    /// Error rate during transition period.
329    #[serde(default = "default_policy_transition_error")]
330    pub transition_error_rate: f64,
331    /// Number of months for transition.
332    #[serde(default = "default_policy_transition")]
333    pub transition_months: u32,
334    /// Controls affected by this policy change.
335    #[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/// Policy category.
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
363#[serde(rename_all = "snake_case")]
364pub enum PolicyCategory {
365    /// Approval thresholds.
366    #[default]
367    ApprovalThreshold,
368    /// Expense policies.
369    ExpensePolicy,
370    /// Travel policies.
371    TravelPolicy,
372    /// Procurement policies.
373    ProcurementPolicy,
374    /// Credit policies.
375    CreditPolicy,
376    /// Inventory policies.
377    InventoryPolicy,
378    /// Documentation requirements.
379    DocumentationRequirement,
380    /// Other policy.
381    Other,
382}
383
384impl PolicyCategory {
385    /// Get the category code.
386    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/// Configuration for control enhancement.
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct ControlEnhancementConfig {
403    /// Control ID being enhanced.
404    pub control_id: String,
405    /// Description of enhancement.
406    #[serde(default)]
407    pub description: Option<String>,
408    /// Tolerance change.
409    #[serde(default)]
410    pub tolerance_change: Option<ToleranceChange>,
411    /// Error reduction achieved (e.g., 0.02 = 2% fewer errors).
412    #[serde(default = "default_error_reduction")]
413    pub error_reduction: f64,
414    /// Processing time impact (e.g., 1.1 = 10% slower due to more checks).
415    #[serde(default = "default_processing_impact")]
416    pub processing_time_impact: f64,
417    /// Number of months for implementation.
418    #[serde(default = "default_implementation_months")]
419    pub implementation_months: u32,
420    /// Additional evidence requirements.
421    #[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/// Tolerance change for controls.
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct ToleranceChange {
454    /// Old tolerance value.
455    #[serde(with = "rust_decimal::serde::str")]
456    pub old_tolerance: Decimal,
457    /// New tolerance value.
458    #[serde(with = "rust_decimal::serde::str")]
459    pub new_tolerance: Decimal,
460    /// Tolerance type.
461    #[serde(default)]
462    pub tolerance_type: ToleranceType,
463}
464
465/// Type of tolerance.
466#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
467#[serde(rename_all = "snake_case")]
468pub enum ToleranceType {
469    /// Absolute amount tolerance.
470    #[default]
471    Absolute,
472    /// Percentage tolerance.
473    Percentage,
474    /// Count tolerance.
475    Count,
476}
477
478/// A scheduled process evolution event.
479#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct ProcessEvolutionEvent {
481    /// Unique event ID.
482    pub event_id: String,
483    /// Event type and configuration.
484    pub event_type: ProcessEvolutionType,
485    /// Effective date of the event.
486    pub effective_date: NaiveDate,
487    /// Human-readable description.
488    #[serde(default)]
489    pub description: Option<String>,
490    /// Tags for categorization.
491    #[serde(default)]
492    pub tags: Vec<String>,
493}
494
495impl ProcessEvolutionEvent {
496    /// Create a new process evolution event.
497    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    /// Check if the event is active at a given date.
512    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    /// Get the progress through the event (0.0 to 1.0).
522    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        // S-curve should show slow start, fast middle, slow end
560        assert!(start < mid);
561        assert!(mid < end);
562        assert!((end - 0.80).abs() < 0.02); // Should be near target automation (with small tolerance for S-curve)
563    }
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        // Before event
579        assert!(!event.is_active_at(NaiveDate::from_ymd_opt(2023, 12, 1).unwrap()));
580
581        // During event (3 months in = 50% progress)
582        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        // After event
588        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); // Automated should be faster
603    }
604}