1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ScenarioDiff {
8 pub summary: Option<ImpactSummary>,
9 pub record_level: Option<Vec<RecordLevelDiff>>,
10 pub aggregate: Option<AggregateComparison>,
11 pub intervention_trace: Option<InterventionTrace>,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ImpactSummary {
17 pub scenario_name: String,
18 pub generation_timestamp: String,
19 pub interventions_applied: usize,
20 pub kpi_impacts: Vec<KpiImpact>,
21 pub financial_statement_impacts: Option<FinancialStatementImpact>,
22 pub anomaly_impact: Option<AnomalyImpact>,
23 pub control_impact: Option<ControlImpact>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct KpiImpact {
29 pub kpi_name: String,
30 pub baseline_value: f64,
31 pub counterfactual_value: f64,
32 pub absolute_change: f64,
33 pub percent_change: f64,
34 pub direction: ChangeDirection,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub enum ChangeDirection {
40 Increase,
41 Decrease,
42 Unchanged,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct FinancialStatementImpact {
48 pub revenue_change_pct: f64,
49 pub cogs_change_pct: f64,
50 pub margin_change_pct: f64,
51 pub net_income_change_pct: f64,
52 pub total_assets_change_pct: f64,
53 pub total_liabilities_change_pct: f64,
54 pub cash_flow_change_pct: f64,
55 pub top_changed_line_items: Vec<LineItemImpact>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct LineItemImpact {
61 pub line_item: String,
62 pub baseline: f64,
63 pub counterfactual: f64,
64 pub change_pct: f64,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct AnomalyImpact {
70 pub baseline_count: usize,
71 pub counterfactual_count: usize,
72 pub new_types: Vec<String>,
73 pub removed_types: Vec<String>,
74 pub rate_change_pct: f64,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ControlImpact {
80 pub controls_affected: usize,
81 pub new_deficiencies: Vec<ControlDeficiency>,
82 pub material_weakness_risk: bool,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ControlDeficiency {
88 pub control_id: String,
89 pub name: String,
90 pub baseline_effectiveness: f64,
91 pub counterfactual_effectiveness: f64,
92 pub classification: DeficiencyClassification,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub enum DeficiencyClassification {
98 Deficiency,
99 SignificantDeficiency,
100 MaterialWeakness,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct RecordLevelDiff {
106 pub file_name: String,
107 pub records_added: usize,
108 pub records_removed: usize,
109 pub records_modified: usize,
110 pub records_unchanged: usize,
111 pub sample_changes: Vec<RecordChange>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct RecordChange {
117 pub record_id: String,
118 pub change_type: RecordChangeType,
119 pub field_changes: Vec<FieldChange>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub enum RecordChangeType {
125 Added,
126 Removed,
127 Modified,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct FieldChange {
133 pub field_name: String,
134 pub baseline_value: String,
135 pub counterfactual_value: String,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct AggregateComparison {
141 pub metrics: Vec<MetricComparison>,
142 pub period_comparisons: Vec<PeriodComparison>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct MetricComparison {
148 pub metric_name: String,
149 pub baseline: f64,
150 pub counterfactual: f64,
151 pub change_pct: f64,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct PeriodComparison {
157 pub period: String,
158 pub metrics: Vec<MetricComparison>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct InterventionTrace {
164 pub traces: Vec<InterventionEffect>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct InterventionEffect {
170 pub intervention_label: String,
171 pub intervention_type: String,
172 pub causal_path: Vec<CausalPathStep>,
173 pub ultimate_impacts: Vec<KpiImpact>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct CausalPathStep {
179 pub node_id: String,
180 pub node_label: String,
181 pub input_delta: f64,
182 pub output_delta: f64,
183 pub transfer_function: String,
184}
185
186#[cfg(test)]
187#[allow(clippy::unwrap_used)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_scenario_diff_serde_roundtrip() {
193 let diff = ScenarioDiff {
194 summary: Some(ImpactSummary {
195 scenario_name: "test".to_string(),
196 generation_timestamp: "2024-01-01T00:00:00Z".to_string(),
197 interventions_applied: 1,
198 kpi_impacts: vec![KpiImpact {
199 kpi_name: "total_transactions".to_string(),
200 baseline_value: 1000.0,
201 counterfactual_value: 800.0,
202 absolute_change: -200.0,
203 percent_change: -20.0,
204 direction: ChangeDirection::Decrease,
205 }],
206 financial_statement_impacts: None,
207 anomaly_impact: None,
208 control_impact: None,
209 }),
210 record_level: None,
211 aggregate: None,
212 intervention_trace: None,
213 };
214
215 let json = serde_json::to_string(&diff).expect("serialize");
216 let deserialized: ScenarioDiff = serde_json::from_str(&json).expect("deserialize");
217 assert_eq!(
218 deserialized
219 .summary
220 .as_ref()
221 .expect("has summary")
222 .scenario_name,
223 "test"
224 );
225 }
226
227 #[test]
228 fn test_change_direction_variants() {
229 assert_eq!(ChangeDirection::Increase, ChangeDirection::Increase);
230 assert_eq!(ChangeDirection::Decrease, ChangeDirection::Decrease);
231 assert_eq!(ChangeDirection::Unchanged, ChangeDirection::Unchanged);
232 }
233
234 #[test]
235 fn test_record_level_diff_serde() {
236 let diff = RecordLevelDiff {
237 file_name: "journal_entries.csv".to_string(),
238 records_added: 10,
239 records_removed: 0,
240 records_modified: 50,
241 records_unchanged: 940,
242 sample_changes: vec![RecordChange {
243 record_id: "JE-001".to_string(),
244 change_type: RecordChangeType::Modified,
245 field_changes: vec![FieldChange {
246 field_name: "amount".to_string(),
247 baseline_value: "1000.00".to_string(),
248 counterfactual_value: "800.00".to_string(),
249 }],
250 }],
251 };
252
253 let json = serde_json::to_string(&diff).expect("serialize");
254 let deserialized: RecordLevelDiff = serde_json::from_str(&json).expect("deserialize");
255 assert_eq!(deserialized.file_name, "journal_entries.csv");
256 assert_eq!(deserialized.records_modified, 50);
257 }
258
259 #[test]
260 fn test_aggregate_comparison_serde() {
261 let agg = AggregateComparison {
262 metrics: vec![MetricComparison {
263 metric_name: "total_amount".to_string(),
264 baseline: 1_000_000.0,
265 counterfactual: 850_000.0,
266 change_pct: -15.0,
267 }],
268 period_comparisons: vec![PeriodComparison {
269 period: "2024-01".to_string(),
270 metrics: vec![MetricComparison {
271 metric_name: "transaction_count".to_string(),
272 baseline: 100.0,
273 counterfactual: 80.0,
274 change_pct: -20.0,
275 }],
276 }],
277 };
278
279 let json = serde_json::to_string(&agg).expect("serialize");
280 let deserialized: AggregateComparison = serde_json::from_str(&json).expect("deserialize");
281 assert_eq!(deserialized.metrics.len(), 1);
282 assert_eq!(deserialized.period_comparisons.len(), 1);
283 }
284}