1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(tag = "type", rename_all = "snake_case")]
7pub enum InterventionType {
8 EntityEvent(EntityEventIntervention),
10 ParameterShift(ParameterShiftIntervention),
12 ControlFailure(ControlFailureIntervention),
14 ProcessChange(ProcessChangeIntervention),
16 MacroShock(MacroShockIntervention),
18 RegulatoryChange(RegulatoryChangeIntervention),
20 Composite(CompositeIntervention),
22 Custom(CustomIntervention),
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct EntityEventIntervention {
30 pub subtype: InterventionEntityEvent,
31 pub target: EntityTarget,
32 #[serde(default)]
33 pub parameters: HashMap<String, serde_json::Value>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum InterventionEntityEvent {
39 VendorDefault,
40 CustomerChurn,
41 EmployeeDeparture,
42 NewVendorOnboarding,
43 MergerAcquisition,
44 VendorCollusion,
45 CustomerConsolidation,
46 KeyPersonRisk,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct EntityTarget {
51 pub cluster: Option<String>,
53 pub entity_ids: Option<Vec<String>>,
55 pub filter: Option<HashMap<String, String>>,
57 pub count: Option<u32>,
59 pub fraction: Option<f64>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct ParameterShiftIntervention {
67 pub target: String,
69 pub from: Option<serde_json::Value>,
71 pub to: serde_json::Value,
73 #[serde(default)]
75 pub interpolation: InterpolationType,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, Default)]
79#[serde(rename_all = "snake_case")]
80pub enum InterpolationType {
81 #[default]
82 Linear,
83 Exponential,
84 Logistic {
85 steepness: f64,
86 },
87 Step,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ControlFailureIntervention {
94 pub subtype: ControlFailureType,
95 pub control_target: ControlTarget,
97 pub severity: f64,
99 #[serde(default)]
101 pub detectable: bool,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum ControlFailureType {
107 EffectivenessReduction,
108 CompleteBypass,
109 IntermittentFailure { failure_probability: f64 },
110 DelayedDetection { detection_lag_months: u32 },
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(untagged)]
115pub enum ControlTarget {
116 ById { control_id: String },
117 ByCategory { coso_component: String },
118 ByScope { scope: String },
119 All,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ProcessChangeIntervention {
126 pub subtype: ProcessChangeType,
127 #[serde(default)]
128 pub parameters: HashMap<String, serde_json::Value>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum ProcessChangeType {
134 ApprovalThresholdChange,
135 NewApprovalLevel,
136 SystemMigration,
137 ProcessAutomation,
138 OutsourcingTransition,
139 PolicyChange,
140 ReorganizationRestructuring,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct MacroShockIntervention {
147 pub subtype: MacroShockType,
148 pub severity: f64,
150 pub preset: Option<String>,
152 #[serde(default)]
154 pub overrides: HashMap<String, f64>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum MacroShockType {
160 Recession,
161 InflationSpike,
162 CurrencyCrisis,
163 InterestRateShock,
164 CommodityShock,
165 PandemicDisruption,
166 SupplyChainCrisis,
167 CreditCrunch,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct RegulatoryChangeIntervention {
174 pub subtype: RegulatoryChangeType,
175 #[serde(default)]
176 pub parameters: HashMap<String, serde_json::Value>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum RegulatoryChangeType {
182 NewStandardAdoption,
183 MaterialityThresholdChange,
184 ReportingRequirementChange,
185 ComplianceThresholdChange,
186 AuditStandardChange,
187 TaxRateChange,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct CompositeIntervention {
195 pub name: String,
196 pub description: String,
197 pub children: Vec<InterventionType>,
199 #[serde(default)]
201 pub conflict_resolution: ConflictResolution,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, Default)]
205#[serde(rename_all = "snake_case")]
206pub enum ConflictResolution {
207 #[default]
208 FirstWins,
209 LastWins,
210 Average,
211 Error,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct CustomIntervention {
218 pub name: String,
219 #[serde(default)]
221 pub config_overrides: HashMap<String, serde_json::Value>,
222 #[serde(default)]
224 pub downstream_triggers: Vec<String>,
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_intervention_type_tagged_serde() {
233 let json = r#"{
234 "type": "parameter_shift",
235 "target": "transactions.count",
236 "from": 1000,
237 "to": 2000,
238 "interpolation": "linear"
239 }"#;
240 let intervention: InterventionType = serde_json::from_str(json).unwrap();
241 assert!(matches!(intervention, InterventionType::ParameterShift(_)));
242
243 let serialized = serde_json::to_string(&intervention).unwrap();
245 let deserialized: InterventionType = serde_json::from_str(&serialized).unwrap();
246 assert!(matches!(deserialized, InterventionType::ParameterShift(_)));
247 }
248
249 #[test]
250 fn test_entity_event_serde() {
251 let json = r#"{
252 "type": "entity_event",
253 "subtype": "vendor_default",
254 "target": {
255 "cluster": "problematic",
256 "count": 5
257 },
258 "parameters": {}
259 }"#;
260 let intervention: InterventionType = serde_json::from_str(json).unwrap();
261 if let InterventionType::EntityEvent(e) = intervention {
262 assert!(matches!(e.subtype, InterventionEntityEvent::VendorDefault));
263 assert_eq!(e.target.cluster, Some("problematic".to_string()));
264 assert_eq!(e.target.count, Some(5));
265 } else {
266 panic!("Expected EntityEvent");
267 }
268 }
269
270 #[test]
271 fn test_macro_shock_serde() {
272 let json = r#"{
273 "type": "macro_shock",
274 "subtype": "recession",
275 "severity": 1.5,
276 "preset": "2008_financial_crisis",
277 "overrides": {"gdp_growth": -0.03}
278 }"#;
279 let intervention: InterventionType = serde_json::from_str(json).unwrap();
280 if let InterventionType::MacroShock(m) = intervention {
281 assert!(matches!(m.subtype, MacroShockType::Recession));
282 assert_eq!(m.severity, 1.5);
283 assert_eq!(m.preset, Some("2008_financial_crisis".to_string()));
284 } else {
285 panic!("Expected MacroShock");
286 }
287 }
288
289 #[test]
290 fn test_control_failure_serde() {
291 let json = r#"{
292 "type": "control_failure",
293 "subtype": "complete_bypass",
294 "control_target": {"control_id": "C003"},
295 "severity": 0.0,
296 "detectable": false
297 }"#;
298 let intervention: InterventionType = serde_json::from_str(json).unwrap();
299 assert!(matches!(intervention, InterventionType::ControlFailure(_)));
300 }
301
302 #[test]
303 fn test_composite_serde() {
304 let json = r#"{
305 "type": "composite",
306 "name": "recession_scenario",
307 "description": "Combined recession effects",
308 "children": [
309 {
310 "type": "macro_shock",
311 "subtype": "recession",
312 "severity": 1.0,
313 "overrides": {}
314 }
315 ],
316 "conflict_resolution": "first_wins"
317 }"#;
318 let intervention: InterventionType = serde_json::from_str(json).unwrap();
319 if let InterventionType::Composite(c) = intervention {
320 assert_eq!(c.name, "recession_scenario");
321 assert_eq!(c.children.len(), 1);
322 } else {
323 panic!("Expected Composite");
324 }
325 }
326
327 #[test]
328 fn test_conflict_resolution_default() {
329 let cr = ConflictResolution::default();
330 assert!(matches!(cr, ConflictResolution::FirstWins));
331 }
332}