1use chrono::NaiveDate;
8use datasynth_core::models::{
9 documents::DocumentReference, CustomerPool, MaterialPool, VendorPool,
10};
11use rand::prelude::*;
12use rand_chacha::ChaCha8Rng;
13
14use super::{
15 O2CDocumentChain, O2CGenerator, O2CGeneratorConfig, P2PDocumentChain, P2PGenerator,
16 P2PGeneratorConfig,
17};
18
19#[derive(Debug, Clone)]
21pub struct DocumentChainManagerConfig {
22 pub p2p_config: P2PGeneratorConfig,
24 pub o2c_config: O2CGeneratorConfig,
26 pub p2p_to_o2c_ratio: f64,
28}
29
30impl Default for DocumentChainManagerConfig {
31 fn default() -> Self {
32 Self {
33 p2p_config: P2PGeneratorConfig::default(),
34 o2c_config: O2CGeneratorConfig::default(),
35 p2p_to_o2c_ratio: 1.0,
36 }
37 }
38}
39
40#[derive(Debug, Default)]
42pub struct DocumentChainStats {
43 pub p2p_chains: usize,
45 pub p2p_three_way_match_passed: usize,
47 pub p2p_completed: usize,
49 pub o2c_chains: usize,
51 pub o2c_credit_check_passed: usize,
53 pub o2c_completed: usize,
55 pub purchase_orders: usize,
57 pub goods_receipts: usize,
59 pub vendor_invoices: usize,
61 pub ap_payments: usize,
63 pub sales_orders: usize,
65 pub deliveries: usize,
67 pub customer_invoices: usize,
69 pub ar_receipts: usize,
71}
72
73#[derive(Debug)]
75pub struct GeneratedDocumentFlows {
76 pub p2p_chains: Vec<P2PDocumentChain>,
78 pub o2c_chains: Vec<O2CDocumentChain>,
80 pub document_references: Vec<DocumentReference>,
82 pub stats: DocumentChainStats,
84}
85
86pub struct DocumentChainManager {
88 rng: ChaCha8Rng,
89 seed: u64,
90 config: DocumentChainManagerConfig,
91 p2p_generator: P2PGenerator,
92 o2c_generator: O2CGenerator,
93}
94
95impl DocumentChainManager {
96 pub fn new(seed: u64) -> Self {
98 Self::with_config(seed, DocumentChainManagerConfig::default())
99 }
100
101 pub fn with_config(seed: u64, config: DocumentChainManagerConfig) -> Self {
103 Self {
104 rng: ChaCha8Rng::seed_from_u64(seed),
105 seed,
106 p2p_generator: P2PGenerator::with_config(seed, config.p2p_config.clone()),
107 o2c_generator: O2CGenerator::with_config(seed + 1000, config.o2c_config.clone()),
108 config,
109 }
110 }
111
112 pub fn generate_flows(
114 &mut self,
115 company_code: &str,
116 total_chains: usize,
117 vendors: &VendorPool,
118 customers: &CustomerPool,
119 materials: &MaterialPool,
120 date_range: (NaiveDate, NaiveDate),
121 fiscal_year: u16,
122 created_by: &str,
123 ) -> GeneratedDocumentFlows {
124 let ratio = self.config.p2p_to_o2c_ratio;
126 let p2p_count = ((total_chains as f64) * ratio / (1.0 + ratio)) as usize;
127 let o2c_count = total_chains - p2p_count;
128
129 let p2p_chains = self.p2p_generator.generate_chains(
131 p2p_count,
132 company_code,
133 vendors,
134 materials,
135 date_range,
136 fiscal_year,
137 created_by,
138 );
139
140 let o2c_chains = self.o2c_generator.generate_chains(
142 o2c_count,
143 company_code,
144 customers,
145 materials,
146 date_range,
147 fiscal_year,
148 created_by,
149 );
150
151 let document_references = self.collect_document_references(&p2p_chains, &o2c_chains);
153
154 let stats = self.calculate_stats(&p2p_chains, &o2c_chains);
156
157 GeneratedDocumentFlows {
158 p2p_chains,
159 o2c_chains,
160 document_references,
161 stats,
162 }
163 }
164
165 pub fn generate_balanced_flows(
167 &mut self,
168 chains_per_type: usize,
169 company_code: &str,
170 vendors: &VendorPool,
171 customers: &CustomerPool,
172 materials: &MaterialPool,
173 date_range: (NaiveDate, NaiveDate),
174 fiscal_year: u16,
175 created_by: &str,
176 ) -> GeneratedDocumentFlows {
177 let p2p_chains = self.p2p_generator.generate_chains(
179 chains_per_type,
180 company_code,
181 vendors,
182 materials,
183 date_range,
184 fiscal_year,
185 created_by,
186 );
187
188 let o2c_chains = self.o2c_generator.generate_chains(
190 chains_per_type,
191 company_code,
192 customers,
193 materials,
194 date_range,
195 fiscal_year,
196 created_by,
197 );
198
199 let document_references = self.collect_document_references(&p2p_chains, &o2c_chains);
200 let stats = self.calculate_stats(&p2p_chains, &o2c_chains);
201
202 GeneratedDocumentFlows {
203 p2p_chains,
204 o2c_chains,
205 document_references,
206 stats,
207 }
208 }
209
210 pub fn generate_multi_company_flows(
212 &mut self,
213 company_codes: &[String],
214 chains_per_company: usize,
215 vendors_by_company: &std::collections::HashMap<String, VendorPool>,
216 customers_by_company: &std::collections::HashMap<String, CustomerPool>,
217 materials: &MaterialPool, date_range: (NaiveDate, NaiveDate),
219 fiscal_year: u16,
220 created_by: &str,
221 ) -> Vec<GeneratedDocumentFlows> {
222 let mut results = Vec::new();
223
224 for company_code in company_codes {
225 let vendors = vendors_by_company
226 .get(company_code)
227 .expect("Vendor pool not found for company");
228 let customers = customers_by_company
229 .get(company_code)
230 .expect("Customer pool not found for company");
231
232 let flows = self.generate_flows(
233 company_code,
234 chains_per_company,
235 vendors,
236 customers,
237 materials,
238 date_range,
239 fiscal_year,
240 created_by,
241 );
242
243 results.push(flows);
244 }
245
246 results
247 }
248
249 fn collect_document_references(
251 &self,
252 p2p_chains: &[P2PDocumentChain],
253 o2c_chains: &[O2CDocumentChain],
254 ) -> Vec<DocumentReference> {
255 let mut references = Vec::new();
256
257 for chain in p2p_chains {
259 for ref_doc in &chain.purchase_order.header.document_references {
261 references.push(ref_doc.clone());
262 }
263
264 for gr in &chain.goods_receipts {
266 for ref_doc in &gr.header.document_references {
267 references.push(ref_doc.clone());
268 }
269 }
270
271 if let Some(invoice) = &chain.vendor_invoice {
273 for ref_doc in &invoice.header.document_references {
274 references.push(ref_doc.clone());
275 }
276 }
277
278 if let Some(payment) = &chain.payment {
280 for ref_doc in &payment.header.document_references {
281 references.push(ref_doc.clone());
282 }
283 }
284 }
285
286 for chain in o2c_chains {
288 for ref_doc in &chain.sales_order.header.document_references {
290 references.push(ref_doc.clone());
291 }
292
293 for dlv in &chain.deliveries {
295 for ref_doc in &dlv.header.document_references {
296 references.push(ref_doc.clone());
297 }
298 }
299
300 if let Some(invoice) = &chain.customer_invoice {
302 for ref_doc in &invoice.header.document_references {
303 references.push(ref_doc.clone());
304 }
305 }
306
307 if let Some(receipt) = &chain.customer_receipt {
309 for ref_doc in &receipt.header.document_references {
310 references.push(ref_doc.clone());
311 }
312 }
313 }
314
315 references
316 }
317
318 fn calculate_stats(
320 &self,
321 p2p_chains: &[P2PDocumentChain],
322 o2c_chains: &[O2CDocumentChain],
323 ) -> DocumentChainStats {
324 let mut stats = DocumentChainStats {
325 p2p_chains: p2p_chains.len(),
326 ..Default::default()
327 };
328
329 for chain in p2p_chains {
331 stats.purchase_orders += 1;
332 stats.goods_receipts += chain.goods_receipts.len();
333
334 if chain.three_way_match_passed {
335 stats.p2p_three_way_match_passed += 1;
336 }
337
338 if chain.vendor_invoice.is_some() {
339 stats.vendor_invoices += 1;
340 }
341
342 if chain.payment.is_some() {
343 stats.ap_payments += 1;
344 }
345
346 if chain.is_complete {
347 stats.p2p_completed += 1;
348 }
349 }
350
351 stats.o2c_chains = o2c_chains.len();
353 for chain in o2c_chains {
354 stats.sales_orders += 1;
355 stats.deliveries += chain.deliveries.len();
356
357 if chain.credit_check_passed {
358 stats.o2c_credit_check_passed += 1;
359 }
360
361 if chain.customer_invoice.is_some() {
362 stats.customer_invoices += 1;
363 }
364
365 if chain.customer_receipt.is_some() {
366 stats.ar_receipts += 1;
367 }
368
369 if chain.is_complete {
370 stats.o2c_completed += 1;
371 }
372 }
373
374 stats
375 }
376
377 pub fn p2p_generator(&mut self) -> &mut P2PGenerator {
379 &mut self.p2p_generator
380 }
381
382 pub fn o2c_generator(&mut self) -> &mut O2CGenerator {
384 &mut self.o2c_generator
385 }
386
387 pub fn reset(&mut self) {
389 self.rng = ChaCha8Rng::seed_from_u64(self.seed);
390 self.p2p_generator.reset();
391 self.o2c_generator.reset();
392 }
393}
394
395pub fn extract_je_sources(flows: &GeneratedDocumentFlows) -> JournalEntrySources {
397 let mut sources = JournalEntrySources::default();
398
399 for chain in &flows.p2p_chains {
400 for gr in &chain.goods_receipts {
402 sources.goods_receipts.push(gr.clone());
403 }
404
405 if let Some(invoice) = &chain.vendor_invoice {
407 sources.vendor_invoices.push(invoice.clone());
408 }
409
410 if let Some(payment) = &chain.payment {
412 sources.ap_payments.push(payment.clone());
413 }
414 }
415
416 for chain in &flows.o2c_chains {
417 for dlv in &chain.deliveries {
419 sources.deliveries.push(dlv.clone());
420 }
421
422 if let Some(invoice) = &chain.customer_invoice {
424 sources.customer_invoices.push(invoice.clone());
425 }
426
427 if let Some(receipt) = &chain.customer_receipt {
429 sources.ar_receipts.push(receipt.clone());
430 }
431 }
432
433 sources
434}
435
436#[derive(Debug, Default)]
438pub struct JournalEntrySources {
439 pub goods_receipts: Vec<datasynth_core::models::documents::GoodsReceipt>,
441 pub vendor_invoices: Vec<datasynth_core::models::documents::VendorInvoice>,
443 pub ap_payments: Vec<datasynth_core::models::documents::Payment>,
445 pub deliveries: Vec<datasynth_core::models::documents::Delivery>,
447 pub customer_invoices: Vec<datasynth_core::models::documents::CustomerInvoice>,
449 pub ar_receipts: Vec<datasynth_core::models::documents::Payment>,
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use datasynth_core::models::{
457 CreditRating, Customer, CustomerPaymentBehavior, Material, MaterialType, Vendor,
458 };
459
460 fn create_test_pools() -> (VendorPool, CustomerPool, MaterialPool) {
461 let mut vendors = VendorPool::new();
462 for i in 1..=5 {
463 vendors.add_vendor(Vendor::new(
464 &format!("V-{:06}", i),
465 &format!("Vendor {}", i),
466 datasynth_core::models::VendorType::Supplier,
467 ));
468 }
469
470 let mut customers = CustomerPool::new();
471 for i in 1..=5 {
472 let mut customer = Customer::new(
473 &format!("C-{:06}", i),
474 &format!("Customer {}", i),
475 datasynth_core::models::CustomerType::Corporate,
476 );
477 customer.credit_rating = CreditRating::A;
478 customer.credit_limit = rust_decimal::Decimal::from(1_000_000);
479 customer.payment_behavior = CustomerPaymentBehavior::OnTime;
480 customers.add_customer(customer);
481 }
482
483 let mut materials = MaterialPool::new();
484 for i in 1..=10 {
485 let mut mat = Material::new(
486 format!("MAT-{:06}", i),
487 format!("Material {}", i),
488 MaterialType::FinishedGood,
489 );
490 mat.standard_cost = rust_decimal::Decimal::from(50 + i * 10);
491 mat.list_price = rust_decimal::Decimal::from(100 + i * 20);
492 materials.add_material(mat);
493 }
494
495 (vendors, customers, materials)
496 }
497
498 #[test]
499 fn test_manager_creation() {
500 let manager = DocumentChainManager::new(42);
501 assert!(manager.config.p2p_to_o2c_ratio == 1.0);
502 }
503
504 #[test]
505 fn test_generate_flows() {
506 let mut manager = DocumentChainManager::new(42);
507 let (vendors, customers, materials) = create_test_pools();
508
509 let flows = manager.generate_flows(
510 "1000",
511 20,
512 &vendors,
513 &customers,
514 &materials,
515 (
516 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
517 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
518 ),
519 2024,
520 "JSMITH",
521 );
522
523 assert_eq!(flows.p2p_chains.len() + flows.o2c_chains.len(), 20);
524 assert!(flows.stats.purchase_orders > 0);
525 assert!(flows.stats.sales_orders > 0);
526 }
527
528 #[test]
529 fn test_balanced_flows() {
530 let mut manager = DocumentChainManager::new(42);
531 let (vendors, customers, materials) = create_test_pools();
532
533 let flows = manager.generate_balanced_flows(
534 10,
535 "1000",
536 &vendors,
537 &customers,
538 &materials,
539 (
540 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
541 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
542 ),
543 2024,
544 "JSMITH",
545 );
546
547 assert_eq!(flows.p2p_chains.len(), 10);
548 assert_eq!(flows.o2c_chains.len(), 10);
549 }
550
551 #[test]
552 fn test_document_references_collected() {
553 let mut manager = DocumentChainManager::new(42);
554 let (vendors, customers, materials) = create_test_pools();
555
556 let flows = manager.generate_balanced_flows(
557 5,
558 "1000",
559 &vendors,
560 &customers,
561 &materials,
562 (
563 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
564 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
565 ),
566 2024,
567 "JSMITH",
568 );
569
570 assert!(!flows.document_references.is_empty());
572 }
573
574 #[test]
575 fn test_stats_calculation() {
576 let mut manager = DocumentChainManager::new(42);
577 let (vendors, customers, materials) = create_test_pools();
578
579 let flows = manager.generate_balanced_flows(
580 5,
581 "1000",
582 &vendors,
583 &customers,
584 &materials,
585 (
586 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
587 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
588 ),
589 2024,
590 "JSMITH",
591 );
592
593 let stats = &flows.stats;
594 assert_eq!(stats.p2p_chains, 5);
595 assert_eq!(stats.o2c_chains, 5);
596 assert_eq!(stats.purchase_orders, 5);
597 assert_eq!(stats.sales_orders, 5);
598 }
599
600 #[test]
601 fn test_je_sources_extraction() {
602 let mut manager = DocumentChainManager::new(42);
603 let (vendors, customers, materials) = create_test_pools();
604
605 let flows = manager.generate_balanced_flows(
606 5,
607 "1000",
608 &vendors,
609 &customers,
610 &materials,
611 (
612 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
613 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
614 ),
615 2024,
616 "JSMITH",
617 );
618
619 let sources = extract_je_sources(&flows);
620
621 assert!(!sources.goods_receipts.is_empty());
623 assert!(!sources.vendor_invoices.is_empty());
624 assert!(!sources.deliveries.is_empty());
625 assert!(!sources.customer_invoices.is_empty());
626 }
627
628 #[test]
629 fn test_custom_ratio() {
630 let config = DocumentChainManagerConfig {
631 p2p_to_o2c_ratio: 2.0, ..Default::default()
633 };
634
635 let mut manager = DocumentChainManager::with_config(42, config);
636 let (vendors, customers, materials) = create_test_pools();
637
638 let flows = manager.generate_flows(
639 "1000",
640 30,
641 &vendors,
642 &customers,
643 &materials,
644 (
645 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
646 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
647 ),
648 2024,
649 "JSMITH",
650 );
651
652 assert!(flows.p2p_chains.len() > flows.o2c_chains.len());
654 }
655
656 #[test]
657 fn test_reset() {
658 let mut manager = DocumentChainManager::new(42);
659 let (vendors, customers, materials) = create_test_pools();
660
661 let flows1 = manager.generate_balanced_flows(
662 5,
663 "1000",
664 &vendors,
665 &customers,
666 &materials,
667 (
668 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
669 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
670 ),
671 2024,
672 "JSMITH",
673 );
674
675 manager.reset();
676
677 let flows2 = manager.generate_balanced_flows(
678 5,
679 "1000",
680 &vendors,
681 &customers,
682 &materials,
683 (
684 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
685 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
686 ),
687 2024,
688 "JSMITH",
689 );
690
691 assert_eq!(
693 flows1.p2p_chains[0].purchase_order.header.document_id,
694 flows2.p2p_chains[0].purchase_order.header.document_id
695 );
696 }
697}