use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InterventionType {
EntityEvent(EntityEventIntervention),
ParameterShift(ParameterShiftIntervention),
ControlFailure(ControlFailureIntervention),
ProcessChange(ProcessChangeIntervention),
MacroShock(MacroShockIntervention),
RegulatoryChange(RegulatoryChangeIntervention),
Composite(CompositeIntervention),
Custom(CustomIntervention),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityEventIntervention {
pub subtype: InterventionEntityEvent,
pub target: EntityTarget,
#[serde(default)]
pub parameters: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InterventionEntityEvent {
VendorDefault,
CustomerChurn,
EmployeeDeparture,
NewVendorOnboarding,
MergerAcquisition,
VendorCollusion,
CustomerConsolidation,
KeyPersonRisk,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityTarget {
pub cluster: Option<String>,
pub entity_ids: Option<Vec<String>>,
pub filter: Option<HashMap<String, String>>,
pub count: Option<u32>,
pub fraction: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterShiftIntervention {
pub target: String,
pub from: Option<serde_json::Value>,
pub to: serde_json::Value,
#[serde(default)]
pub interpolation: InterpolationType,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum InterpolationType {
#[default]
Linear,
Exponential,
Logistic {
steepness: f64,
},
Step,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ControlFailureIntervention {
pub subtype: ControlFailureType,
pub control_target: ControlTarget,
pub severity: f64,
#[serde(default)]
pub detectable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ControlFailureType {
EffectivenessReduction,
CompleteBypass,
IntermittentFailure { failure_probability: f64 },
DelayedDetection { detection_lag_months: u32 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ControlTarget {
ById { control_id: String },
ByCategory { coso_component: String },
ByScope { scope: String },
All,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessChangeIntervention {
pub subtype: ProcessChangeType,
#[serde(default)]
pub parameters: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProcessChangeType {
ApprovalThresholdChange,
NewApprovalLevel,
SystemMigration,
ProcessAutomation,
OutsourcingTransition,
PolicyChange,
ReorganizationRestructuring,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MacroShockIntervention {
pub subtype: MacroShockType,
pub severity: f64,
pub preset: Option<String>,
#[serde(default)]
pub overrides: HashMap<String, f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MacroShockType {
Recession,
InflationSpike,
CurrencyCrisis,
InterestRateShock,
CommodityShock,
PandemicDisruption,
SupplyChainCrisis,
CreditCrunch,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegulatoryChangeIntervention {
pub subtype: RegulatoryChangeType,
#[serde(default)]
pub parameters: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RegulatoryChangeType {
NewStandardAdoption,
MaterialityThresholdChange,
ReportingRequirementChange,
ComplianceThresholdChange,
AuditStandardChange,
TaxRateChange,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompositeIntervention {
pub name: String,
pub description: String,
pub children: Vec<InterventionType>,
#[serde(default)]
pub conflict_resolution: ConflictResolution,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ConflictResolution {
#[default]
FirstWins,
LastWins,
Average,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomIntervention {
pub name: String,
#[serde(default)]
pub config_overrides: HashMap<String, serde_json::Value>,
#[serde(default)]
pub downstream_triggers: Vec<String>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_intervention_type_tagged_serde() {
let json = r#"{
"type": "parameter_shift",
"target": "transactions.count",
"from": 1000,
"to": 2000,
"interpolation": "linear"
}"#;
let intervention: InterventionType = serde_json::from_str(json).unwrap();
assert!(matches!(intervention, InterventionType::ParameterShift(_)));
let serialized = serde_json::to_string(&intervention).unwrap();
let deserialized: InterventionType = serde_json::from_str(&serialized).unwrap();
assert!(matches!(deserialized, InterventionType::ParameterShift(_)));
}
#[test]
fn test_entity_event_serde() {
let json = r#"{
"type": "entity_event",
"subtype": "vendor_default",
"target": {
"cluster": "problematic",
"count": 5
},
"parameters": {}
}"#;
let intervention: InterventionType = serde_json::from_str(json).unwrap();
if let InterventionType::EntityEvent(e) = intervention {
assert!(matches!(e.subtype, InterventionEntityEvent::VendorDefault));
assert_eq!(e.target.cluster, Some("problematic".to_string()));
assert_eq!(e.target.count, Some(5));
} else {
panic!("Expected EntityEvent");
}
}
#[test]
fn test_macro_shock_serde() {
let json = r#"{
"type": "macro_shock",
"subtype": "recession",
"severity": 1.5,
"preset": "2008_financial_crisis",
"overrides": {"gdp_growth": -0.03}
}"#;
let intervention: InterventionType = serde_json::from_str(json).unwrap();
if let InterventionType::MacroShock(m) = intervention {
assert!(matches!(m.subtype, MacroShockType::Recession));
assert_eq!(m.severity, 1.5);
assert_eq!(m.preset, Some("2008_financial_crisis".to_string()));
} else {
panic!("Expected MacroShock");
}
}
#[test]
fn test_control_failure_serde() {
let json = r#"{
"type": "control_failure",
"subtype": "complete_bypass",
"control_target": {"control_id": "C003"},
"severity": 0.0,
"detectable": false
}"#;
let intervention: InterventionType = serde_json::from_str(json).unwrap();
assert!(matches!(intervention, InterventionType::ControlFailure(_)));
}
#[test]
fn test_composite_serde() {
let json = r#"{
"type": "composite",
"name": "recession_scenario",
"description": "Combined recession effects",
"children": [
{
"type": "macro_shock",
"subtype": "recession",
"severity": 1.0,
"overrides": {}
}
],
"conflict_resolution": "first_wins"
}"#;
let intervention: InterventionType = serde_json::from_str(json).unwrap();
if let InterventionType::Composite(c) = intervention {
assert_eq!(c.name, "recession_scenario");
assert_eq!(c.children.len(), 1);
} else {
panic!("Expected Composite");
}
}
#[test]
fn test_conflict_resolution_default() {
let cr = ConflictResolution::default();
assert!(matches!(cr, ConflictResolution::FirstWins));
}
}