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)]
228#[allow(clippy::unwrap_used)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_intervention_type_tagged_serde() {
234 let json = r#"{
235 "type": "parameter_shift",
236 "target": "transactions.count",
237 "from": 1000,
238 "to": 2000,
239 "interpolation": "linear"
240 }"#;
241 let intervention: InterventionType = serde_json::from_str(json).unwrap();
242 assert!(matches!(intervention, InterventionType::ParameterShift(_)));
243
244 let serialized = serde_json::to_string(&intervention).unwrap();
246 let deserialized: InterventionType = serde_json::from_str(&serialized).unwrap();
247 assert!(matches!(deserialized, InterventionType::ParameterShift(_)));
248 }
249
250 #[test]
251 fn test_entity_event_serde() {
252 let json = r#"{
253 "type": "entity_event",
254 "subtype": "vendor_default",
255 "target": {
256 "cluster": "problematic",
257 "count": 5
258 },
259 "parameters": {}
260 }"#;
261 let intervention: InterventionType = serde_json::from_str(json).unwrap();
262 if let InterventionType::EntityEvent(e) = intervention {
263 assert!(matches!(e.subtype, InterventionEntityEvent::VendorDefault));
264 assert_eq!(e.target.cluster, Some("problematic".to_string()));
265 assert_eq!(e.target.count, Some(5));
266 } else {
267 panic!("Expected EntityEvent");
268 }
269 }
270
271 #[test]
272 fn test_macro_shock_serde() {
273 let json = r#"{
274 "type": "macro_shock",
275 "subtype": "recession",
276 "severity": 1.5,
277 "preset": "2008_financial_crisis",
278 "overrides": {"gdp_growth": -0.03}
279 }"#;
280 let intervention: InterventionType = serde_json::from_str(json).unwrap();
281 if let InterventionType::MacroShock(m) = intervention {
282 assert!(matches!(m.subtype, MacroShockType::Recession));
283 assert_eq!(m.severity, 1.5);
284 assert_eq!(m.preset, Some("2008_financial_crisis".to_string()));
285 } else {
286 panic!("Expected MacroShock");
287 }
288 }
289
290 #[test]
291 fn test_control_failure_serde() {
292 let json = r#"{
293 "type": "control_failure",
294 "subtype": "complete_bypass",
295 "control_target": {"control_id": "C003"},
296 "severity": 0.0,
297 "detectable": false
298 }"#;
299 let intervention: InterventionType = serde_json::from_str(json).unwrap();
300 assert!(matches!(intervention, InterventionType::ControlFailure(_)));
301 }
302
303 #[test]
304 fn test_composite_serde() {
305 let json = r#"{
306 "type": "composite",
307 "name": "recession_scenario",
308 "description": "Combined recession effects",
309 "children": [
310 {
311 "type": "macro_shock",
312 "subtype": "recession",
313 "severity": 1.0,
314 "overrides": {}
315 }
316 ],
317 "conflict_resolution": "first_wins"
318 }"#;
319 let intervention: InterventionType = serde_json::from_str(json).unwrap();
320 if let InterventionType::Composite(c) = intervention {
321 assert_eq!(c.name, "recession_scenario");
322 assert_eq!(c.children.len(), 1);
323 } else {
324 panic!("Expected Composite");
325 }
326 }
327
328 #[test]
329 fn test_conflict_resolution_default() {
330 let cr = ConflictResolution::default();
331 assert!(matches!(cr, ConflictResolution::FirstWins));
332 }
333}