1use chrono::{Datelike, NaiveDate};
7use datasynth_core::utils::{seeded_rng, weighted_select};
8use datasynth_core::FrameworkAccounts;
9use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11use rust_decimal::Decimal;
12use rust_decimal_macros::dec;
13use std::collections::HashMap;
14use tracing::debug;
15
16use datasynth_core::models::documents::{
17 CustomerInvoice, CustomerInvoiceItem, CustomerInvoiceType, GoodsReceipt, GoodsReceiptItem,
18 PurchaseOrder, PurchaseOrderItem, VendorInvoice, VendorInvoiceItem,
19};
20use datasynth_core::models::intercompany::{
21 ICLoan, ICMatchedPair, ICTransactionType, OwnershipStructure, RecurringFrequency,
22 TransferPricingMethod, TransferPricingPolicy,
23};
24use datasynth_core::models::{JournalEntry, JournalEntryLine};
25
26#[derive(Debug, Clone)]
28pub struct ICGeneratorConfig {
29 pub ic_transaction_rate: f64,
31 pub transfer_pricing_method: TransferPricingMethod,
33 pub markup_percent: Decimal,
35 pub generate_matched_pairs: bool,
37 pub transaction_type_weights: HashMap<ICTransactionType, f64>,
39 pub generate_netting: bool,
41 pub netting_frequency: RecurringFrequency,
43 pub generate_loans: bool,
45 pub loan_amount_range: (Decimal, Decimal),
47 pub loan_interest_rate_range: (Decimal, Decimal),
49 pub default_currency: String,
51}
52
53impl Default for ICGeneratorConfig {
54 fn default() -> Self {
55 let mut weights = HashMap::new();
56 weights.insert(ICTransactionType::GoodsSale, 0.35);
57 weights.insert(ICTransactionType::ServiceProvided, 0.20);
58 weights.insert(ICTransactionType::ManagementFee, 0.15);
59 weights.insert(ICTransactionType::Royalty, 0.10);
60 weights.insert(ICTransactionType::CostSharing, 0.10);
61 weights.insert(ICTransactionType::LoanInterest, 0.05);
62 weights.insert(ICTransactionType::ExpenseRecharge, 0.05);
63
64 Self {
65 ic_transaction_rate: 0.15,
66 transfer_pricing_method: TransferPricingMethod::CostPlus,
67 markup_percent: dec!(5),
68 generate_matched_pairs: true,
69 transaction_type_weights: weights,
70 generate_netting: true,
71 netting_frequency: RecurringFrequency::Monthly,
72 generate_loans: true,
73 loan_amount_range: (dec!(100000), dec!(10000000)),
74 loan_interest_rate_range: (dec!(2), dec!(8)),
75 default_currency: "USD".to_string(),
76 }
77 }
78}
79
80pub struct ICGenerator {
82 config: ICGeneratorConfig,
84 rng: ChaCha8Rng,
86 ownership_structure: OwnershipStructure,
88 transfer_pricing_policies: HashMap<String, TransferPricingPolicy>,
90 active_loans: Vec<ICLoan>,
92 matched_pairs: Vec<ICMatchedPair>,
94 ic_counter: u64,
96 doc_counter: u64,
98 framework_accounts: FrameworkAccounts,
100}
101
102impl ICGenerator {
103 pub fn new_with_framework(
105 config: ICGeneratorConfig,
106 ownership_structure: OwnershipStructure,
107 seed: u64,
108 framework: &str,
109 ) -> Self {
110 Self {
111 config,
112 rng: seeded_rng(seed, 0),
113 ownership_structure,
114 transfer_pricing_policies: HashMap::new(),
115 active_loans: Vec::new(),
116 matched_pairs: Vec::new(),
117 ic_counter: 0,
118 doc_counter: 0,
119 framework_accounts: FrameworkAccounts::for_framework(framework),
120 }
121 }
122
123 pub fn new(
125 config: ICGeneratorConfig,
126 ownership_structure: OwnershipStructure,
127 seed: u64,
128 ) -> Self {
129 Self::new_with_framework(config, ownership_structure, seed, "us_gaap")
130 }
131
132 pub fn add_transfer_pricing_policy(
134 &mut self,
135 relationship_id: String,
136 policy: TransferPricingPolicy,
137 ) {
138 self.transfer_pricing_policies
139 .insert(relationship_id, policy);
140 }
141
142 fn generate_ic_reference(&mut self, date: NaiveDate) -> String {
144 self.ic_counter += 1;
145 format!("IC{}{:06}", date.format("%Y%m"), self.ic_counter)
146 }
147
148 fn generate_doc_number(&mut self, prefix: &str) -> String {
150 self.doc_counter += 1;
151 format!("{}{:08}", prefix, self.doc_counter)
152 }
153
154 fn select_transaction_type(&mut self) -> ICTransactionType {
156 let options: Vec<(ICTransactionType, f64)> = self
157 .config
158 .transaction_type_weights
159 .iter()
160 .map(|(&tx_type, &weight)| (tx_type, weight))
161 .collect();
162
163 if options.is_empty() {
164 return ICTransactionType::GoodsSale;
165 }
166
167 *weighted_select(&mut self.rng, &options)
168 }
169
170 fn select_company_pair(&mut self) -> Option<(String, String)> {
172 let relationships = self.ownership_structure.relationships.clone();
173 if relationships.is_empty() {
174 return None;
175 }
176
177 let rel = relationships.choose(&mut self.rng)?;
178
179 if self.rng.random_bool(0.5) {
181 Some((rel.parent_company.clone(), rel.subsidiary_company.clone()))
182 } else {
183 Some((rel.subsidiary_company.clone(), rel.parent_company.clone()))
184 }
185 }
186
187 fn generate_base_amount(&mut self, tx_type: ICTransactionType) -> Decimal {
189 let (min, max) = match tx_type {
190 ICTransactionType::GoodsSale => (dec!(1000), dec!(500000)),
191 ICTransactionType::ServiceProvided => (dec!(5000), dec!(200000)),
192 ICTransactionType::ManagementFee => (dec!(10000), dec!(100000)),
193 ICTransactionType::Royalty => (dec!(5000), dec!(150000)),
194 ICTransactionType::CostSharing => (dec!(2000), dec!(50000)),
195 ICTransactionType::LoanInterest => (dec!(1000), dec!(50000)),
196 ICTransactionType::ExpenseRecharge => (dec!(500), dec!(20000)),
197 ICTransactionType::Dividend => (dec!(50000), dec!(1000000)),
198 _ => (dec!(1000), dec!(100000)),
199 };
200
201 let range = max - min;
202 let random_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
203 (min + range * random_factor).round_dp(2)
204 }
205
206 fn apply_transfer_pricing(&self, base_amount: Decimal, relationship_id: &str) -> Decimal {
208 if let Some(policy) = self.transfer_pricing_policies.get(relationship_id) {
209 policy.calculate_transfer_price(base_amount)
210 } else {
211 base_amount * (Decimal::ONE + self.config.markup_percent / dec!(100))
213 }
214 }
215
216 pub fn generate_ic_transaction(
218 &mut self,
219 date: NaiveDate,
220 _fiscal_period: &str,
221 ) -> Option<ICMatchedPair> {
222 if !self.rng.random_bool(self.config.ic_transaction_rate) {
224 return None;
225 }
226
227 let (seller, buyer) = self.select_company_pair()?;
228 let tx_type = self.select_transaction_type();
229 let base_amount = self.generate_base_amount(tx_type);
230
231 let relationship_id = format!("{seller}-{buyer}");
233 let transfer_price = self.apply_transfer_pricing(base_amount, &relationship_id);
234
235 let ic_reference = self.generate_ic_reference(date);
236 let seller_doc = self.generate_doc_number("ICS");
237 let buyer_doc = self.generate_doc_number("ICB");
238
239 let mut pair = ICMatchedPair::new(
240 ic_reference,
241 tx_type,
242 seller.clone(),
243 buyer.clone(),
244 transfer_price,
245 self.config.default_currency.clone(),
246 date,
247 );
248
249 pair.seller_document = seller_doc;
251 pair.buyer_document = buyer_doc;
252
253 if tx_type.has_withholding_tax() {
255 pair.calculate_withholding_tax();
256 }
257
258 self.matched_pairs.push(pair.clone());
259 Some(pair)
260 }
261
262 pub fn generate_journal_entries(
264 &mut self,
265 pair: &ICMatchedPair,
266 fiscal_year: i32,
267 fiscal_period: u32,
268 ) -> (JournalEntry, JournalEntry) {
269 let (seller_dr_desc, seller_cr_desc) = pair.transaction_type.seller_accounts();
270 let (buyer_dr_desc, buyer_cr_desc) = pair.transaction_type.buyer_accounts();
271
272 let seller_entry = self.create_seller_entry(
274 pair,
275 fiscal_year,
276 fiscal_period,
277 seller_dr_desc,
278 seller_cr_desc,
279 );
280
281 let buyer_entry = self.create_buyer_entry(
283 pair,
284 fiscal_year,
285 fiscal_period,
286 buyer_dr_desc,
287 buyer_cr_desc,
288 );
289
290 (seller_entry, buyer_entry)
291 }
292
293 fn create_seller_entry(
295 &mut self,
296 pair: &ICMatchedPair,
297 _fiscal_year: i32,
298 _fiscal_period: u32,
299 dr_desc: &str,
300 cr_desc: &str,
301 ) -> JournalEntry {
302 let mut je = JournalEntry::new_simple(
303 pair.seller_document.clone(),
304 pair.seller_company.clone(),
305 pair.posting_date,
306 format!(
307 "IC {} to {}",
308 pair.transaction_type.seller_accounts().1,
309 pair.buyer_company
310 ),
311 );
312
313 je.header.reference = Some(pair.ic_reference.clone());
314 je.header.document_type = "IC".to_string();
315 je.header.currency = pair.currency.clone();
316 je.header.exchange_rate = Decimal::ONE;
317 je.header.created_by = "IC_GENERATOR".to_string();
318
319 let mut debit_amount = pair.amount;
321 if pair.withholding_tax.is_some() {
322 debit_amount = pair.net_amount();
323 }
324
325 je.add_line(JournalEntryLine {
326 line_number: 1,
327 gl_account: self.get_seller_receivable_account(&pair.buyer_company),
328 debit_amount,
329 text: Some(format!("{} - {}", dr_desc, pair.description)),
330 assignment: Some(pair.ic_reference.clone()),
331 reference: Some(pair.buyer_document.clone()),
332 ..Default::default()
333 });
334
335 je.add_line(JournalEntryLine {
337 line_number: 2,
338 gl_account: self.get_seller_revenue_account(pair.transaction_type),
339 credit_amount: pair.amount,
340 text: Some(format!("{} - {}", cr_desc, pair.description)),
341 assignment: Some(pair.ic_reference.clone()),
342 ..Default::default()
343 });
344
345 if let Some(wht) = pair.withholding_tax {
351 je.add_line(JournalEntryLine {
352 line_number: 3,
353 gl_account: self.framework_accounts.sales_tax_payable.clone(), debit_amount: wht,
355 text: Some("Withholding tax on IC transaction".to_string()),
356 assignment: Some(pair.ic_reference.clone()),
357 ..Default::default()
358 });
359 }
360
361 je
362 }
363
364 fn create_buyer_entry(
366 &mut self,
367 pair: &ICMatchedPair,
368 _fiscal_year: i32,
369 _fiscal_period: u32,
370 dr_desc: &str,
371 cr_desc: &str,
372 ) -> JournalEntry {
373 let mut je = JournalEntry::new_simple(
374 pair.buyer_document.clone(),
375 pair.buyer_company.clone(),
376 pair.posting_date,
377 format!(
378 "IC {} from {}",
379 pair.transaction_type.buyer_accounts().0,
380 pair.seller_company
381 ),
382 );
383
384 je.header.reference = Some(pair.ic_reference.clone());
385 je.header.document_type = "IC".to_string();
386 je.header.currency = pair.currency.clone();
387 je.header.exchange_rate = Decimal::ONE;
388 je.header.created_by = "IC_GENERATOR".to_string();
389
390 je.add_line(JournalEntryLine {
392 line_number: 1,
393 gl_account: self.get_buyer_expense_account(pair.transaction_type),
394 debit_amount: pair.amount,
395 cost_center: Some("CC100".to_string()),
396 text: Some(format!("{} - {}", dr_desc, pair.description)),
397 assignment: Some(pair.ic_reference.clone()),
398 reference: Some(pair.seller_document.clone()),
399 ..Default::default()
400 });
401
402 je.add_line(JournalEntryLine {
404 line_number: 2,
405 gl_account: self.get_buyer_payable_account(&pair.seller_company),
406 credit_amount: pair.amount,
407 text: Some(format!("{} - {}", cr_desc, pair.description)),
408 assignment: Some(pair.ic_reference.clone()),
409 ..Default::default()
410 });
411
412 je
413 }
414
415 fn get_seller_receivable_account(&self, buyer_company: &str) -> String {
420 let suffix: String = buyer_company.chars().take(2).collect();
421 format!("{}{}", self.framework_accounts.ic_ar_clearing, suffix)
422 }
423
424 fn get_seller_revenue_account(&self, tx_type: ICTransactionType) -> String {
429 let fa = &self.framework_accounts;
430 match tx_type {
431 ICTransactionType::GoodsSale => fa.product_revenue.clone(),
432 ICTransactionType::ServiceProvided => fa.service_revenue.clone(),
433 ICTransactionType::ManagementFee => fa.ic_revenue.clone(),
434 ICTransactionType::Royalty => fa.other_revenue.clone(),
435 ICTransactionType::LoanInterest => fa.other_revenue.clone(),
436 ICTransactionType::Dividend => fa.other_revenue.clone(),
437 _ => fa.ic_revenue.clone(),
438 }
439 }
440
441 fn get_buyer_expense_account(&self, tx_type: ICTransactionType) -> String {
446 let fa = &self.framework_accounts;
447 match tx_type {
448 ICTransactionType::GoodsSale => fa.cogs.clone(),
449 ICTransactionType::ServiceProvided => fa.rent.clone(), ICTransactionType::ManagementFee => fa.rent.clone(), ICTransactionType::Royalty => fa.rent.clone(), ICTransactionType::LoanInterest => fa.interest_expense.clone(),
453 ICTransactionType::Dividend => fa.retained_earnings.clone(),
454 _ => fa.cogs.clone(),
455 }
456 }
457
458 fn get_buyer_payable_account(&self, seller_company: &str) -> String {
463 let suffix: String = seller_company.chars().take(2).collect();
464 format!("{}{}", self.framework_accounts.ic_ap_clearing, suffix)
465 }
466
467 pub fn generate_ic_loan(
469 &mut self,
470 lender: String,
471 borrower: String,
472 start_date: NaiveDate,
473 term_months: u32,
474 ) -> ICLoan {
475 let (min_amount, max_amount) = self.config.loan_amount_range;
476 let range = max_amount - min_amount;
477 let random_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
478 let principal = (min_amount + range * random_factor).round_dp(0);
479
480 let (min_rate, max_rate) = self.config.loan_interest_rate_range;
481 let rate_range = max_rate - min_rate;
482 let rate_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
483 let interest_rate = (min_rate + rate_range * rate_factor).round_dp(2);
484
485 let maturity_date = start_date
486 .checked_add_months(chrono::Months::new(term_months))
487 .unwrap_or(start_date);
488
489 let loan_id = format!(
490 "LOAN{}{:04}",
491 start_date.format("%Y"),
492 self.active_loans.len() + 1
493 );
494
495 let loan = ICLoan::new(
496 loan_id,
497 lender,
498 borrower,
499 principal,
500 self.config.default_currency.clone(),
501 interest_rate,
502 start_date,
503 maturity_date,
504 );
505
506 self.active_loans.push(loan.clone());
507 loan
508 }
509
510 pub fn generate_loan_interest_entries(
512 &mut self,
513 as_of_date: NaiveDate,
514 fiscal_year: i32,
515 fiscal_period: u32,
516 ) -> Vec<(JournalEntry, JournalEntry)> {
517 let loans_data: Vec<_> = self
519 .active_loans
520 .iter()
521 .filter(|loan| !loan.is_repaid())
522 .map(|loan| {
523 let period_start = NaiveDate::from_ymd_opt(
524 if fiscal_period == 1 {
525 fiscal_year - 1
526 } else {
527 fiscal_year
528 },
529 if fiscal_period == 1 {
530 12
531 } else {
532 fiscal_period - 1
533 },
534 1,
535 )
536 .unwrap_or(as_of_date);
537
538 let interest = loan.calculate_interest(period_start, as_of_date);
539 (
540 loan.loan_id.clone(),
541 loan.lender_company.clone(),
542 loan.borrower_company.clone(),
543 loan.currency.clone(),
544 interest,
545 )
546 })
547 .filter(|(_, _, _, _, interest)| *interest > Decimal::ZERO)
548 .collect();
549
550 let mut entries = Vec::new();
551
552 for (loan_id, lender, borrower, currency, interest) in loans_data {
553 let ic_ref = self.generate_ic_reference(as_of_date);
554 let seller_doc = self.generate_doc_number("INT");
555 let buyer_doc = self.generate_doc_number("INT");
556
557 let mut pair = ICMatchedPair::new(
558 ic_ref,
559 ICTransactionType::LoanInterest,
560 lender,
561 borrower,
562 interest,
563 currency,
564 as_of_date,
565 );
566 pair.seller_document = seller_doc;
567 pair.buyer_document = buyer_doc;
568 pair.description = format!("Interest on loan {loan_id}");
569
570 let (seller_je, buyer_je) =
571 self.generate_journal_entries(&pair, fiscal_year, fiscal_period);
572 entries.push((seller_je, buyer_je));
573 }
574
575 entries
576 }
577
578 pub fn get_matched_pairs(&self) -> &[ICMatchedPair] {
580 &self.matched_pairs
581 }
582
583 pub fn get_open_pairs(&self) -> Vec<&ICMatchedPair> {
585 self.matched_pairs.iter().filter(|p| p.is_open()).collect()
586 }
587
588 pub fn get_active_loans(&self) -> &[ICLoan] {
590 &self.active_loans
591 }
592
593 pub fn generate_transactions_for_period(
595 &mut self,
596 start_date: NaiveDate,
597 end_date: NaiveDate,
598 transactions_per_day: usize,
599 ) -> Vec<ICMatchedPair> {
600 debug!(%start_date, %end_date, transactions_per_day, "Generating intercompany transactions");
601 let mut pairs = Vec::new();
602 let mut current_date = start_date;
603
604 while current_date <= end_date {
605 let fiscal_period = format!("{}{:02}", current_date.year(), current_date.month());
606
607 for _ in 0..transactions_per_day {
608 if let Some(pair) = self.generate_ic_transaction(current_date, &fiscal_period) {
609 pairs.push(pair);
610 }
611 }
612
613 current_date = current_date.succ_opt().unwrap_or(current_date);
614 }
615
616 pairs
617 }
618
619 pub fn reset_counters(&mut self) {
621 self.ic_counter = 0;
622 self.doc_counter = 0;
623 self.matched_pairs.clear();
624 }
625
626 pub fn generate_ic_document_chains(&mut self, pairs: &[ICMatchedPair]) -> ICDocumentChains {
639 let eligible_types = [
640 ICTransactionType::GoodsSale,
641 ICTransactionType::ServiceProvided,
642 ICTransactionType::ManagementFee,
643 ICTransactionType::Royalty,
644 ICTransactionType::ExpenseRecharge,
645 ];
646
647 let mut chains = ICDocumentChains {
648 seller_invoices: Vec::new(),
649 buyer_orders: Vec::new(),
650 buyer_goods_receipts: Vec::new(),
651 buyer_invoices: Vec::new(),
652 };
653
654 for pair in pairs {
655 if !eligible_types.contains(&pair.transaction_type) {
656 continue;
657 }
658
659 let date = pair.posting_date;
660 let fiscal_year = date.year() as u16;
661 let fiscal_period = date.month() as u8;
662
663 let ci_doc_id = self.generate_doc_number("IC-CI");
665 let due_date = date + chrono::Duration::days(30);
666
667 let mut ci = CustomerInvoice::new(
668 &ci_doc_id,
669 &pair.seller_company,
670 &pair.buyer_company,
671 fiscal_year,
672 fiscal_period,
673 date,
674 due_date,
675 "IC_GENERATOR",
676 );
677 ci.invoice_type = CustomerInvoiceType::Intercompany;
678 ci.is_intercompany = true;
679 ci.ic_partner = Some(pair.buyer_company.clone());
680 ci.header.reference = Some(pair.ic_reference.clone());
681 ci.header.currency = pair.currency.clone();
682 ci.header.posting_date = Some(date);
683
684 let description = format!("IC {:?} to {}", pair.transaction_type, pair.buyer_company);
685 ci.add_item(CustomerInvoiceItem::new(
686 1,
687 &description,
688 Decimal::ONE,
689 pair.amount,
690 ));
691
692 chains.seller_invoices.push(ci);
693
694 let po_doc_id = self.generate_doc_number("IC-PO");
696
697 let mut po = PurchaseOrder::new(
698 &po_doc_id,
699 &pair.buyer_company,
700 &pair.seller_company,
701 fiscal_year,
702 fiscal_period,
703 date,
704 "IC_GENERATOR",
705 );
706 po.header.reference = Some(pair.ic_reference.clone());
707 po.header.currency = pair.currency.clone();
708
709 let po_desc = format!(
710 "IC {:?} from {}",
711 pair.transaction_type, pair.seller_company
712 );
713 po.add_item(PurchaseOrderItem::new(
714 1,
715 &po_desc,
716 Decimal::ONE,
717 pair.amount,
718 ));
719
720 chains.buyer_orders.push(po);
721
722 let gr_doc_id = self.generate_doc_number("IC-GR");
724
725 let mut gr = GoodsReceipt::from_purchase_order(
726 &gr_doc_id,
727 &pair.buyer_company,
728 &po_doc_id,
729 &pair.seller_company,
730 "1000", "0001", fiscal_year,
733 fiscal_period,
734 date,
735 "IC_GENERATOR",
736 );
737 gr.header.reference = Some(pair.ic_reference.clone());
738 gr.header.currency = pair.currency.clone();
739
740 let gr_desc = format!(
741 "IC {:?} receipt from {}",
742 pair.transaction_type, pair.seller_company
743 );
744 gr.add_item(GoodsReceiptItem::from_po(
745 1,
746 &gr_desc,
747 Decimal::ONE,
748 pair.amount,
749 &po_doc_id,
750 1,
751 ));
752
753 chains.buyer_goods_receipts.push(gr);
754
755 let vi_doc_id = self.generate_doc_number("IC-VI");
757 let vendor_inv_number = format!("EXT-{}", pair.ic_reference);
758
759 let mut vi = VendorInvoice::from_po_gr(
760 &vi_doc_id,
761 &pair.buyer_company,
762 &pair.seller_company,
763 &vendor_inv_number,
764 &po_doc_id,
765 &gr_doc_id,
766 fiscal_year,
767 fiscal_period,
768 date,
769 "IC_GENERATOR",
770 );
771 vi.header.reference = Some(pair.ic_reference.clone());
772 vi.header.currency = pair.currency.clone();
773
774 let vi_desc = format!(
775 "IC {:?} invoice from {}",
776 pair.transaction_type, pair.seller_company
777 );
778 vi.add_item(VendorInvoiceItem::from_po_gr(
779 1,
780 &vi_desc,
781 Decimal::ONE,
782 pair.amount,
783 &po_doc_id,
784 1,
785 Some(gr_doc_id.clone()),
786 Some(1),
787 ));
788
789 chains.buyer_invoices.push(vi);
790 }
791
792 chains
793 }
794}
795
796#[derive(Debug, Clone)]
802pub struct ICDocumentChains {
803 pub seller_invoices: Vec<CustomerInvoice>,
805 pub buyer_orders: Vec<PurchaseOrder>,
807 pub buyer_goods_receipts: Vec<GoodsReceipt>,
809 pub buyer_invoices: Vec<VendorInvoice>,
811}
812
813#[cfg(test)]
814#[allow(clippy::unwrap_used)]
815mod tests {
816 use super::*;
817 use chrono::NaiveDate;
818 use datasynth_core::models::intercompany::IntercompanyRelationship;
819 use rust_decimal_macros::dec;
820
821 fn create_test_ownership_structure() -> OwnershipStructure {
822 let mut structure = OwnershipStructure::new("1000".to_string());
823 structure.add_relationship(IntercompanyRelationship::new(
824 "REL001".to_string(),
825 "1000".to_string(),
826 "1100".to_string(),
827 dec!(100),
828 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
829 ));
830 structure.add_relationship(IntercompanyRelationship::new(
831 "REL002".to_string(),
832 "1000".to_string(),
833 "1200".to_string(),
834 dec!(100),
835 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
836 ));
837 structure
838 }
839
840 #[test]
841 fn test_ic_generator_creation() {
842 let config = ICGeneratorConfig::default();
843 let structure = create_test_ownership_structure();
844 let generator = ICGenerator::new(config, structure, 12345);
845
846 assert!(generator.matched_pairs.is_empty());
847 assert!(generator.active_loans.is_empty());
848 }
849
850 #[test]
851 fn test_generate_ic_transaction() {
852 let config = ICGeneratorConfig {
853 ic_transaction_rate: 1.0, ..Default::default()
855 };
856
857 let structure = create_test_ownership_structure();
858 let mut generator = ICGenerator::new(config, structure, 12345);
859
860 let date = NaiveDate::from_ymd_opt(2022, 6, 15).unwrap();
861 let pair = generator.generate_ic_transaction(date, "202206");
862
863 assert!(pair.is_some());
864 let pair = pair.unwrap();
865 assert!(!pair.ic_reference.is_empty());
866 assert!(pair.amount > Decimal::ZERO);
867 }
868
869 #[test]
870 fn test_generate_journal_entries() {
871 let config = ICGeneratorConfig {
872 ic_transaction_rate: 1.0,
873 ..Default::default()
874 };
875
876 let structure = create_test_ownership_structure();
877 let mut generator = ICGenerator::new(config, structure, 12345);
878
879 let date = NaiveDate::from_ymd_opt(2022, 6, 15).unwrap();
880 let pair = generator.generate_ic_transaction(date, "202206").unwrap();
881
882 let (seller_je, buyer_je) = generator.generate_journal_entries(&pair, 2022, 6);
883
884 assert_eq!(seller_je.company_code(), pair.seller_company);
885 assert_eq!(buyer_je.company_code(), pair.buyer_company);
886 assert_eq!(seller_je.header.reference, Some(pair.ic_reference.clone()));
887 assert_eq!(buyer_je.header.reference, Some(pair.ic_reference));
888 }
889
890 #[test]
891 fn test_generate_ic_loan() {
892 let config = ICGeneratorConfig::default();
893 let structure = create_test_ownership_structure();
894 let mut generator = ICGenerator::new(config, structure, 12345);
895
896 let loan = generator.generate_ic_loan(
897 "1000".to_string(),
898 "1100".to_string(),
899 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
900 24,
901 );
902
903 assert!(!loan.loan_id.is_empty());
904 assert!(loan.principal > Decimal::ZERO);
905 assert!(loan.interest_rate > Decimal::ZERO);
906 assert_eq!(generator.active_loans.len(), 1);
907 }
908
909 #[test]
910 fn test_generate_transactions_for_period() {
911 let config = ICGeneratorConfig {
912 ic_transaction_rate: 1.0,
913 ..Default::default()
914 };
915
916 let structure = create_test_ownership_structure();
917 let mut generator = ICGenerator::new(config, structure, 12345);
918
919 let start = NaiveDate::from_ymd_opt(2022, 6, 1).unwrap();
920 let end = NaiveDate::from_ymd_opt(2022, 6, 5).unwrap();
921
922 let pairs = generator.generate_transactions_for_period(start, end, 2);
923
924 assert_eq!(pairs.len(), 10);
926 }
927}