Skip to main content

datasynth_ocpm/models/
event.rs

1//! Event model for OCPM.
2//!
3//! Events represent occurrences of activities on objects. A key feature
4//! of OCPM is that events can involve multiple objects (many-to-many).
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11use super::ObjectAttributeValue;
12
13/// An event instance in OCPM event log.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct OcpmEvent {
16    /// Unique event ID
17    pub event_id: Uuid,
18    /// Activity type that occurred
19    pub activity_id: String,
20    /// Activity name (for convenience)
21    pub activity_name: String,
22    /// Event timestamp
23    pub timestamp: DateTime<Utc>,
24    /// Lifecycle transition (Start, Complete, Abort, etc.)
25    pub lifecycle: EventLifecycle,
26    /// Resource (user/system) that performed the event
27    pub resource_id: String,
28    /// Resource name (for convenience)
29    pub resource_name: Option<String>,
30    /// Company code
31    pub company_code: String,
32    /// Objects involved in this event (many-to-many)
33    pub object_refs: Vec<EventObjectRef>,
34    /// Event attributes
35    pub attributes: HashMap<String, ObjectAttributeValue>,
36    /// Related document reference (JE, PO number, etc.)
37    pub document_ref: Option<String>,
38    /// Related journal entry ID
39    pub journal_entry_id: Option<Uuid>,
40    /// Anomaly flag
41    pub is_anomaly: bool,
42    /// Anomaly type if applicable
43    pub anomaly_type: Option<String>,
44    /// Case ID for process instance tracking
45    pub case_id: Option<Uuid>,
46}
47
48impl OcpmEvent {
49    /// Create a new event.
50    pub fn new(
51        activity_id: &str,
52        activity_name: &str,
53        timestamp: DateTime<Utc>,
54        resource_id: &str,
55        company_code: &str,
56    ) -> Self {
57        Self {
58            event_id: Uuid::new_v4(),
59            activity_id: activity_id.into(),
60            activity_name: activity_name.into(),
61            timestamp,
62            lifecycle: EventLifecycle::Complete,
63            resource_id: resource_id.into(),
64            resource_name: None,
65            company_code: company_code.into(),
66            object_refs: Vec::new(),
67            attributes: HashMap::new(),
68            document_ref: None,
69            journal_entry_id: None,
70            is_anomaly: false,
71            anomaly_type: None,
72            case_id: None,
73        }
74    }
75
76    /// Set a specific event ID.
77    pub fn with_id(mut self, id: Uuid) -> Self {
78        self.event_id = id;
79        self
80    }
81
82    /// Set the lifecycle phase.
83    pub fn with_lifecycle(mut self, lifecycle: EventLifecycle) -> Self {
84        self.lifecycle = lifecycle;
85        self
86    }
87
88    /// Set the resource name.
89    pub fn with_resource_name(mut self, name: &str) -> Self {
90        self.resource_name = Some(name.into());
91        self
92    }
93
94    /// Add an object reference.
95    pub fn with_object(mut self, object_ref: EventObjectRef) -> Self {
96        self.object_refs.push(object_ref);
97        self
98    }
99
100    /// Add multiple object references.
101    pub fn with_objects(mut self, refs: Vec<EventObjectRef>) -> Self {
102        self.object_refs.extend(refs);
103        self
104    }
105
106    /// Add an attribute.
107    pub fn with_attribute(mut self, key: &str, value: ObjectAttributeValue) -> Self {
108        self.attributes.insert(key.into(), value);
109        self
110    }
111
112    /// Set document reference.
113    pub fn with_document_ref(mut self, doc_ref: &str) -> Self {
114        self.document_ref = Some(doc_ref.into());
115        self
116    }
117
118    /// Set journal entry ID.
119    pub fn with_journal_entry(mut self, je_id: Uuid) -> Self {
120        self.journal_entry_id = Some(je_id);
121        self
122    }
123
124    /// Set case ID.
125    pub fn with_case(mut self, case_id: Uuid) -> Self {
126        self.case_id = Some(case_id);
127        self
128    }
129
130    /// Mark as anomalous.
131    pub fn mark_anomaly(&mut self, anomaly_type: &str) {
132        self.is_anomaly = true;
133        self.anomaly_type = Some(anomaly_type.into());
134    }
135
136    /// Get all object IDs involved in this event.
137    pub fn object_ids(&self) -> Vec<Uuid> {
138        self.object_refs.iter().map(|r| r.object_id).collect()
139    }
140
141    /// Get object refs of a specific type.
142    pub fn objects_of_type(&self, type_id: &str) -> Vec<&EventObjectRef> {
143        self.object_refs
144            .iter()
145            .filter(|r| r.object_type_id == type_id)
146            .collect()
147    }
148
149    /// Check if this event creates any object.
150    pub fn creates_objects(&self) -> bool {
151        self.object_refs
152            .iter()
153            .any(|r| r.qualifier == ObjectQualifier::Created)
154    }
155
156    /// Check if this event completes any object.
157    pub fn completes_objects(&self) -> bool {
158        self.object_refs
159            .iter()
160            .any(|r| r.qualifier == ObjectQualifier::Consumed)
161    }
162}
163
164/// Event lifecycle phase.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
166#[serde(rename_all = "snake_case")]
167pub enum EventLifecycle {
168    /// Activity started
169    Start,
170    /// Activity completed
171    #[default]
172    Complete,
173    /// Activity aborted
174    Abort,
175    /// Activity suspended
176    Suspend,
177    /// Activity resumed
178    Resume,
179    /// Atomic event (no duration, single timestamp)
180    Atomic,
181}
182
183impl EventLifecycle {
184    /// Check if this is a completion event.
185    pub fn is_completion(&self) -> bool {
186        matches!(self, Self::Complete | Self::Abort)
187    }
188
189    /// Check if this is a start event.
190    pub fn is_start(&self) -> bool {
191        matches!(self, Self::Start)
192    }
193}
194
195/// Reference from event to object with qualifier.
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
197pub struct EventObjectRef {
198    /// Object ID
199    pub object_id: Uuid,
200    /// Object type ID
201    pub object_type_id: String,
202    /// Object external ID (for convenience)
203    pub external_id: Option<String>,
204    /// Qualifier describing the relationship
205    pub qualifier: ObjectQualifier,
206}
207
208impl EventObjectRef {
209    /// Create a new object reference.
210    pub fn new(object_id: Uuid, object_type_id: &str, qualifier: ObjectQualifier) -> Self {
211        Self {
212            object_id,
213            object_type_id: object_type_id.into(),
214            external_id: None,
215            qualifier,
216        }
217    }
218
219    /// Set the external ID.
220    pub fn with_external_id(mut self, external_id: &str) -> Self {
221        self.external_id = Some(external_id.into());
222        self
223    }
224
225    /// Create a reference for a created object.
226    pub fn created(object_id: Uuid, object_type_id: &str) -> Self {
227        Self::new(object_id, object_type_id, ObjectQualifier::Created)
228    }
229
230    /// Create a reference for an updated object.
231    pub fn updated(object_id: Uuid, object_type_id: &str) -> Self {
232        Self::new(object_id, object_type_id, ObjectQualifier::Updated)
233    }
234
235    /// Create a reference for a read/referenced object.
236    pub fn read(object_id: Uuid, object_type_id: &str) -> Self {
237        Self::new(object_id, object_type_id, ObjectQualifier::Read)
238    }
239
240    /// Create a reference for a consumed/completed object.
241    pub fn consumed(object_id: Uuid, object_type_id: &str) -> Self {
242        Self::new(object_id, object_type_id, ObjectQualifier::Consumed)
243    }
244
245    /// Create a reference for a context object.
246    pub fn context(object_id: Uuid, object_type_id: &str) -> Self {
247        Self::new(object_id, object_type_id, ObjectQualifier::Context)
248    }
249}
250
251/// Qualifier for event-object relationship.
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
253#[serde(rename_all = "snake_case")]
254pub enum ObjectQualifier {
255    /// Object is created by this event
256    Created,
257    /// Object is updated by this event
258    #[default]
259    Updated,
260    /// Object is read/referenced by this event (no change)
261    Read,
262    /// Object is consumed/completed by this event
263    Consumed,
264    /// Object is a context object (indirect involvement)
265    Context,
266}
267
268impl ObjectQualifier {
269    /// Check if this qualifier indicates an object change.
270    pub fn changes_object(&self) -> bool {
271        matches!(self, Self::Created | Self::Updated | Self::Consumed)
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_event_creation() {
281        let event = OcpmEvent::new(
282            "create_po",
283            "Create Purchase Order",
284            Utc::now(),
285            "user001",
286            "1000",
287        );
288
289        assert_eq!(event.activity_id, "create_po");
290        assert_eq!(event.lifecycle, EventLifecycle::Complete);
291        assert!(!event.is_anomaly);
292    }
293
294    #[test]
295    fn test_event_with_objects() {
296        let po_id = Uuid::new_v4();
297        let vendor_id = Uuid::new_v4();
298
299        let event = OcpmEvent::new("create_po", "Create PO", Utc::now(), "user001", "1000")
300            .with_object(EventObjectRef::created(po_id, "purchase_order"))
301            .with_object(EventObjectRef::read(vendor_id, "vendor"));
302
303        assert_eq!(event.object_refs.len(), 2);
304        assert!(event.creates_objects());
305    }
306
307    #[test]
308    fn test_object_qualifier() {
309        assert!(ObjectQualifier::Created.changes_object());
310        assert!(ObjectQualifier::Updated.changes_object());
311        assert!(!ObjectQualifier::Read.changes_object());
312        assert!(!ObjectQualifier::Context.changes_object());
313    }
314}