use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ProcessEvolutionType {
ApprovalWorkflowChange(ApprovalWorkflowChangeConfig),
ProcessAutomation(ProcessAutomationConfig),
PolicyChange(PolicyChangeConfig),
ControlEnhancement(ControlEnhancementConfig),
}
impl ProcessEvolutionType {
pub fn type_name(&self) -> &'static str {
match self {
Self::ApprovalWorkflowChange(_) => "approval_workflow_change",
Self::ProcessAutomation(_) => "process_automation",
Self::PolicyChange(_) => "policy_change",
Self::ControlEnhancement(_) => "control_enhancement",
}
}
pub fn processing_time_factor(&self) -> f64 {
match self {
Self::ApprovalWorkflowChange(c) => c.time_delta,
Self::ProcessAutomation(c) => c.processing_time_reduction,
Self::PolicyChange(_) => 1.0, Self::ControlEnhancement(c) => c.processing_time_impact,
}
}
pub fn error_rate_impact(&self) -> f64 {
match self {
Self::ApprovalWorkflowChange(c) => c.error_rate_impact,
Self::ProcessAutomation(c) => c.error_rate_after - c.error_rate_before,
Self::PolicyChange(c) => c.transition_error_rate,
Self::ControlEnhancement(c) => -c.error_reduction, }
}
pub fn transition_months(&self) -> u32 {
match self {
Self::ApprovalWorkflowChange(c) => c.transition_months,
Self::ProcessAutomation(c) => c.rollout_months,
Self::PolicyChange(c) => c.transition_months,
Self::ControlEnhancement(c) => c.implementation_months,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum WorkflowType {
#[default]
SingleApprover,
DualApproval,
MultiLevel,
Automated,
Matrix,
Parallel,
}
impl WorkflowType {
pub fn processing_time_multiplier(&self) -> f64 {
match self {
Self::SingleApprover => 1.0,
Self::DualApproval => 1.5,
Self::MultiLevel => 2.5,
Self::Automated => 0.2,
Self::Matrix => 2.0,
Self::Parallel => 1.2,
}
}
pub fn error_detection_rate(&self) -> f64 {
match self {
Self::SingleApprover => 0.70,
Self::DualApproval => 0.85,
Self::MultiLevel => 0.90,
Self::Automated => 0.95,
Self::Matrix => 0.88,
Self::Parallel => 0.82,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalWorkflowChangeConfig {
pub from: WorkflowType,
pub to: WorkflowType,
#[serde(default = "default_time_delta")]
pub time_delta: f64,
#[serde(default = "default_workflow_error_impact")]
pub error_rate_impact: f64,
#[serde(default = "default_workflow_transition")]
pub transition_months: u32,
#[serde(default)]
pub threshold_changes: Vec<ThresholdChange>,
}
fn default_time_delta() -> f64 {
1.0
}
fn default_workflow_error_impact() -> f64 {
0.02
}
fn default_workflow_transition() -> u32 {
3
}
impl Default for ApprovalWorkflowChangeConfig {
fn default() -> Self {
Self {
from: WorkflowType::SingleApprover,
to: WorkflowType::DualApproval,
time_delta: 1.5,
error_rate_impact: 0.02,
transition_months: 3,
threshold_changes: Vec::new(),
}
}
}
impl ApprovalWorkflowChangeConfig {
pub fn calculated_time_delta(&self) -> f64 {
self.to.processing_time_multiplier() / self.from.processing_time_multiplier()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThresholdChange {
pub category: String,
#[serde(with = "crate::serde_decimal")]
pub old_threshold: Decimal,
#[serde(with = "crate::serde_decimal")]
pub new_threshold: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessAutomationConfig {
pub process_name: String,
#[serde(default = "default_manual_rate_before")]
pub manual_rate_before: f64,
#[serde(default = "default_manual_rate_after")]
pub manual_rate_after: f64,
#[serde(default = "default_error_rate_before")]
pub error_rate_before: f64,
#[serde(default = "default_error_rate_after")]
pub error_rate_after: f64,
#[serde(default = "default_processing_reduction")]
pub processing_time_reduction: f64,
#[serde(default = "default_rollout_months")]
pub rollout_months: u32,
#[serde(default)]
pub rollout_curve: RolloutCurve,
#[serde(default)]
pub affected_transaction_types: Vec<String>,
}
fn default_manual_rate_before() -> f64 {
0.80
}
fn default_manual_rate_after() -> f64 {
0.15
}
fn default_error_rate_before() -> f64 {
0.05
}
fn default_error_rate_after() -> f64 {
0.01
}
fn default_processing_reduction() -> f64 {
0.30
}
fn default_rollout_months() -> u32 {
6
}
impl Default for ProcessAutomationConfig {
fn default() -> Self {
Self {
process_name: "three_way_match".to_string(),
manual_rate_before: 0.80,
manual_rate_after: 0.15,
error_rate_before: 0.05,
error_rate_after: 0.01,
processing_time_reduction: 0.30,
rollout_months: 6,
rollout_curve: RolloutCurve::SCurve,
affected_transaction_types: Vec::new(),
}
}
}
impl ProcessAutomationConfig {
pub fn automation_rate_at_progress(&self, progress: f64) -> f64 {
let target_automation = 1.0 - self.manual_rate_after;
let starting_automation = 1.0 - self.manual_rate_before;
let range = target_automation - starting_automation;
match self.rollout_curve {
RolloutCurve::Linear => starting_automation + range * progress,
RolloutCurve::SCurve => {
let steepness = 8.0;
let midpoint = 0.5;
let s_value = 1.0 / (1.0 + (-steepness * (progress - midpoint)).exp());
starting_automation + range * s_value
}
RolloutCurve::Exponential => {
starting_automation + range * (1.0 - (-3.0 * progress).exp())
}
RolloutCurve::Step => {
if progress >= 1.0 {
target_automation
} else {
starting_automation
}
}
}
}
pub fn error_rate_at_progress(&self, progress: f64) -> f64 {
let automation_progress = self.automation_rate_at_progress(progress);
let target_automation = 1.0 - self.manual_rate_after;
let starting_automation = 1.0 - self.manual_rate_before;
if (target_automation - starting_automation).abs() < 0.001 {
return self.error_rate_before;
}
let automation_fraction =
(automation_progress - starting_automation) / (target_automation - starting_automation);
self.error_rate_before
+ (self.error_rate_after - self.error_rate_before) * automation_fraction
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RolloutCurve {
Linear,
#[default]
SCurve,
Exponential,
Step,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyChangeConfig {
pub category: PolicyCategory,
#[serde(default)]
pub description: Option<String>,
#[serde(default, with = "crate::serde_decimal::option")]
pub old_value: Option<Decimal>,
#[serde(default, with = "crate::serde_decimal::option")]
pub new_value: Option<Decimal>,
#[serde(default = "default_policy_transition_error")]
pub transition_error_rate: f64,
#[serde(default = "default_policy_transition")]
pub transition_months: u32,
#[serde(default)]
pub affected_controls: Vec<String>,
}
fn default_policy_transition_error() -> f64 {
0.03
}
fn default_policy_transition() -> u32 {
3
}
impl Default for PolicyChangeConfig {
fn default() -> Self {
Self {
category: PolicyCategory::ApprovalThreshold,
description: None,
old_value: None,
new_value: None,
transition_error_rate: 0.03,
transition_months: 3,
affected_controls: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PolicyCategory {
#[default]
ApprovalThreshold,
ExpensePolicy,
TravelPolicy,
ProcurementPolicy,
CreditPolicy,
InventoryPolicy,
DocumentationRequirement,
Other,
}
impl PolicyCategory {
pub fn code(&self) -> &'static str {
match self {
Self::ApprovalThreshold => "APPR",
Self::ExpensePolicy => "EXPS",
Self::TravelPolicy => "TRVL",
Self::ProcurementPolicy => "PROC",
Self::CreditPolicy => "CRED",
Self::InventoryPolicy => "INVT",
Self::DocumentationRequirement => "DOCS",
Self::Other => "OTHR",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ControlEnhancementConfig {
pub control_id: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub tolerance_change: Option<ToleranceChange>,
#[serde(default = "default_error_reduction")]
pub error_reduction: f64,
#[serde(default = "default_processing_impact")]
pub processing_time_impact: f64,
#[serde(default = "default_implementation_months")]
pub implementation_months: u32,
#[serde(default)]
pub additional_evidence: Vec<String>,
}
fn default_error_reduction() -> f64 {
0.02
}
fn default_processing_impact() -> f64 {
1.05
}
fn default_implementation_months() -> u32 {
2
}
impl Default for ControlEnhancementConfig {
fn default() -> Self {
Self {
control_id: String::new(),
description: None,
tolerance_change: None,
error_reduction: 0.02,
processing_time_impact: 1.05,
implementation_months: 2,
additional_evidence: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToleranceChange {
#[serde(with = "crate::serde_decimal")]
pub old_tolerance: Decimal,
#[serde(with = "crate::serde_decimal")]
pub new_tolerance: Decimal,
#[serde(default)]
pub tolerance_type: ToleranceType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ToleranceType {
#[default]
Absolute,
Percentage,
Count,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessEvolutionEvent {
pub event_id: String,
pub event_type: ProcessEvolutionType,
pub effective_date: NaiveDate,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
}
impl ProcessEvolutionEvent {
pub fn new(
event_id: impl Into<String>,
event_type: ProcessEvolutionType,
effective_date: NaiveDate,
) -> Self {
Self {
event_id: event_id.into(),
event_type,
effective_date,
description: None,
tags: Vec::new(),
}
}
pub fn is_active_at(&self, date: NaiveDate) -> bool {
if date < self.effective_date {
return false;
}
let transition_months = self.event_type.transition_months();
let end_date = self.effective_date + chrono::Duration::days(transition_months as i64 * 30);
date <= end_date
}
pub fn progress_at(&self, date: NaiveDate) -> f64 {
if date < self.effective_date {
return 0.0;
}
let transition_months = self.event_type.transition_months();
if transition_months == 0 {
return 1.0;
}
let days_elapsed = (date - self.effective_date).num_days() as f64;
let total_days = transition_months as f64 * 30.0;
(days_elapsed / total_days).min(1.0)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_workflow_type_multipliers() {
assert!((WorkflowType::SingleApprover.processing_time_multiplier() - 1.0).abs() < 0.001);
assert!(WorkflowType::DualApproval.processing_time_multiplier() > 1.0);
assert!(WorkflowType::Automated.processing_time_multiplier() < 1.0);
}
#[test]
fn test_process_automation_s_curve() {
let config = ProcessAutomationConfig {
manual_rate_before: 0.80,
manual_rate_after: 0.20,
..Default::default()
};
let start = config.automation_rate_at_progress(0.0);
let mid = config.automation_rate_at_progress(0.5);
let end = config.automation_rate_at_progress(1.0);
assert!(start < mid);
assert!(mid < end);
assert!((end - 0.80).abs() < 0.02); }
#[test]
fn test_process_evolution_event_progress() {
let config = ProcessAutomationConfig {
rollout_months: 6,
..Default::default()
};
let event = ProcessEvolutionEvent::new(
"AUTO-001",
ProcessEvolutionType::ProcessAutomation(config),
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
assert!(!event.is_active_at(NaiveDate::from_ymd_opt(2023, 12, 1).unwrap()));
let during = NaiveDate::from_ymd_opt(2024, 4, 1).unwrap();
assert!(event.is_active_at(during));
let progress = event.progress_at(during);
assert!(progress > 0.4 && progress < 0.6);
let after = NaiveDate::from_ymd_opt(2024, 12, 1).unwrap();
assert!(!event.is_active_at(after));
assert!((event.progress_at(after) - 1.0).abs() < 0.001);
}
#[test]
fn test_approval_workflow_time_delta() {
let config = ApprovalWorkflowChangeConfig {
from: WorkflowType::SingleApprover,
to: WorkflowType::Automated,
..Default::default()
};
let calculated = config.calculated_time_delta();
assert!(calculated < 1.0); }
}