1use 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#[derive(Debug, Clone)]
19pub struct P2pDocuments {
20 pub po_number: String,
22 pub po_id: Uuid,
24 pub gr_number: Option<String>,
26 pub gr_id: Option<Uuid>,
28 pub invoice_number: Option<String>,
30 pub invoice_id: Option<Uuid>,
32 pub payment_number: Option<String>,
34 pub payment_id: Option<Uuid>,
36 pub vendor_id: String,
38 pub company_code: String,
40 pub amount: Decimal,
42 pub currency: String,
44}
45
46impl P2pDocuments {
47 pub fn new(
49 po_number: &str,
50 vendor_id: &str,
51 company_code: &str,
52 amount: Decimal,
53 currency: &str,
54 ) -> Self {
55 Self {
56 po_number: po_number.into(),
57 po_id: Uuid::new_v4(),
58 gr_number: None,
59 gr_id: None,
60 invoice_number: None,
61 invoice_id: None,
62 payment_number: None,
63 payment_id: None,
64 vendor_id: vendor_id.into(),
65 company_code: company_code.into(),
66 amount,
67 currency: currency.into(),
68 }
69 }
70
71 pub fn with_goods_receipt(mut self, gr_number: &str) -> Self {
73 self.gr_number = Some(gr_number.into());
74 self.gr_id = Some(Uuid::new_v4());
75 self
76 }
77
78 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 pub fn with_payment(mut self, payment_number: &str) -> Self {
87 self.payment_number = Some(payment_number.into());
88 self.payment_id = Some(Uuid::new_v4());
89 self
90 }
91}
92
93impl OcpmEventGenerator {
94 pub fn generate_p2p_case(
96 &mut self,
97 documents: &P2pDocuments,
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 let po_type = ObjectType::purchase_order();
111 let gr_type = ObjectType::goods_receipt();
112 let invoice_type = ObjectType::vendor_invoice();
113
114 let po_object = self.create_object(
116 &po_type,
117 &documents.po_number,
118 &documents.company_code,
119 current_time,
120 );
121 objects.push(po_object.clone());
122
123 let create_po = ActivityType::create_po();
125 let resource = self.select_resource(&create_po, available_users);
126 let mut event = self.create_event(
127 &create_po,
128 current_time,
129 &resource,
130 &documents.company_code,
131 case_id,
132 );
133 event = event
134 .with_object(
135 EventObjectRef::created(po_object.object_id, &po_type.type_id)
136 .with_external_id(&documents.po_number),
137 )
138 .with_document_ref(&documents.po_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 "vendor_id",
147 ObjectAttributeValue::String(documents.vendor_id.clone()),
148 );
149 events.push(event);
150
151 current_time = self.calculate_event_time(current_time, &create_po);
153 current_time += self.generate_inter_activity_delay(30, 480); let approve_po = ActivityType::approve_po();
156 let resource = self.select_resource(&approve_po, available_users);
157 let mut event = self.create_event(
158 &approve_po,
159 current_time,
160 &resource,
161 &documents.company_code,
162 case_id,
163 );
164 event = event
165 .with_object(
166 EventObjectRef::updated(po_object.object_id, &po_type.type_id)
167 .with_external_id(&documents.po_number),
168 )
169 .with_document_ref(&documents.po_number);
170 events.push(event);
171
172 current_time = self.calculate_event_time(current_time, &approve_po);
174
175 let release_po = ActivityType::release_po();
176 let resource = self.select_resource(&release_po, available_users);
177 let mut event = self.create_event(
178 &release_po,
179 current_time,
180 &resource,
181 &documents.company_code,
182 case_id,
183 );
184 event = event
185 .with_object(
186 EventObjectRef::updated(po_object.object_id, &po_type.type_id)
187 .with_external_id(&documents.po_number),
188 )
189 .with_document_ref(&documents.po_number);
190 events.push(event);
191
192 if matches!(variant_type, VariantType::ErrorPath) {
194 let case_trace = self.create_case_trace(
195 case_id,
196 &events,
197 BusinessProcess::P2P,
198 po_object.object_id,
199 &po_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 current_time = self.calculate_event_time(current_time, &release_po);
213 current_time += self.generate_inter_activity_delay(1440, 10080); if let Some(gr_number) = &documents.gr_number {
216 let gr_object =
217 self.create_object(&gr_type, gr_number, &documents.company_code, current_time);
218 objects.push(gr_object.clone());
219
220 relationships.push(ObjectRelationship::new(
222 "references",
223 gr_object.object_id,
224 &gr_type.type_id,
225 po_object.object_id,
226 &po_type.type_id,
227 ));
228
229 let create_gr = ActivityType::create_gr();
230 let resource = self.select_resource(&create_gr, available_users);
231 let mut event = self.create_event(
232 &create_gr,
233 current_time,
234 &resource,
235 &documents.company_code,
236 case_id,
237 );
238 event = event
239 .with_object(
240 EventObjectRef::created(gr_object.object_id, &gr_type.type_id)
241 .with_external_id(gr_number),
242 )
243 .with_object(
244 EventObjectRef::updated(po_object.object_id, &po_type.type_id)
245 .with_external_id(&documents.po_number),
246 )
247 .with_document_ref(gr_number);
248 events.push(event);
249
250 current_time = self.calculate_event_time(current_time, &create_gr);
252
253 let post_gr = ActivityType::post_gr();
254 let resource = self.select_resource(&post_gr, available_users);
255 let mut event = self.create_event(
256 &post_gr,
257 current_time,
258 &resource,
259 &documents.company_code,
260 case_id,
261 );
262 event = event
263 .with_object(
264 EventObjectRef::updated(gr_object.object_id, &gr_type.type_id)
265 .with_external_id(gr_number),
266 )
267 .with_document_ref(gr_number);
268 events.push(event);
269 }
270
271 current_time = self.calculate_event_time(current_time, &ActivityType::post_gr());
273 current_time += self.generate_inter_activity_delay(1440, 20160); if let Some(invoice_number) = &documents.invoice_number {
276 let invoice_object = self.create_object(
277 &invoice_type,
278 invoice_number,
279 &documents.company_code,
280 current_time,
281 );
282 objects.push(invoice_object.clone());
283
284 relationships.push(ObjectRelationship::new(
286 "references",
287 invoice_object.object_id,
288 &invoice_type.type_id,
289 po_object.object_id,
290 &po_type.type_id,
291 ));
292
293 let receive_invoice = ActivityType::receive_invoice();
294 let resource = self.select_resource(&receive_invoice, available_users);
295 let mut event = self.create_event(
296 &receive_invoice,
297 current_time,
298 &resource,
299 &documents.company_code,
300 case_id,
301 );
302 event = event
303 .with_object(
304 EventObjectRef::created(invoice_object.object_id, &invoice_type.type_id)
305 .with_external_id(invoice_number),
306 )
307 .with_document_ref(invoice_number);
308 Self::add_event_attribute(
309 &mut event,
310 "invoice_amount",
311 ObjectAttributeValue::Decimal(documents.amount),
312 );
313 events.push(event);
314
315 if !matches!(variant_type, VariantType::ExceptionPath)
317 || !self.should_skip_activity(0.3)
318 {
319 current_time = self.calculate_event_time(current_time, &receive_invoice);
321 current_time += self.generate_inter_activity_delay(60, 480);
322
323 let verify_invoice = ActivityType::verify_invoice();
324 let resource = self.select_resource(&verify_invoice, available_users);
325 let mut event = self.create_event(
326 &verify_invoice,
327 current_time,
328 &resource,
329 &documents.company_code,
330 case_id,
331 );
332 event = event
333 .with_object(
334 EventObjectRef::updated(invoice_object.object_id, &invoice_type.type_id)
335 .with_external_id(invoice_number),
336 )
337 .with_object(
338 EventObjectRef::read(po_object.object_id, &po_type.type_id)
339 .with_external_id(&documents.po_number),
340 );
341
342 if let Some(gr_id) = documents.gr_id {
343 event = event.with_object(EventObjectRef::read(gr_id, &gr_type.type_id));
344 }
345
346 Self::add_event_attribute(
347 &mut event,
348 "match_result",
349 ObjectAttributeValue::String("matched".into()),
350 );
351 events.push(event);
352 }
353
354 current_time = self.calculate_event_time(current_time, &ActivityType::verify_invoice());
356
357 let post_invoice = ActivityType::post_invoice();
358 let resource = self.select_resource(&post_invoice, available_users);
359 let mut event = self.create_event(
360 &post_invoice,
361 current_time,
362 &resource,
363 &documents.company_code,
364 case_id,
365 );
366 event = event
367 .with_object(
368 EventObjectRef::updated(invoice_object.object_id, &invoice_type.type_id)
369 .with_external_id(invoice_number),
370 )
371 .with_object(
372 EventObjectRef::updated(po_object.object_id, &po_type.type_id)
373 .with_external_id(&documents.po_number),
374 )
375 .with_document_ref(invoice_number);
376 events.push(event);
377
378 if documents.payment_number.is_some() {
380 current_time = self.calculate_event_time(current_time, &post_invoice);
381 current_time += self.generate_inter_activity_delay(1440, 43200); let execute_payment = ActivityType::execute_payment();
384 let resource = self.select_resource(&execute_payment, available_users);
385 let mut event = self.create_event(
386 &execute_payment,
387 current_time,
388 &resource,
389 &documents.company_code,
390 case_id,
391 );
392 event = event
393 .with_object(
394 EventObjectRef::consumed(invoice_object.object_id, &invoice_type.type_id)
395 .with_external_id(invoice_number),
396 )
397 .with_object(
398 EventObjectRef::consumed(po_object.object_id, &po_type.type_id)
399 .with_external_id(&documents.po_number),
400 )
401 .with_document_ref(documents.payment_number.as_deref().unwrap_or(""));
402 Self::add_event_attribute(
403 &mut event,
404 "payment_amount",
405 ObjectAttributeValue::Decimal(documents.amount),
406 );
407 events.push(event);
408 }
409 }
410
411 let case_trace = self.create_case_trace(
412 case_id,
413 &events,
414 BusinessProcess::P2P,
415 po_object.object_id,
416 &po_type.type_id,
417 &documents.company_code,
418 );
419
420 CaseGenerationResult {
421 events,
422 objects,
423 relationships,
424 case_trace,
425 variant_type,
426 }
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn test_p2p_case_generation() {
436 let mut generator = OcpmEventGenerator::new(42);
437 let documents = P2pDocuments::new(
438 "PO-000001",
439 "V000001",
440 "1000",
441 Decimal::new(10000, 0),
442 "USD",
443 )
444 .with_goods_receipt("GR-000001")
445 .with_invoice("INV-000001")
446 .with_payment("PAY-000001");
447
448 let result = generator.generate_p2p_case(
449 &documents,
450 Utc::now(),
451 &["user001".into(), "user002".into()],
452 );
453
454 assert!(result.events.len() >= 3);
456 assert!(!result.objects.is_empty());
458 assert!(!result.case_trace.activity_sequence.is_empty());
460 }
461
462 #[test]
463 fn test_p2p_error_path() {
464 let mut generator = OcpmEventGenerator::with_config(
466 123,
467 super::super::OcpmGeneratorConfig {
468 error_path_rate: 1.0,
469 happy_path_rate: 0.0,
470 exception_path_rate: 0.0,
471 ..Default::default()
472 },
473 );
474
475 let documents =
476 P2pDocuments::new("PO-000002", "V000001", "1000", Decimal::new(5000, 0), "USD")
477 .with_goods_receipt("GR-000002")
478 .with_invoice("INV-000002");
479
480 let result = generator.generate_p2p_case(&documents, Utc::now(), &[]);
481
482 assert_eq!(result.variant_type, VariantType::ErrorPath);
484 assert_eq!(result.events.len(), 3);
485 }
486}