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::intercompany::{
17 ICLoan, ICMatchedPair, ICTransactionType, OwnershipStructure, RecurringFrequency,
18 TransferPricingMethod, TransferPricingPolicy,
19};
20use datasynth_core::models::{JournalEntry, JournalEntryLine};
21
22#[derive(Debug, Clone)]
24pub struct ICGeneratorConfig {
25 pub ic_transaction_rate: f64,
27 pub transfer_pricing_method: TransferPricingMethod,
29 pub markup_percent: Decimal,
31 pub generate_matched_pairs: bool,
33 pub transaction_type_weights: HashMap<ICTransactionType, f64>,
35 pub generate_netting: bool,
37 pub netting_frequency: RecurringFrequency,
39 pub generate_loans: bool,
41 pub loan_amount_range: (Decimal, Decimal),
43 pub loan_interest_rate_range: (Decimal, Decimal),
45 pub default_currency: String,
47}
48
49impl Default for ICGeneratorConfig {
50 fn default() -> Self {
51 let mut weights = HashMap::new();
52 weights.insert(ICTransactionType::GoodsSale, 0.35);
53 weights.insert(ICTransactionType::ServiceProvided, 0.20);
54 weights.insert(ICTransactionType::ManagementFee, 0.15);
55 weights.insert(ICTransactionType::Royalty, 0.10);
56 weights.insert(ICTransactionType::CostSharing, 0.10);
57 weights.insert(ICTransactionType::LoanInterest, 0.05);
58 weights.insert(ICTransactionType::ExpenseRecharge, 0.05);
59
60 Self {
61 ic_transaction_rate: 0.15,
62 transfer_pricing_method: TransferPricingMethod::CostPlus,
63 markup_percent: dec!(5),
64 generate_matched_pairs: true,
65 transaction_type_weights: weights,
66 generate_netting: true,
67 netting_frequency: RecurringFrequency::Monthly,
68 generate_loans: true,
69 loan_amount_range: (dec!(100000), dec!(10000000)),
70 loan_interest_rate_range: (dec!(2), dec!(8)),
71 default_currency: "USD".to_string(),
72 }
73 }
74}
75
76pub struct ICGenerator {
78 config: ICGeneratorConfig,
80 rng: ChaCha8Rng,
82 ownership_structure: OwnershipStructure,
84 transfer_pricing_policies: HashMap<String, TransferPricingPolicy>,
86 active_loans: Vec<ICLoan>,
88 matched_pairs: Vec<ICMatchedPair>,
90 ic_counter: u64,
92 doc_counter: u64,
94 framework_accounts: FrameworkAccounts,
96}
97
98impl ICGenerator {
99 pub fn new_with_framework(
101 config: ICGeneratorConfig,
102 ownership_structure: OwnershipStructure,
103 seed: u64,
104 framework: &str,
105 ) -> Self {
106 Self {
107 config,
108 rng: seeded_rng(seed, 0),
109 ownership_structure,
110 transfer_pricing_policies: HashMap::new(),
111 active_loans: Vec::new(),
112 matched_pairs: Vec::new(),
113 ic_counter: 0,
114 doc_counter: 0,
115 framework_accounts: FrameworkAccounts::for_framework(framework),
116 }
117 }
118
119 pub fn new(
121 config: ICGeneratorConfig,
122 ownership_structure: OwnershipStructure,
123 seed: u64,
124 ) -> Self {
125 Self::new_with_framework(config, ownership_structure, seed, "us_gaap")
126 }
127
128 pub fn add_transfer_pricing_policy(
130 &mut self,
131 relationship_id: String,
132 policy: TransferPricingPolicy,
133 ) {
134 self.transfer_pricing_policies
135 .insert(relationship_id, policy);
136 }
137
138 fn generate_ic_reference(&mut self, date: NaiveDate) -> String {
140 self.ic_counter += 1;
141 format!("IC{}{:06}", date.format("%Y%m"), self.ic_counter)
142 }
143
144 fn generate_doc_number(&mut self, prefix: &str) -> String {
146 self.doc_counter += 1;
147 format!("{}{:08}", prefix, self.doc_counter)
148 }
149
150 fn select_transaction_type(&mut self) -> ICTransactionType {
152 let options: Vec<(ICTransactionType, f64)> = self
153 .config
154 .transaction_type_weights
155 .iter()
156 .map(|(&tx_type, &weight)| (tx_type, weight))
157 .collect();
158
159 if options.is_empty() {
160 return ICTransactionType::GoodsSale;
161 }
162
163 *weighted_select(&mut self.rng, &options)
164 }
165
166 fn select_company_pair(&mut self) -> Option<(String, String)> {
168 let relationships = self.ownership_structure.relationships.clone();
169 if relationships.is_empty() {
170 return None;
171 }
172
173 let rel = relationships.choose(&mut self.rng)?;
174
175 if self.rng.random_bool(0.5) {
177 Some((rel.parent_company.clone(), rel.subsidiary_company.clone()))
178 } else {
179 Some((rel.subsidiary_company.clone(), rel.parent_company.clone()))
180 }
181 }
182
183 fn generate_base_amount(&mut self, tx_type: ICTransactionType) -> Decimal {
185 let (min, max) = match tx_type {
186 ICTransactionType::GoodsSale => (dec!(1000), dec!(500000)),
187 ICTransactionType::ServiceProvided => (dec!(5000), dec!(200000)),
188 ICTransactionType::ManagementFee => (dec!(10000), dec!(100000)),
189 ICTransactionType::Royalty => (dec!(5000), dec!(150000)),
190 ICTransactionType::CostSharing => (dec!(2000), dec!(50000)),
191 ICTransactionType::LoanInterest => (dec!(1000), dec!(50000)),
192 ICTransactionType::ExpenseRecharge => (dec!(500), dec!(20000)),
193 ICTransactionType::Dividend => (dec!(50000), dec!(1000000)),
194 _ => (dec!(1000), dec!(100000)),
195 };
196
197 let range = max - min;
198 let random_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
199 (min + range * random_factor).round_dp(2)
200 }
201
202 fn apply_transfer_pricing(&self, base_amount: Decimal, relationship_id: &str) -> Decimal {
204 if let Some(policy) = self.transfer_pricing_policies.get(relationship_id) {
205 policy.calculate_transfer_price(base_amount)
206 } else {
207 base_amount * (Decimal::ONE + self.config.markup_percent / dec!(100))
209 }
210 }
211
212 pub fn generate_ic_transaction(
214 &mut self,
215 date: NaiveDate,
216 _fiscal_period: &str,
217 ) -> Option<ICMatchedPair> {
218 if !self.rng.random_bool(self.config.ic_transaction_rate) {
220 return None;
221 }
222
223 let (seller, buyer) = self.select_company_pair()?;
224 let tx_type = self.select_transaction_type();
225 let base_amount = self.generate_base_amount(tx_type);
226
227 let relationship_id = format!("{seller}-{buyer}");
229 let transfer_price = self.apply_transfer_pricing(base_amount, &relationship_id);
230
231 let ic_reference = self.generate_ic_reference(date);
232 let seller_doc = self.generate_doc_number("ICS");
233 let buyer_doc = self.generate_doc_number("ICB");
234
235 let mut pair = ICMatchedPair::new(
236 ic_reference,
237 tx_type,
238 seller.clone(),
239 buyer.clone(),
240 transfer_price,
241 self.config.default_currency.clone(),
242 date,
243 );
244
245 pair.seller_document = seller_doc;
247 pair.buyer_document = buyer_doc;
248
249 if tx_type.has_withholding_tax() {
251 pair.calculate_withholding_tax();
252 }
253
254 self.matched_pairs.push(pair.clone());
255 Some(pair)
256 }
257
258 pub fn generate_journal_entries(
260 &mut self,
261 pair: &ICMatchedPair,
262 fiscal_year: i32,
263 fiscal_period: u32,
264 ) -> (JournalEntry, JournalEntry) {
265 let (seller_dr_desc, seller_cr_desc) = pair.transaction_type.seller_accounts();
266 let (buyer_dr_desc, buyer_cr_desc) = pair.transaction_type.buyer_accounts();
267
268 let seller_entry = self.create_seller_entry(
270 pair,
271 fiscal_year,
272 fiscal_period,
273 seller_dr_desc,
274 seller_cr_desc,
275 );
276
277 let buyer_entry = self.create_buyer_entry(
279 pair,
280 fiscal_year,
281 fiscal_period,
282 buyer_dr_desc,
283 buyer_cr_desc,
284 );
285
286 (seller_entry, buyer_entry)
287 }
288
289 fn create_seller_entry(
291 &mut self,
292 pair: &ICMatchedPair,
293 _fiscal_year: i32,
294 _fiscal_period: u32,
295 dr_desc: &str,
296 cr_desc: &str,
297 ) -> JournalEntry {
298 let mut je = JournalEntry::new_simple(
299 pair.seller_document.clone(),
300 pair.seller_company.clone(),
301 pair.posting_date,
302 format!(
303 "IC {} to {}",
304 pair.transaction_type.seller_accounts().1,
305 pair.buyer_company
306 ),
307 );
308
309 je.header.reference = Some(pair.ic_reference.clone());
310 je.header.document_type = "IC".to_string();
311 je.header.currency = pair.currency.clone();
312 je.header.exchange_rate = Decimal::ONE;
313 je.header.created_by = "IC_GENERATOR".to_string();
314
315 let mut debit_amount = pair.amount;
317 if pair.withholding_tax.is_some() {
318 debit_amount = pair.net_amount();
319 }
320
321 je.add_line(JournalEntryLine {
322 line_number: 1,
323 gl_account: self.get_seller_receivable_account(&pair.buyer_company),
324 debit_amount,
325 text: Some(format!("{} - {}", dr_desc, pair.description)),
326 assignment: Some(pair.ic_reference.clone()),
327 reference: Some(pair.buyer_document.clone()),
328 ..Default::default()
329 });
330
331 je.add_line(JournalEntryLine {
333 line_number: 2,
334 gl_account: self.get_seller_revenue_account(pair.transaction_type),
335 credit_amount: pair.amount,
336 text: Some(format!("{} - {}", cr_desc, pair.description)),
337 assignment: Some(pair.ic_reference.clone()),
338 ..Default::default()
339 });
340
341 if let Some(wht) = pair.withholding_tax {
347 je.add_line(JournalEntryLine {
348 line_number: 3,
349 gl_account: self.framework_accounts.sales_tax_payable.clone(), debit_amount: wht,
351 text: Some("Withholding tax on IC transaction".to_string()),
352 assignment: Some(pair.ic_reference.clone()),
353 ..Default::default()
354 });
355 }
356
357 je
358 }
359
360 fn create_buyer_entry(
362 &mut self,
363 pair: &ICMatchedPair,
364 _fiscal_year: i32,
365 _fiscal_period: u32,
366 dr_desc: &str,
367 cr_desc: &str,
368 ) -> JournalEntry {
369 let mut je = JournalEntry::new_simple(
370 pair.buyer_document.clone(),
371 pair.buyer_company.clone(),
372 pair.posting_date,
373 format!(
374 "IC {} from {}",
375 pair.transaction_type.buyer_accounts().0,
376 pair.seller_company
377 ),
378 );
379
380 je.header.reference = Some(pair.ic_reference.clone());
381 je.header.document_type = "IC".to_string();
382 je.header.currency = pair.currency.clone();
383 je.header.exchange_rate = Decimal::ONE;
384 je.header.created_by = "IC_GENERATOR".to_string();
385
386 je.add_line(JournalEntryLine {
388 line_number: 1,
389 gl_account: self.get_buyer_expense_account(pair.transaction_type),
390 debit_amount: pair.amount,
391 cost_center: Some("CC100".to_string()),
392 text: Some(format!("{} - {}", dr_desc, pair.description)),
393 assignment: Some(pair.ic_reference.clone()),
394 reference: Some(pair.seller_document.clone()),
395 ..Default::default()
396 });
397
398 je.add_line(JournalEntryLine {
400 line_number: 2,
401 gl_account: self.get_buyer_payable_account(&pair.seller_company),
402 credit_amount: pair.amount,
403 text: Some(format!("{} - {}", cr_desc, pair.description)),
404 assignment: Some(pair.ic_reference.clone()),
405 ..Default::default()
406 });
407
408 je
409 }
410
411 fn get_seller_receivable_account(&self, buyer_company: &str) -> String {
416 let suffix: String = buyer_company.chars().take(2).collect();
417 format!("{}{}", self.framework_accounts.ic_ar_clearing, suffix)
418 }
419
420 fn get_seller_revenue_account(&self, tx_type: ICTransactionType) -> String {
425 let fa = &self.framework_accounts;
426 match tx_type {
427 ICTransactionType::GoodsSale => fa.product_revenue.clone(),
428 ICTransactionType::ServiceProvided => fa.service_revenue.clone(),
429 ICTransactionType::ManagementFee => fa.ic_revenue.clone(),
430 ICTransactionType::Royalty => fa.other_revenue.clone(),
431 ICTransactionType::LoanInterest => fa.other_revenue.clone(),
432 ICTransactionType::Dividend => fa.other_revenue.clone(),
433 _ => fa.ic_revenue.clone(),
434 }
435 }
436
437 fn get_buyer_expense_account(&self, tx_type: ICTransactionType) -> String {
442 let fa = &self.framework_accounts;
443 match tx_type {
444 ICTransactionType::GoodsSale => fa.cogs.clone(),
445 ICTransactionType::ServiceProvided => fa.rent.clone(), ICTransactionType::ManagementFee => fa.rent.clone(), ICTransactionType::Royalty => fa.rent.clone(), ICTransactionType::LoanInterest => fa.interest_expense.clone(),
449 ICTransactionType::Dividend => fa.retained_earnings.clone(),
450 _ => fa.cogs.clone(),
451 }
452 }
453
454 fn get_buyer_payable_account(&self, seller_company: &str) -> String {
459 let suffix: String = seller_company.chars().take(2).collect();
460 format!("{}{}", self.framework_accounts.ic_ap_clearing, suffix)
461 }
462
463 pub fn generate_ic_loan(
465 &mut self,
466 lender: String,
467 borrower: String,
468 start_date: NaiveDate,
469 term_months: u32,
470 ) -> ICLoan {
471 let (min_amount, max_amount) = self.config.loan_amount_range;
472 let range = max_amount - min_amount;
473 let random_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
474 let principal = (min_amount + range * random_factor).round_dp(0);
475
476 let (min_rate, max_rate) = self.config.loan_interest_rate_range;
477 let rate_range = max_rate - min_rate;
478 let rate_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
479 let interest_rate = (min_rate + rate_range * rate_factor).round_dp(2);
480
481 let maturity_date = start_date
482 .checked_add_months(chrono::Months::new(term_months))
483 .unwrap_or(start_date);
484
485 let loan_id = format!(
486 "LOAN{}{:04}",
487 start_date.format("%Y"),
488 self.active_loans.len() + 1
489 );
490
491 let loan = ICLoan::new(
492 loan_id,
493 lender,
494 borrower,
495 principal,
496 self.config.default_currency.clone(),
497 interest_rate,
498 start_date,
499 maturity_date,
500 );
501
502 self.active_loans.push(loan.clone());
503 loan
504 }
505
506 pub fn generate_loan_interest_entries(
508 &mut self,
509 as_of_date: NaiveDate,
510 fiscal_year: i32,
511 fiscal_period: u32,
512 ) -> Vec<(JournalEntry, JournalEntry)> {
513 let loans_data: Vec<_> = self
515 .active_loans
516 .iter()
517 .filter(|loan| !loan.is_repaid())
518 .map(|loan| {
519 let period_start = NaiveDate::from_ymd_opt(
520 if fiscal_period == 1 {
521 fiscal_year - 1
522 } else {
523 fiscal_year
524 },
525 if fiscal_period == 1 {
526 12
527 } else {
528 fiscal_period - 1
529 },
530 1,
531 )
532 .unwrap_or(as_of_date);
533
534 let interest = loan.calculate_interest(period_start, as_of_date);
535 (
536 loan.loan_id.clone(),
537 loan.lender_company.clone(),
538 loan.borrower_company.clone(),
539 loan.currency.clone(),
540 interest,
541 )
542 })
543 .filter(|(_, _, _, _, interest)| *interest > Decimal::ZERO)
544 .collect();
545
546 let mut entries = Vec::new();
547
548 for (loan_id, lender, borrower, currency, interest) in loans_data {
549 let ic_ref = self.generate_ic_reference(as_of_date);
550 let seller_doc = self.generate_doc_number("INT");
551 let buyer_doc = self.generate_doc_number("INT");
552
553 let mut pair = ICMatchedPair::new(
554 ic_ref,
555 ICTransactionType::LoanInterest,
556 lender,
557 borrower,
558 interest,
559 currency,
560 as_of_date,
561 );
562 pair.seller_document = seller_doc;
563 pair.buyer_document = buyer_doc;
564 pair.description = format!("Interest on loan {loan_id}");
565
566 let (seller_je, buyer_je) =
567 self.generate_journal_entries(&pair, fiscal_year, fiscal_period);
568 entries.push((seller_je, buyer_je));
569 }
570
571 entries
572 }
573
574 pub fn get_matched_pairs(&self) -> &[ICMatchedPair] {
576 &self.matched_pairs
577 }
578
579 pub fn get_open_pairs(&self) -> Vec<&ICMatchedPair> {
581 self.matched_pairs.iter().filter(|p| p.is_open()).collect()
582 }
583
584 pub fn get_active_loans(&self) -> &[ICLoan] {
586 &self.active_loans
587 }
588
589 pub fn generate_transactions_for_period(
591 &mut self,
592 start_date: NaiveDate,
593 end_date: NaiveDate,
594 transactions_per_day: usize,
595 ) -> Vec<ICMatchedPair> {
596 debug!(%start_date, %end_date, transactions_per_day, "Generating intercompany transactions");
597 let mut pairs = Vec::new();
598 let mut current_date = start_date;
599
600 while current_date <= end_date {
601 let fiscal_period = format!("{}{:02}", current_date.year(), current_date.month());
602
603 for _ in 0..transactions_per_day {
604 if let Some(pair) = self.generate_ic_transaction(current_date, &fiscal_period) {
605 pairs.push(pair);
606 }
607 }
608
609 current_date = current_date.succ_opt().unwrap_or(current_date);
610 }
611
612 pairs
613 }
614
615 pub fn reset_counters(&mut self) {
617 self.ic_counter = 0;
618 self.doc_counter = 0;
619 self.matched_pairs.clear();
620 }
621}
622
623#[cfg(test)]
624#[allow(clippy::unwrap_used)]
625mod tests {
626 use super::*;
627 use chrono::NaiveDate;
628 use datasynth_core::models::intercompany::IntercompanyRelationship;
629 use rust_decimal_macros::dec;
630
631 fn create_test_ownership_structure() -> OwnershipStructure {
632 let mut structure = OwnershipStructure::new("1000".to_string());
633 structure.add_relationship(IntercompanyRelationship::new(
634 "REL001".to_string(),
635 "1000".to_string(),
636 "1100".to_string(),
637 dec!(100),
638 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
639 ));
640 structure.add_relationship(IntercompanyRelationship::new(
641 "REL002".to_string(),
642 "1000".to_string(),
643 "1200".to_string(),
644 dec!(100),
645 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
646 ));
647 structure
648 }
649
650 #[test]
651 fn test_ic_generator_creation() {
652 let config = ICGeneratorConfig::default();
653 let structure = create_test_ownership_structure();
654 let generator = ICGenerator::new(config, structure, 12345);
655
656 assert!(generator.matched_pairs.is_empty());
657 assert!(generator.active_loans.is_empty());
658 }
659
660 #[test]
661 fn test_generate_ic_transaction() {
662 let config = ICGeneratorConfig {
663 ic_transaction_rate: 1.0, ..Default::default()
665 };
666
667 let structure = create_test_ownership_structure();
668 let mut generator = ICGenerator::new(config, structure, 12345);
669
670 let date = NaiveDate::from_ymd_opt(2022, 6, 15).unwrap();
671 let pair = generator.generate_ic_transaction(date, "202206");
672
673 assert!(pair.is_some());
674 let pair = pair.unwrap();
675 assert!(!pair.ic_reference.is_empty());
676 assert!(pair.amount > Decimal::ZERO);
677 }
678
679 #[test]
680 fn test_generate_journal_entries() {
681 let config = ICGeneratorConfig {
682 ic_transaction_rate: 1.0,
683 ..Default::default()
684 };
685
686 let structure = create_test_ownership_structure();
687 let mut generator = ICGenerator::new(config, structure, 12345);
688
689 let date = NaiveDate::from_ymd_opt(2022, 6, 15).unwrap();
690 let pair = generator.generate_ic_transaction(date, "202206").unwrap();
691
692 let (seller_je, buyer_je) = generator.generate_journal_entries(&pair, 2022, 6);
693
694 assert_eq!(seller_je.company_code(), pair.seller_company);
695 assert_eq!(buyer_je.company_code(), pair.buyer_company);
696 assert_eq!(seller_je.header.reference, Some(pair.ic_reference.clone()));
697 assert_eq!(buyer_je.header.reference, Some(pair.ic_reference));
698 }
699
700 #[test]
701 fn test_generate_ic_loan() {
702 let config = ICGeneratorConfig::default();
703 let structure = create_test_ownership_structure();
704 let mut generator = ICGenerator::new(config, structure, 12345);
705
706 let loan = generator.generate_ic_loan(
707 "1000".to_string(),
708 "1100".to_string(),
709 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
710 24,
711 );
712
713 assert!(!loan.loan_id.is_empty());
714 assert!(loan.principal > Decimal::ZERO);
715 assert!(loan.interest_rate > Decimal::ZERO);
716 assert_eq!(generator.active_loans.len(), 1);
717 }
718
719 #[test]
720 fn test_generate_transactions_for_period() {
721 let config = ICGeneratorConfig {
722 ic_transaction_rate: 1.0,
723 ..Default::default()
724 };
725
726 let structure = create_test_ownership_structure();
727 let mut generator = ICGenerator::new(config, structure, 12345);
728
729 let start = NaiveDate::from_ymd_opt(2022, 6, 1).unwrap();
730 let end = NaiveDate::from_ymd_opt(2022, 6, 5).unwrap();
731
732 let pairs = generator.generate_transactions_for_period(start, end, 2);
733
734 assert_eq!(pairs.len(), 10);
736 }
737}