Skip to main content

datasynth_ocpm/models/
process_variant.rs

1//! Process variant and case trace models for OCPM.
2//!
3//! Process variants represent distinct execution patterns through processes.
4//! Case traces link events to process instances.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use datasynth_core::models::BusinessProcess;
11
12/// A distinct execution sequence through the process.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ProcessVariant {
15    /// Unique variant ID
16    pub variant_id: String,
17    /// Business process
18    pub business_process: BusinessProcess,
19    /// Sequence of activity IDs in this variant
20    pub activity_sequence: Vec<String>,
21    /// Frequency count (how many times this variant occurred)
22    pub frequency: u64,
23    /// Percentage of total cases
24    pub frequency_percent: f64,
25    /// Average duration for this variant (in hours)
26    pub avg_duration_hours: f64,
27    /// Minimum duration observed
28    pub min_duration_hours: f64,
29    /// Maximum duration observed
30    pub max_duration_hours: f64,
31    /// Standard deviation of duration
32    pub std_duration_hours: f64,
33    /// Example case IDs following this variant
34    pub example_case_ids: Vec<Uuid>,
35    /// Is this a happy path (expected) variant
36    pub is_happy_path: bool,
37    /// Deviation indicators
38    pub has_rework: bool,
39    /// Has skipped steps
40    pub has_skipped_steps: bool,
41    /// Has out-of-order steps
42    pub has_out_of_order: bool,
43}
44
45impl ProcessVariant {
46    /// Create a new process variant.
47    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    /// Set the activity sequence.
67    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    /// Mark as happy path.
73    pub fn happy_path(mut self) -> Self {
74        self.is_happy_path = true;
75        self
76    }
77
78    /// Mark as having rework.
79    pub fn with_rework(mut self) -> Self {
80        self.has_rework = true;
81        self
82    }
83
84    /// Mark as having skipped steps.
85    pub fn with_skipped_steps(mut self) -> Self {
86        self.has_skipped_steps = true;
87        self
88    }
89
90    /// Increment frequency and add example case.
91    pub fn add_case(&mut self, case_id: Uuid, duration_hours: f64) {
92        // Online update of statistics
93        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            // Welford's online algorithm for mean and variance
103            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            // M2 update (for variance calculation)
107            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        // Keep only a few example cases
115        if self.example_case_ids.len() < 5 {
116            self.example_case_ids.push(case_id);
117        }
118    }
119
120    /// Generate a hash-based variant ID from the sequence.
121    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    /// Standard P2P happy path variant.
131    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    /// Standard O2C happy path variant.
148    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/// Case execution trace linking events to a process instance.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct CaseTrace {
169    /// Unique case ID
170    pub case_id: Uuid,
171    /// Variant ID (computed after case completion)
172    pub variant_id: Option<String>,
173    /// Business process
174    pub business_process: BusinessProcess,
175    /// Case start time
176    pub start_time: DateTime<Utc>,
177    /// Case end time (if completed)
178    pub end_time: Option<DateTime<Utc>>,
179    /// Event IDs in chronological order
180    pub event_ids: Vec<Uuid>,
181    /// Activity sequence (for variant matching)
182    pub activity_sequence: Vec<String>,
183    /// Primary object ID (the main object being processed)
184    pub primary_object_id: Uuid,
185    /// Primary object type
186    pub primary_object_type: String,
187    /// Case status
188    pub status: CaseStatus,
189    /// Company code
190    pub company_code: String,
191    /// Is this case marked as anomalous
192    pub is_anomaly: bool,
193}
194
195impl CaseTrace {
196    /// Create a new case trace.
197    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    /// Add an event to the trace.
220    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        // Update start time if this is the first event
225        if self.event_ids.len() == 1 {
226            self.start_time = timestamp;
227        }
228    }
229
230    /// Complete the case.
231    pub fn complete(&mut self) {
232        self.status = CaseStatus::Completed;
233        self.end_time = Some(Utc::now());
234    }
235
236    /// Complete the case at a specific time.
237    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    /// Abort the case.
243    pub fn abort(&mut self) {
244        self.status = CaseStatus::Aborted;
245        self.end_time = Some(Utc::now());
246    }
247
248    /// Get the duration in hours (if completed).
249    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    /// Check if the case is completed.
255    pub fn is_completed(&self) -> bool {
256        matches!(self.status, CaseStatus::Completed)
257    }
258}
259
260/// Case status.
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
262#[serde(rename_all = "snake_case")]
263pub enum CaseStatus {
264    /// Case is in progress
265    #[default]
266    InProgress,
267    /// Case completed successfully
268    Completed,
269    /// Case was aborted
270    Aborted,
271    /// Case is on hold
272    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}