1use chrono::NaiveDate;
8use datasynth_core::models::{
9 documents::DocumentReference, CustomerPool, MaterialPool, VendorPool,
10};
11use datasynth_core::utils::seeded_rng;
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: seeded_rng(seed, 0),
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 Some(vendors) = vendors_by_company.get(company_code) else {
226 tracing::warn!(
227 "Vendor pool not found for company '{}'; skipping",
228 company_code
229 );
230 continue;
231 };
232 let Some(customers) = customers_by_company.get(company_code) else {
233 tracing::warn!(
234 "Customer pool not found for company '{}'; skipping",
235 company_code
236 );
237 continue;
238 };
239
240 let flows = self.generate_flows(
241 company_code,
242 chains_per_company,
243 vendors,
244 customers,
245 materials,
246 date_range,
247 fiscal_year,
248 created_by,
249 );
250
251 results.push(flows);
252 }
253
254 results
255 }
256
257 fn collect_document_references(
259 &self,
260 p2p_chains: &[P2PDocumentChain],
261 o2c_chains: &[O2CDocumentChain],
262 ) -> Vec<DocumentReference> {
263 let mut references = Vec::new();
264
265 for chain in p2p_chains {
267 for ref_doc in &chain.purchase_order.header.document_references {
269 references.push(ref_doc.clone());
270 }
271
272 for gr in &chain.goods_receipts {
274 for ref_doc in &gr.header.document_references {
275 references.push(ref_doc.clone());
276 }
277 }
278
279 if let Some(invoice) = &chain.vendor_invoice {
281 for ref_doc in &invoice.header.document_references {
282 references.push(ref_doc.clone());
283 }
284 }
285
286 if let Some(payment) = &chain.payment {
288 for ref_doc in &payment.header.document_references {
289 references.push(ref_doc.clone());
290 }
291 }
292 }
293
294 for chain in o2c_chains {
296 for ref_doc in &chain.sales_order.header.document_references {
298 references.push(ref_doc.clone());
299 }
300
301 for dlv in &chain.deliveries {
303 for ref_doc in &dlv.header.document_references {
304 references.push(ref_doc.clone());
305 }
306 }
307
308 if let Some(invoice) = &chain.customer_invoice {
310 for ref_doc in &invoice.header.document_references {
311 references.push(ref_doc.clone());
312 }
313 }
314
315 if let Some(receipt) = &chain.customer_receipt {
317 for ref_doc in &receipt.header.document_references {
318 references.push(ref_doc.clone());
319 }
320 }
321 }
322
323 references
324 }
325
326 fn calculate_stats(
328 &self,
329 p2p_chains: &[P2PDocumentChain],
330 o2c_chains: &[O2CDocumentChain],
331 ) -> DocumentChainStats {
332 let mut stats = DocumentChainStats {
333 p2p_chains: p2p_chains.len(),
334 ..Default::default()
335 };
336
337 for chain in p2p_chains {
339 stats.purchase_orders += 1;
340 stats.goods_receipts += chain.goods_receipts.len();
341
342 if chain.three_way_match_passed {
343 stats.p2p_three_way_match_passed += 1;
344 }
345
346 if chain.vendor_invoice.is_some() {
347 stats.vendor_invoices += 1;
348 }
349
350 if chain.payment.is_some() {
351 stats.ap_payments += 1;
352 }
353
354 if chain.is_complete {
355 stats.p2p_completed += 1;
356 }
357 }
358
359 stats.o2c_chains = o2c_chains.len();
361 for chain in o2c_chains {
362 stats.sales_orders += 1;
363 stats.deliveries += chain.deliveries.len();
364
365 if chain.credit_check_passed {
366 stats.o2c_credit_check_passed += 1;
367 }
368
369 if chain.customer_invoice.is_some() {
370 stats.customer_invoices += 1;
371 }
372
373 if chain.customer_receipt.is_some() {
374 stats.ar_receipts += 1;
375 }
376
377 if chain.is_complete {
378 stats.o2c_completed += 1;
379 }
380 }
381
382 stats
383 }
384
385 pub fn p2p_generator(&mut self) -> &mut P2PGenerator {
387 &mut self.p2p_generator
388 }
389
390 pub fn o2c_generator(&mut self) -> &mut O2CGenerator {
392 &mut self.o2c_generator
393 }
394
395 pub fn reset(&mut self) {
397 self.rng = seeded_rng(self.seed, 0);
398 self.p2p_generator.reset();
399 self.o2c_generator.reset();
400 }
401}
402
403pub fn extract_je_sources(flows: &GeneratedDocumentFlows) -> JournalEntrySources {
405 let mut sources = JournalEntrySources::default();
406
407 for chain in &flows.p2p_chains {
408 for gr in &chain.goods_receipts {
410 sources.goods_receipts.push(gr.clone());
411 }
412
413 if let Some(invoice) = &chain.vendor_invoice {
415 sources.vendor_invoices.push(invoice.clone());
416 }
417
418 if let Some(payment) = &chain.payment {
420 sources.ap_payments.push(payment.clone());
421 }
422 }
423
424 for chain in &flows.o2c_chains {
425 for dlv in &chain.deliveries {
427 sources.deliveries.push(dlv.clone());
428 }
429
430 if let Some(invoice) = &chain.customer_invoice {
432 sources.customer_invoices.push(invoice.clone());
433 }
434
435 if let Some(receipt) = &chain.customer_receipt {
437 sources.ar_receipts.push(receipt.clone());
438 }
439 }
440
441 sources
442}
443
444#[derive(Debug, Default)]
446pub struct JournalEntrySources {
447 pub goods_receipts: Vec<datasynth_core::models::documents::GoodsReceipt>,
449 pub vendor_invoices: Vec<datasynth_core::models::documents::VendorInvoice>,
451 pub ap_payments: Vec<datasynth_core::models::documents::Payment>,
453 pub deliveries: Vec<datasynth_core::models::documents::Delivery>,
455 pub customer_invoices: Vec<datasynth_core::models::documents::CustomerInvoice>,
457 pub ar_receipts: Vec<datasynth_core::models::documents::Payment>,
459}
460
461#[cfg(test)]
462#[allow(clippy::unwrap_used)]
463mod tests {
464 use super::*;
465 use datasynth_core::models::{
466 CreditRating, Customer, CustomerPaymentBehavior, Material, MaterialType, Vendor,
467 };
468
469 fn create_test_pools() -> (VendorPool, CustomerPool, MaterialPool) {
470 let mut vendors = VendorPool::new();
471 for i in 1..=5 {
472 vendors.add_vendor(Vendor::new(
473 &format!("V-{:06}", i),
474 &format!("Vendor {}", i),
475 datasynth_core::models::VendorType::Supplier,
476 ));
477 }
478
479 let mut customers = CustomerPool::new();
480 for i in 1..=5 {
481 let mut customer = Customer::new(
482 &format!("C-{:06}", i),
483 &format!("Customer {}", i),
484 datasynth_core::models::CustomerType::Corporate,
485 );
486 customer.credit_rating = CreditRating::A;
487 customer.credit_limit = rust_decimal::Decimal::from(1_000_000);
488 customer.payment_behavior = CustomerPaymentBehavior::OnTime;
489 customers.add_customer(customer);
490 }
491
492 let mut materials = MaterialPool::new();
493 for i in 1..=10 {
494 let mut mat = Material::new(
495 format!("MAT-{:06}", i),
496 format!("Material {}", i),
497 MaterialType::FinishedGood,
498 );
499 mat.standard_cost = rust_decimal::Decimal::from(50 + i * 10);
500 mat.list_price = rust_decimal::Decimal::from(100 + i * 20);
501 materials.add_material(mat);
502 }
503
504 (vendors, customers, materials)
505 }
506
507 #[test]
508 fn test_manager_creation() {
509 let manager = DocumentChainManager::new(42);
510 assert!(manager.config.p2p_to_o2c_ratio == 1.0);
511 }
512
513 #[test]
514 fn test_generate_flows() {
515 let mut manager = DocumentChainManager::new(42);
516 let (vendors, customers, materials) = create_test_pools();
517
518 let flows = manager.generate_flows(
519 "1000",
520 20,
521 &vendors,
522 &customers,
523 &materials,
524 (
525 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
526 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
527 ),
528 2024,
529 "JSMITH",
530 );
531
532 assert_eq!(flows.p2p_chains.len() + flows.o2c_chains.len(), 20);
533 assert!(flows.stats.purchase_orders > 0);
534 assert!(flows.stats.sales_orders > 0);
535 }
536
537 #[test]
538 fn test_balanced_flows() {
539 let mut manager = DocumentChainManager::new(42);
540 let (vendors, customers, materials) = create_test_pools();
541
542 let flows = manager.generate_balanced_flows(
543 10,
544 "1000",
545 &vendors,
546 &customers,
547 &materials,
548 (
549 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
550 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
551 ),
552 2024,
553 "JSMITH",
554 );
555
556 assert_eq!(flows.p2p_chains.len(), 10);
557 assert_eq!(flows.o2c_chains.len(), 10);
558 }
559
560 #[test]
561 fn test_document_references_collected() {
562 let mut manager = DocumentChainManager::new(42);
563 let (vendors, customers, materials) = create_test_pools();
564
565 let flows = manager.generate_balanced_flows(
566 5,
567 "1000",
568 &vendors,
569 &customers,
570 &materials,
571 (
572 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
573 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
574 ),
575 2024,
576 "JSMITH",
577 );
578
579 assert!(!flows.document_references.is_empty());
581 }
582
583 #[test]
584 fn test_stats_calculation() {
585 let mut manager = DocumentChainManager::new(42);
586 let (vendors, customers, materials) = create_test_pools();
587
588 let flows = manager.generate_balanced_flows(
589 5,
590 "1000",
591 &vendors,
592 &customers,
593 &materials,
594 (
595 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
596 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
597 ),
598 2024,
599 "JSMITH",
600 );
601
602 let stats = &flows.stats;
603 assert_eq!(stats.p2p_chains, 5);
604 assert_eq!(stats.o2c_chains, 5);
605 assert_eq!(stats.purchase_orders, 5);
606 assert_eq!(stats.sales_orders, 5);
607 }
608
609 #[test]
610 fn test_je_sources_extraction() {
611 let mut manager = DocumentChainManager::new(42);
612 let (vendors, customers, materials) = create_test_pools();
613
614 let flows = manager.generate_balanced_flows(
615 5,
616 "1000",
617 &vendors,
618 &customers,
619 &materials,
620 (
621 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
622 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
623 ),
624 2024,
625 "JSMITH",
626 );
627
628 let sources = extract_je_sources(&flows);
629
630 assert!(!sources.goods_receipts.is_empty());
632 assert!(!sources.vendor_invoices.is_empty());
633 assert!(!sources.deliveries.is_empty());
634 assert!(!sources.customer_invoices.is_empty());
635 }
636
637 #[test]
638 fn test_custom_ratio() {
639 let config = DocumentChainManagerConfig {
640 p2p_to_o2c_ratio: 2.0, ..Default::default()
642 };
643
644 let mut manager = DocumentChainManager::with_config(42, config);
645 let (vendors, customers, materials) = create_test_pools();
646
647 let flows = manager.generate_flows(
648 "1000",
649 30,
650 &vendors,
651 &customers,
652 &materials,
653 (
654 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
655 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
656 ),
657 2024,
658 "JSMITH",
659 );
660
661 assert!(flows.p2p_chains.len() > flows.o2c_chains.len());
663 }
664
665 #[test]
666 fn test_reset() {
667 let mut manager = DocumentChainManager::new(42);
668 let (vendors, customers, materials) = create_test_pools();
669
670 let flows1 = manager.generate_balanced_flows(
671 5,
672 "1000",
673 &vendors,
674 &customers,
675 &materials,
676 (
677 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
678 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
679 ),
680 2024,
681 "JSMITH",
682 );
683
684 manager.reset();
685
686 let flows2 = manager.generate_balanced_flows(
687 5,
688 "1000",
689 &vendors,
690 &customers,
691 &materials,
692 (
693 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
694 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap(),
695 ),
696 2024,
697 "JSMITH",
698 );
699
700 assert_eq!(
702 flows1.p2p_chains[0].purchase_order.header.document_id,
703 flows2.p2p_chains[0].purchase_order.header.document_id
704 );
705 }
706}