1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use datasynth_core::models::BusinessProcess;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ProcessVariant {
15 pub variant_id: String,
17 pub business_process: BusinessProcess,
19 pub activity_sequence: Vec<String>,
21 pub frequency: u64,
23 pub frequency_percent: f64,
25 pub avg_duration_hours: f64,
27 pub min_duration_hours: f64,
29 pub max_duration_hours: f64,
31 pub std_duration_hours: f64,
33 pub example_case_ids: Vec<Uuid>,
35 pub is_happy_path: bool,
37 pub has_rework: bool,
39 pub has_skipped_steps: bool,
41 pub has_out_of_order: bool,
43}
44
45impl ProcessVariant {
46 pub fn new(variant_id: &str, business_process: BusinessProcess) -> Self {
48 Self {
49 variant_id: variant_id.into(),
50 business_process,
51 activity_sequence: Vec::new(),
52 frequency: 0,
53 frequency_percent: 0.0,
54 avg_duration_hours: 0.0,
55 min_duration_hours: 0.0,
56 max_duration_hours: 0.0,
57 std_duration_hours: 0.0,
58 example_case_ids: Vec::new(),
59 is_happy_path: false,
60 has_rework: false,
61 has_skipped_steps: false,
62 has_out_of_order: false,
63 }
64 }
65
66 pub fn with_sequence(mut self, sequence: Vec<&str>) -> Self {
68 self.activity_sequence = sequence.into_iter().map(String::from).collect();
69 self
70 }
71
72 pub fn happy_path(mut self) -> Self {
74 self.is_happy_path = true;
75 self
76 }
77
78 pub fn with_rework(mut self) -> Self {
80 self.has_rework = true;
81 self
82 }
83
84 pub fn with_skipped_steps(mut self) -> Self {
86 self.has_skipped_steps = true;
87 self
88 }
89
90 pub fn add_case(&mut self, case_id: Uuid, duration_hours: f64) {
92 let n = self.frequency as f64;
94 self.frequency += 1;
95
96 if self.frequency == 1 {
97 self.avg_duration_hours = duration_hours;
98 self.min_duration_hours = duration_hours;
99 self.max_duration_hours = duration_hours;
100 self.std_duration_hours = 0.0;
101 } else {
102 let delta = duration_hours - self.avg_duration_hours;
104 self.avg_duration_hours += delta / (n + 1.0);
105 let delta2 = duration_hours - self.avg_duration_hours;
106 self.std_duration_hours =
108 ((self.std_duration_hours.powi(2) * n + delta * delta2) / (n + 1.0)).sqrt();
109
110 self.min_duration_hours = self.min_duration_hours.min(duration_hours);
111 self.max_duration_hours = self.max_duration_hours.max(duration_hours);
112 }
113
114 if self.example_case_ids.len() < 5 {
116 self.example_case_ids.push(case_id);
117 }
118 }
119
120 pub fn sequence_hash(&self) -> String {
122 use std::collections::hash_map::DefaultHasher;
123 use std::hash::{Hash, Hasher};
124
125 let mut hasher = DefaultHasher::new();
126 self.activity_sequence.hash(&mut hasher);
127 format!("V{:016X}", hasher.finish())
128 }
129
130 pub fn p2p_happy_path() -> Self {
132 Self::new("P2P_HAPPY", BusinessProcess::P2P)
133 .with_sequence(vec![
134 "create_po",
135 "approve_po",
136 "release_po",
137 "create_gr",
138 "post_gr",
139 "receive_invoice",
140 "verify_invoice",
141 "post_invoice",
142 "execute_payment",
143 ])
144 .happy_path()
145 }
146
147 pub fn o2c_happy_path() -> Self {
149 Self::new("O2C_HAPPY", BusinessProcess::O2C)
150 .with_sequence(vec![
151 "create_so",
152 "check_credit",
153 "release_so",
154 "create_delivery",
155 "pick",
156 "pack",
157 "ship",
158 "create_customer_invoice",
159 "post_customer_invoice",
160 "receive_payment",
161 ])
162 .happy_path()
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct CaseTrace {
169 pub case_id: Uuid,
171 pub variant_id: Option<String>,
173 pub business_process: BusinessProcess,
175 pub start_time: DateTime<Utc>,
177 pub end_time: Option<DateTime<Utc>>,
179 pub event_ids: Vec<Uuid>,
181 pub activity_sequence: Vec<String>,
183 pub primary_object_id: Uuid,
185 pub primary_object_type: String,
187 pub status: CaseStatus,
189 pub company_code: String,
191 pub is_anomaly: bool,
193}
194
195impl CaseTrace {
196 pub fn new(
198 business_process: BusinessProcess,
199 primary_object_id: Uuid,
200 primary_object_type: &str,
201 company_code: &str,
202 ) -> Self {
203 Self {
204 case_id: Uuid::new_v4(),
205 variant_id: None,
206 business_process,
207 start_time: Utc::now(),
208 end_time: None,
209 event_ids: Vec::new(),
210 activity_sequence: Vec::new(),
211 primary_object_id,
212 primary_object_type: primary_object_type.into(),
213 status: CaseStatus::InProgress,
214 company_code: company_code.into(),
215 is_anomaly: false,
216 }
217 }
218
219 pub fn add_event(&mut self, event_id: Uuid, activity_id: &str, timestamp: DateTime<Utc>) {
221 self.event_ids.push(event_id);
222 self.activity_sequence.push(activity_id.into());
223
224 if self.event_ids.len() == 1 {
226 self.start_time = timestamp;
227 }
228 }
229
230 pub fn complete(&mut self) {
232 self.status = CaseStatus::Completed;
233 self.end_time = Some(Utc::now());
234 }
235
236 pub fn complete_at(&mut self, end_time: DateTime<Utc>) {
238 self.status = CaseStatus::Completed;
239 self.end_time = Some(end_time);
240 }
241
242 pub fn abort(&mut self) {
244 self.status = CaseStatus::Aborted;
245 self.end_time = Some(Utc::now());
246 }
247
248 pub fn duration_hours(&self) -> Option<f64> {
250 self.end_time
251 .map(|end| (end - self.start_time).num_seconds() as f64 / 3600.0)
252 }
253
254 pub fn is_completed(&self) -> bool {
256 matches!(self.status, CaseStatus::Completed)
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
262#[serde(rename_all = "snake_case")]
263pub enum CaseStatus {
264 #[default]
266 InProgress,
267 Completed,
269 Aborted,
271 OnHold,
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_variant_creation() {
281 let variant = ProcessVariant::p2p_happy_path();
282 assert!(variant.is_happy_path);
283 assert_eq!(variant.activity_sequence.len(), 9);
284 }
285
286 #[test]
287 fn test_variant_statistics() {
288 let mut variant = ProcessVariant::new("TEST", BusinessProcess::P2P);
289
290 variant.add_case(Uuid::new_v4(), 10.0);
291 variant.add_case(Uuid::new_v4(), 20.0);
292 variant.add_case(Uuid::new_v4(), 15.0);
293
294 assert_eq!(variant.frequency, 3);
295 assert_eq!(variant.min_duration_hours, 10.0);
296 assert_eq!(variant.max_duration_hours, 20.0);
297 assert!((variant.avg_duration_hours - 15.0).abs() < 0.01);
298 }
299
300 #[test]
301 fn test_case_trace() {
302 let po_id = Uuid::new_v4();
303 let mut trace = CaseTrace::new(BusinessProcess::P2P, po_id, "purchase_order", "1000");
304
305 trace.add_event(Uuid::new_v4(), "create_po", Utc::now());
306 trace.add_event(Uuid::new_v4(), "approve_po", Utc::now());
307
308 assert_eq!(trace.activity_sequence.len(), 2);
309 assert_eq!(trace.status, CaseStatus::InProgress);
310
311 trace.complete();
312 assert!(trace.is_completed());
313 }
314}