Skip to main content

datasynth_ocpm/generator/
o2c_generator.rs

1//! O2C (Order-to-Cash) process event generator.
2//!
3//! Generates OCPM events for the complete O2C flow:
4//! Create SO → Check Credit → Release SO → Create Delivery → Pick → Pack →
5//! Ship → Create Invoice → Post Invoice → Receive Payment
6
7use chrono::{DateTime, Utc};
8use rust_decimal::Decimal;
9use uuid::Uuid;
10
11use super::{CaseGenerationResult, OcpmEventGenerator, VariantType};
12use crate::models::{
13    ActivityType, EventObjectRef, ObjectAttributeValue, ObjectRelationship, ObjectType,
14};
15use datasynth_core::models::BusinessProcess;
16
17/// O2C document references for event generation.
18#[derive(Debug, Clone)]
19pub struct O2cDocuments {
20    /// Sales order number
21    pub so_number: String,
22    /// Sales order UUID
23    pub so_id: Uuid,
24    /// Delivery number
25    pub delivery_number: Option<String>,
26    /// Delivery UUID
27    pub delivery_id: Option<Uuid>,
28    /// Customer invoice number
29    pub invoice_number: Option<String>,
30    /// Invoice UUID
31    pub invoice_id: Option<Uuid>,
32    /// Receipt number
33    pub receipt_number: Option<String>,
34    /// Receipt UUID
35    pub receipt_id: Option<Uuid>,
36    /// Customer ID
37    pub customer_id: String,
38    /// Company code
39    pub company_code: String,
40    /// Total amount
41    pub amount: Decimal,
42    /// Currency
43    pub currency: String,
44}
45
46impl O2cDocuments {
47    /// Create new O2C documents.
48    pub fn new(
49        so_number: &str,
50        customer_id: &str,
51        company_code: &str,
52        amount: Decimal,
53        currency: &str,
54    ) -> Self {
55        Self {
56            so_number: so_number.into(),
57            so_id: Uuid::new_v4(),
58            delivery_number: None,
59            delivery_id: None,
60            invoice_number: None,
61            invoice_id: None,
62            receipt_number: None,
63            receipt_id: None,
64            customer_id: customer_id.into(),
65            company_code: company_code.into(),
66            amount,
67            currency: currency.into(),
68        }
69    }
70
71    /// Set delivery info.
72    pub fn with_delivery(mut self, delivery_number: &str) -> Self {
73        self.delivery_number = Some(delivery_number.into());
74        self.delivery_id = Some(Uuid::new_v4());
75        self
76    }
77
78    /// Set invoice info.
79    pub fn with_invoice(mut self, invoice_number: &str) -> Self {
80        self.invoice_number = Some(invoice_number.into());
81        self.invoice_id = Some(Uuid::new_v4());
82        self
83    }
84
85    /// Set receipt info.
86    pub fn with_receipt(mut self, receipt_number: &str) -> Self {
87        self.receipt_number = Some(receipt_number.into());
88        self.receipt_id = Some(Uuid::new_v4());
89        self
90    }
91}
92
93impl OcpmEventGenerator {
94    /// Generate complete O2C process events.
95    pub fn generate_o2c_case(
96        &mut self,
97        documents: &O2cDocuments,
98        start_time: DateTime<Utc>,
99        available_users: &[String],
100    ) -> CaseGenerationResult {
101        let case_id = self.new_case_id();
102        let variant_type = self.select_variant_type();
103
104        let mut events = Vec::new();
105        let mut objects = Vec::new();
106        let mut relationships = Vec::new();
107        let mut current_time = start_time;
108
109        // Create object types
110        let so_type = ObjectType::sales_order();
111        let delivery_type = ObjectType::delivery();
112        let invoice_type = ObjectType::customer_invoice();
113
114        // Create SO object
115        let so_object = self.create_object(
116            &so_type,
117            &documents.so_number,
118            &documents.company_code,
119            current_time,
120        );
121        objects.push(so_object.clone());
122
123        // Activity: Create SO
124        let create_so = ActivityType::create_so();
125        let resource = self.select_resource(&create_so, available_users);
126        let mut event = self.create_event(
127            &create_so,
128            current_time,
129            &resource,
130            &documents.company_code,
131            case_id,
132        );
133        event = event
134            .with_object(
135                EventObjectRef::created(so_object.object_id, &so_type.type_id)
136                    .with_external_id(&documents.so_number),
137            )
138            .with_document_ref(&documents.so_number);
139        Self::add_event_attribute(
140            &mut event,
141            "amount",
142            ObjectAttributeValue::Decimal(documents.amount),
143        );
144        Self::add_event_attribute(
145            &mut event,
146            "customer_id",
147            ObjectAttributeValue::String(documents.customer_id.clone()),
148        );
149        events.push(event);
150
151        // Activity: Check Credit
152        current_time = self.calculate_event_time(current_time, &create_so);
153
154        let check_credit = ActivityType::check_credit();
155        let resource = self.select_resource(&check_credit, available_users);
156        let mut event = self.create_event(
157            &check_credit,
158            current_time,
159            &resource,
160            &documents.company_code,
161            case_id,
162        );
163        event = event.with_object(
164            EventObjectRef::updated(so_object.object_id, &so_type.type_id)
165                .with_external_id(&documents.so_number),
166        );
167        Self::add_event_attribute(
168            &mut event,
169            "credit_result",
170            ObjectAttributeValue::String("approved".into()),
171        );
172        events.push(event);
173
174        // Activity: Release SO
175        current_time = self.calculate_event_time(current_time, &check_credit);
176
177        let release_so = ActivityType::release_so();
178        let resource = self.select_resource(&release_so, available_users);
179        let mut event = self.create_event(
180            &release_so,
181            current_time,
182            &resource,
183            &documents.company_code,
184            case_id,
185        );
186        event = event.with_object(
187            EventObjectRef::updated(so_object.object_id, &so_type.type_id)
188                .with_external_id(&documents.so_number),
189        );
190        events.push(event);
191
192        // Skip remaining steps for error paths
193        if matches!(variant_type, VariantType::ErrorPath) {
194            let case_trace = self.create_case_trace(
195                case_id,
196                &events,
197                BusinessProcess::O2C,
198                so_object.object_id,
199                &so_type.type_id,
200                &documents.company_code,
201            );
202            return CaseGenerationResult {
203                events,
204                objects,
205                relationships,
206                case_trace,
207                variant_type,
208            };
209        }
210
211        // Activity: Create Delivery
212        if let Some(delivery_number) = &documents.delivery_number {
213            current_time = self.calculate_event_time(current_time, &release_so);
214            current_time += self.generate_inter_activity_delay(60, 480);
215
216            let delivery_object = self.create_object(
217                &delivery_type,
218                delivery_number,
219                &documents.company_code,
220                current_time,
221            );
222            objects.push(delivery_object.clone());
223
224            // Add relationship: Delivery references SO
225            relationships.push(ObjectRelationship::new(
226                "references",
227                delivery_object.object_id,
228                &delivery_type.type_id,
229                so_object.object_id,
230                &so_type.type_id,
231            ));
232
233            let create_delivery = ActivityType::create_delivery();
234            let resource = self.select_resource(&create_delivery, available_users);
235            let mut event = self.create_event(
236                &create_delivery,
237                current_time,
238                &resource,
239                &documents.company_code,
240                case_id,
241            );
242            event = event
243                .with_object(
244                    EventObjectRef::created(delivery_object.object_id, &delivery_type.type_id)
245                        .with_external_id(delivery_number),
246                )
247                .with_document_ref(delivery_number);
248            events.push(event);
249
250            // Activity: Pick
251            current_time = self.calculate_event_time(current_time, &create_delivery);
252
253            let pick = ActivityType::pick();
254            let resource = self.select_resource(&pick, available_users);
255            let mut event = self.create_event(
256                &pick,
257                current_time,
258                &resource,
259                &documents.company_code,
260                case_id,
261            );
262            event = event.with_object(
263                EventObjectRef::updated(delivery_object.object_id, &delivery_type.type_id)
264                    .with_external_id(delivery_number),
265            );
266            events.push(event);
267
268            // Activity: Pack
269            current_time = self.calculate_event_time(current_time, &pick);
270
271            let pack = ActivityType::pack();
272            let resource = self.select_resource(&pack, available_users);
273            let mut event = self.create_event(
274                &pack,
275                current_time,
276                &resource,
277                &documents.company_code,
278                case_id,
279            );
280            event = event.with_object(
281                EventObjectRef::updated(delivery_object.object_id, &delivery_type.type_id)
282                    .with_external_id(delivery_number),
283            );
284            events.push(event);
285
286            // Activity: Ship
287            current_time = self.calculate_event_time(current_time, &pack);
288
289            let ship = ActivityType::ship();
290            let resource = self.select_resource(&ship, available_users);
291            let mut event = self.create_event(
292                &ship,
293                current_time,
294                &resource,
295                &documents.company_code,
296                case_id,
297            );
298            event = event
299                .with_object(
300                    EventObjectRef::updated(delivery_object.object_id, &delivery_type.type_id)
301                        .with_external_id(delivery_number),
302                )
303                .with_object(
304                    EventObjectRef::updated(so_object.object_id, &so_type.type_id)
305                        .with_external_id(&documents.so_number),
306                );
307            events.push(event);
308        }
309
310        // Activity: Create Customer Invoice
311        if let Some(invoice_number) = &documents.invoice_number {
312            current_time = self.calculate_event_time(current_time, &ActivityType::ship());
313            current_time += self.generate_inter_activity_delay(60, 1440);
314
315            let invoice_object = self.create_object(
316                &invoice_type,
317                invoice_number,
318                &documents.company_code,
319                current_time,
320            );
321            objects.push(invoice_object.clone());
322
323            // Add relationship: Invoice references SO
324            relationships.push(ObjectRelationship::new(
325                "references",
326                invoice_object.object_id,
327                &invoice_type.type_id,
328                so_object.object_id,
329                &so_type.type_id,
330            ));
331
332            let create_invoice = ActivityType::create_customer_invoice();
333            let resource = self.select_resource(&create_invoice, available_users);
334            let mut event = self.create_event(
335                &create_invoice,
336                current_time,
337                &resource,
338                &documents.company_code,
339                case_id,
340            );
341            event = event
342                .with_object(
343                    EventObjectRef::created(invoice_object.object_id, &invoice_type.type_id)
344                        .with_external_id(invoice_number),
345                )
346                .with_object(
347                    EventObjectRef::updated(so_object.object_id, &so_type.type_id)
348                        .with_external_id(&documents.so_number),
349                )
350                .with_document_ref(invoice_number);
351            Self::add_event_attribute(
352                &mut event,
353                "invoice_amount",
354                ObjectAttributeValue::Decimal(documents.amount),
355            );
356            events.push(event);
357
358            // Activity: Post Customer Invoice
359            current_time = self.calculate_event_time(current_time, &create_invoice);
360
361            let post_invoice = ActivityType::post_customer_invoice();
362            let resource = self.select_resource(&post_invoice, available_users);
363            let mut event = self.create_event(
364                &post_invoice,
365                current_time,
366                &resource,
367                &documents.company_code,
368                case_id,
369            );
370            event = event.with_object(
371                EventObjectRef::updated(invoice_object.object_id, &invoice_type.type_id)
372                    .with_external_id(invoice_number),
373            );
374            events.push(event);
375
376            // Activity: Receive Payment
377            if documents.receipt_number.is_some() {
378                current_time = self.calculate_event_time(current_time, &post_invoice);
379                current_time += self.generate_inter_activity_delay(1440, 43200); // 1-30 days
380
381                let receive_payment = ActivityType::receive_payment();
382                let resource = self.select_resource(&receive_payment, available_users);
383                let mut event = self.create_event(
384                    &receive_payment,
385                    current_time,
386                    &resource,
387                    &documents.company_code,
388                    case_id,
389                );
390                event = event
391                    .with_object(
392                        EventObjectRef::consumed(invoice_object.object_id, &invoice_type.type_id)
393                            .with_external_id(invoice_number),
394                    )
395                    .with_object(
396                        EventObjectRef::consumed(so_object.object_id, &so_type.type_id)
397                            .with_external_id(&documents.so_number),
398                    )
399                    .with_document_ref(documents.receipt_number.as_deref().unwrap_or(""));
400                Self::add_event_attribute(
401                    &mut event,
402                    "payment_amount",
403                    ObjectAttributeValue::Decimal(documents.amount),
404                );
405                events.push(event);
406            }
407        }
408
409        let case_trace = self.create_case_trace(
410            case_id,
411            &events,
412            BusinessProcess::O2C,
413            so_object.object_id,
414            &so_type.type_id,
415            &documents.company_code,
416        );
417
418        CaseGenerationResult {
419            events,
420            objects,
421            relationships,
422            case_trace,
423            variant_type,
424        }
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn test_o2c_case_generation() {
434        let mut generator = OcpmEventGenerator::new(42);
435        let documents = O2cDocuments::new(
436            "SO-000001",
437            "C000001",
438            "1000",
439            Decimal::new(15000, 0),
440            "USD",
441        )
442        .with_delivery("DEL-000001")
443        .with_invoice("INV-000001")
444        .with_receipt("REC-000001");
445
446        let result = generator.generate_o2c_case(
447            &documents,
448            Utc::now(),
449            &["user001".into(), "user002".into()],
450        );
451
452        // Should have at least 3 events (create, check_credit, release)
453        assert!(result.events.len() >= 3);
454        // Should have objects
455        assert!(!result.objects.is_empty());
456        // Should have case trace
457        assert!(!result.case_trace.activity_sequence.is_empty());
458    }
459}