Skip to main content

datasynth_ocpm/generator/
event_generator.rs

1//! Core OCPM event generator.
2//!
3//! Generates OCPM events from document flows and business processes.
4
5use chrono::{DateTime, Duration, Utc};
6use rand::{Rng, SeedableRng};
7use rand_chacha::ChaCha8Rng;
8use uuid::Uuid;
9
10use crate::models::{
11    ActivityType, CaseTrace, EventLifecycle, EventObjectRef, ObjectAttributeValue, ObjectInstance,
12    ObjectQualifier, ObjectRelationship, ObjectType, OcpmEvent,
13};
14use datasynth_core::models::BusinessProcess;
15
16/// Configuration for OCPM event generation.
17#[derive(Debug, Clone)]
18pub struct OcpmGeneratorConfig {
19    /// Enable P2P process events
20    pub generate_p2p: bool,
21    /// Enable O2C process events
22    pub generate_o2c: bool,
23    /// Rate of happy path (normal) variants
24    pub happy_path_rate: f64,
25    /// Rate of exception path variants
26    pub exception_path_rate: f64,
27    /// Rate of error path variants
28    pub error_path_rate: f64,
29    /// Add duration variability to events
30    pub add_duration_variability: bool,
31    /// Standard deviation factor for duration
32    pub duration_std_dev_factor: f64,
33}
34
35impl Default for OcpmGeneratorConfig {
36    fn default() -> Self {
37        Self {
38            generate_p2p: true,
39            generate_o2c: true,
40            happy_path_rate: 0.75,
41            exception_path_rate: 0.20,
42            error_path_rate: 0.05,
43            add_duration_variability: true,
44            duration_std_dev_factor: 0.3,
45        }
46    }
47}
48
49/// Main OCPM event generator.
50pub struct OcpmEventGenerator {
51    /// Random number generator
52    rng: ChaCha8Rng,
53    /// Configuration
54    config: OcpmGeneratorConfig,
55    /// P2P activity types
56    p2p_activities: Vec<ActivityType>,
57    /// O2C activity types
58    o2c_activities: Vec<ActivityType>,
59    /// Case ID counter for generating unique case IDs
60    case_counter: u64,
61}
62
63impl OcpmEventGenerator {
64    /// Create a new OCPM event generator with a seed.
65    pub fn new(seed: u64) -> Self {
66        Self {
67            rng: ChaCha8Rng::seed_from_u64(seed),
68            config: OcpmGeneratorConfig::default(),
69            p2p_activities: ActivityType::p2p_activities(),
70            o2c_activities: ActivityType::o2c_activities(),
71            case_counter: 0,
72        }
73    }
74
75    /// Create with custom configuration.
76    pub fn with_config(seed: u64, config: OcpmGeneratorConfig) -> Self {
77        Self {
78            rng: ChaCha8Rng::seed_from_u64(seed),
79            config,
80            p2p_activities: ActivityType::p2p_activities(),
81            o2c_activities: ActivityType::o2c_activities(),
82            case_counter: 0,
83        }
84    }
85
86    /// Generate a new case ID.
87    pub fn new_case_id(&mut self) -> Uuid {
88        self.case_counter += 1;
89        Uuid::new_v4()
90    }
91
92    /// Select a process variant type based on configuration.
93    pub fn select_variant_type(&mut self) -> VariantType {
94        let r: f64 = self.rng.gen();
95        if r < self.config.happy_path_rate {
96            VariantType::HappyPath
97        } else if r < self.config.happy_path_rate + self.config.exception_path_rate {
98            VariantType::ExceptionPath
99        } else {
100            VariantType::ErrorPath
101        }
102    }
103
104    /// Calculate event timestamp with variability.
105    pub fn calculate_event_time(
106        &mut self,
107        base_time: DateTime<Utc>,
108        activity: &ActivityType,
109    ) -> DateTime<Utc> {
110        if let Some(typical_minutes) = activity.typical_duration_minutes {
111            let std_dev = activity.duration_std_dev.unwrap_or(typical_minutes * 0.3);
112
113            if self.config.add_duration_variability {
114                // Add some variability using normal-like distribution
115                let variability: f64 = self.rng.gen_range(-2.0..2.0) * std_dev;
116                let actual_minutes = (typical_minutes + variability).max(1.0);
117                base_time + Duration::minutes(actual_minutes as i64)
118            } else {
119                base_time + Duration::minutes(typical_minutes as i64)
120            }
121        } else {
122            base_time + Duration::minutes(5) // Default 5 minutes
123        }
124    }
125
126    /// Create an event from an activity type.
127    pub fn create_event(
128        &mut self,
129        activity: &ActivityType,
130        timestamp: DateTime<Utc>,
131        resource_id: &str,
132        company_code: &str,
133        case_id: Uuid,
134    ) -> OcpmEvent {
135        OcpmEvent::new(
136            &activity.activity_id,
137            &activity.name,
138            timestamp,
139            resource_id,
140            company_code,
141        )
142        .with_case(case_id)
143        .with_lifecycle(if activity.is_automated {
144            EventLifecycle::Atomic
145        } else {
146            EventLifecycle::Complete
147        })
148    }
149
150    /// Create an object instance for a document.
151    pub fn create_object(
152        &self,
153        object_type: &ObjectType,
154        external_id: &str,
155        company_code: &str,
156        created_at: DateTime<Utc>,
157    ) -> ObjectInstance {
158        ObjectInstance::new(&object_type.type_id, external_id, company_code)
159            .with_state("active")
160            .with_created_at(created_at)
161    }
162
163    /// Create object reference for an event.
164    pub fn create_object_ref(
165        &self,
166        object: &ObjectInstance,
167        qualifier: ObjectQualifier,
168    ) -> EventObjectRef {
169        EventObjectRef::new(object.object_id, &object.object_type_id, qualifier)
170            .with_external_id(&object.external_id)
171    }
172
173    /// Add an attribute to an event.
174    pub fn add_event_attribute(event: &mut OcpmEvent, key: &str, value: ObjectAttributeValue) {
175        event.attributes.insert(key.into(), value);
176    }
177
178    /// Generate a complete case trace from events.
179    pub fn create_case_trace(
180        &self,
181        _case_id: Uuid,
182        events: &[OcpmEvent],
183        business_process: BusinessProcess,
184        primary_object_id: Uuid,
185        primary_object_type: &str,
186        company_code: &str,
187    ) -> CaseTrace {
188        let activity_sequence: Vec<String> = events.iter().map(|e| e.activity_id.clone()).collect();
189
190        let start_time = events.first().map(|e| e.timestamp).unwrap_or_else(Utc::now);
191        let end_time = events.last().map(|e| e.timestamp);
192
193        let mut trace = CaseTrace::new(
194            business_process,
195            primary_object_id,
196            primary_object_type,
197            company_code,
198        );
199        trace.activity_sequence = activity_sequence;
200        trace.event_ids = events.iter().map(|e| e.event_id).collect();
201        trace.start_time = start_time;
202        trace.end_time = end_time;
203        trace
204    }
205
206    /// Select a resource for an activity.
207    pub fn select_resource(
208        &mut self,
209        activity: &ActivityType,
210        available_users: &[String],
211    ) -> String {
212        if activity.is_automated {
213            "SYSTEM".into()
214        } else if available_users.is_empty() {
215            format!("USER{:04}", self.rng.gen_range(1..100))
216        } else {
217            let idx = self.rng.gen_range(0..available_users.len());
218            available_users[idx].clone()
219        }
220    }
221
222    /// Get P2P activities.
223    pub fn p2p_activities(&self) -> &[ActivityType] {
224        &self.p2p_activities
225    }
226
227    /// Get O2C activities.
228    pub fn o2c_activities(&self) -> &[ActivityType] {
229        &self.o2c_activities
230    }
231
232    /// Generate random delay between activities (in minutes).
233    pub fn generate_inter_activity_delay(
234        &mut self,
235        min_minutes: i64,
236        max_minutes: i64,
237    ) -> Duration {
238        let minutes = self.rng.gen_range(min_minutes..=max_minutes);
239        Duration::minutes(minutes)
240    }
241
242    /// Check if an activity should be skipped (for exception paths).
243    pub fn should_skip_activity(&mut self, skip_probability: f64) -> bool {
244        self.rng.gen::<f64>() < skip_probability
245    }
246
247    /// Generate a random boolean with given probability.
248    pub fn random_bool(&mut self, probability: f64) -> bool {
249        self.rng.gen::<f64>() < probability
250    }
251}
252
253/// Type of process variant.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum VariantType {
256    /// Normal/happy path - all activities completed successfully
257    HappyPath,
258    /// Exception path - some activities skipped or modified
259    ExceptionPath,
260    /// Error path - process aborted or failed
261    ErrorPath,
262}
263
264impl VariantType {
265    /// Get a description of this variant type.
266    pub fn description(&self) -> &'static str {
267        match self {
268            Self::HappyPath => "Standard process execution",
269            Self::ExceptionPath => "Process with exceptions or variations",
270            Self::ErrorPath => "Process failed or aborted",
271        }
272    }
273}
274
275/// Result of generating events for a case.
276#[derive(Debug)]
277pub struct CaseGenerationResult {
278    /// Generated events
279    pub events: Vec<OcpmEvent>,
280    /// Generated objects
281    pub objects: Vec<ObjectInstance>,
282    /// Generated relationships
283    pub relationships: Vec<ObjectRelationship>,
284    /// Case trace
285    pub case_trace: CaseTrace,
286    /// Variant type used
287    pub variant_type: VariantType,
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_generator_creation() {
296        let generator = OcpmEventGenerator::new(42);
297        assert!(!generator.p2p_activities.is_empty());
298        assert!(!generator.o2c_activities.is_empty());
299    }
300
301    #[test]
302    fn test_variant_selection() {
303        let mut generator = OcpmEventGenerator::new(42);
304
305        // Generate many variants and check distribution
306        let mut happy = 0;
307        let mut exception = 0;
308        let mut error = 0;
309
310        for _ in 0..1000 {
311            match generator.select_variant_type() {
312                VariantType::HappyPath => happy += 1,
313                VariantType::ExceptionPath => exception += 1,
314                VariantType::ErrorPath => error += 1,
315            }
316        }
317
318        // Should be roughly 75%/20%/5%
319        assert!(happy > 600 && happy < 850);
320        assert!(exception > 100 && exception < 300);
321        assert!(error > 10 && error < 100);
322    }
323
324    #[test]
325    fn test_case_id_generation() {
326        let mut generator = OcpmEventGenerator::new(42);
327        let id1 = generator.new_case_id();
328        let id2 = generator.new_case_id();
329        assert_ne!(id1, id2);
330    }
331
332    #[test]
333    fn test_event_creation() {
334        let mut generator = OcpmEventGenerator::new(42);
335        let activity = ActivityType::create_po();
336        let case_id = generator.new_case_id();
337
338        let event = generator.create_event(&activity, Utc::now(), "user001", "1000", case_id);
339
340        assert_eq!(event.activity_id, "create_po");
341        assert_eq!(event.case_id, Some(case_id));
342    }
343}