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 {
343 je.add_line(JournalEntryLine {
344 line_number: 3,
345 gl_account: self.framework_accounts.sales_tax_payable.clone(), credit_amount: wht,
347 text: Some("Withholding tax on IC transaction".to_string()),
348 assignment: Some(pair.ic_reference.clone()),
349 ..Default::default()
350 });
351 }
352
353 je
354 }
355
356 fn create_buyer_entry(
358 &mut self,
359 pair: &ICMatchedPair,
360 _fiscal_year: i32,
361 _fiscal_period: u32,
362 dr_desc: &str,
363 cr_desc: &str,
364 ) -> JournalEntry {
365 let mut je = JournalEntry::new_simple(
366 pair.buyer_document.clone(),
367 pair.buyer_company.clone(),
368 pair.posting_date,
369 format!(
370 "IC {} from {}",
371 pair.transaction_type.buyer_accounts().0,
372 pair.seller_company
373 ),
374 );
375
376 je.header.reference = Some(pair.ic_reference.clone());
377 je.header.document_type = "IC".to_string();
378 je.header.currency = pair.currency.clone();
379 je.header.exchange_rate = Decimal::ONE;
380 je.header.created_by = "IC_GENERATOR".to_string();
381
382 je.add_line(JournalEntryLine {
384 line_number: 1,
385 gl_account: self.get_buyer_expense_account(pair.transaction_type),
386 debit_amount: pair.amount,
387 cost_center: Some("CC100".to_string()),
388 text: Some(format!("{} - {}", dr_desc, pair.description)),
389 assignment: Some(pair.ic_reference.clone()),
390 reference: Some(pair.seller_document.clone()),
391 ..Default::default()
392 });
393
394 je.add_line(JournalEntryLine {
396 line_number: 2,
397 gl_account: self.get_buyer_payable_account(&pair.seller_company),
398 credit_amount: pair.amount,
399 text: Some(format!("{} - {}", cr_desc, pair.description)),
400 assignment: Some(pair.ic_reference.clone()),
401 ..Default::default()
402 });
403
404 je
405 }
406
407 fn get_seller_receivable_account(&self, buyer_company: &str) -> String {
412 let suffix: String = buyer_company.chars().take(2).collect();
413 format!("{}{}", self.framework_accounts.ic_ar_clearing, suffix)
414 }
415
416 fn get_seller_revenue_account(&self, tx_type: ICTransactionType) -> String {
421 let fa = &self.framework_accounts;
422 match tx_type {
423 ICTransactionType::GoodsSale => fa.product_revenue.clone(),
424 ICTransactionType::ServiceProvided => fa.service_revenue.clone(),
425 ICTransactionType::ManagementFee => fa.ic_revenue.clone(),
426 ICTransactionType::Royalty => fa.other_revenue.clone(),
427 ICTransactionType::LoanInterest => fa.other_revenue.clone(),
428 ICTransactionType::Dividend => fa.other_revenue.clone(),
429 _ => fa.ic_revenue.clone(),
430 }
431 }
432
433 fn get_buyer_expense_account(&self, tx_type: ICTransactionType) -> String {
438 let fa = &self.framework_accounts;
439 match tx_type {
440 ICTransactionType::GoodsSale => fa.cogs.clone(),
441 ICTransactionType::ServiceProvided => fa.rent.clone(), ICTransactionType::ManagementFee => fa.rent.clone(), ICTransactionType::Royalty => fa.rent.clone(), ICTransactionType::LoanInterest => fa.interest_expense.clone(),
445 ICTransactionType::Dividend => fa.retained_earnings.clone(),
446 _ => fa.cogs.clone(),
447 }
448 }
449
450 fn get_buyer_payable_account(&self, seller_company: &str) -> String {
455 let suffix: String = seller_company.chars().take(2).collect();
456 format!("{}{}", self.framework_accounts.ic_ap_clearing, suffix)
457 }
458
459 pub fn generate_ic_loan(
461 &mut self,
462 lender: String,
463 borrower: String,
464 start_date: NaiveDate,
465 term_months: u32,
466 ) -> ICLoan {
467 let (min_amount, max_amount) = self.config.loan_amount_range;
468 let range = max_amount - min_amount;
469 let random_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
470 let principal = (min_amount + range * random_factor).round_dp(0);
471
472 let (min_rate, max_rate) = self.config.loan_interest_rate_range;
473 let rate_range = max_rate - min_rate;
474 let rate_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
475 let interest_rate = (min_rate + rate_range * rate_factor).round_dp(2);
476
477 let maturity_date = start_date
478 .checked_add_months(chrono::Months::new(term_months))
479 .unwrap_or(start_date);
480
481 let loan_id = format!(
482 "LOAN{}{:04}",
483 start_date.format("%Y"),
484 self.active_loans.len() + 1
485 );
486
487 let loan = ICLoan::new(
488 loan_id,
489 lender,
490 borrower,
491 principal,
492 self.config.default_currency.clone(),
493 interest_rate,
494 start_date,
495 maturity_date,
496 );
497
498 self.active_loans.push(loan.clone());
499 loan
500 }
501
502 pub fn generate_loan_interest_entries(
504 &mut self,
505 as_of_date: NaiveDate,
506 fiscal_year: i32,
507 fiscal_period: u32,
508 ) -> Vec<(JournalEntry, JournalEntry)> {
509 let loans_data: Vec<_> = self
511 .active_loans
512 .iter()
513 .filter(|loan| !loan.is_repaid())
514 .map(|loan| {
515 let period_start = NaiveDate::from_ymd_opt(
516 if fiscal_period == 1 {
517 fiscal_year - 1
518 } else {
519 fiscal_year
520 },
521 if fiscal_period == 1 {
522 12
523 } else {
524 fiscal_period - 1
525 },
526 1,
527 )
528 .unwrap_or(as_of_date);
529
530 let interest = loan.calculate_interest(period_start, as_of_date);
531 (
532 loan.loan_id.clone(),
533 loan.lender_company.clone(),
534 loan.borrower_company.clone(),
535 loan.currency.clone(),
536 interest,
537 )
538 })
539 .filter(|(_, _, _, _, interest)| *interest > Decimal::ZERO)
540 .collect();
541
542 let mut entries = Vec::new();
543
544 for (loan_id, lender, borrower, currency, interest) in loans_data {
545 let ic_ref = self.generate_ic_reference(as_of_date);
546 let seller_doc = self.generate_doc_number("INT");
547 let buyer_doc = self.generate_doc_number("INT");
548
549 let mut pair = ICMatchedPair::new(
550 ic_ref,
551 ICTransactionType::LoanInterest,
552 lender,
553 borrower,
554 interest,
555 currency,
556 as_of_date,
557 );
558 pair.seller_document = seller_doc;
559 pair.buyer_document = buyer_doc;
560 pair.description = format!("Interest on loan {}", loan_id);
561
562 let (seller_je, buyer_je) =
563 self.generate_journal_entries(&pair, fiscal_year, fiscal_period);
564 entries.push((seller_je, buyer_je));
565 }
566
567 entries
568 }
569
570 pub fn get_matched_pairs(&self) -> &[ICMatchedPair] {
572 &self.matched_pairs
573 }
574
575 pub fn get_open_pairs(&self) -> Vec<&ICMatchedPair> {
577 self.matched_pairs.iter().filter(|p| p.is_open()).collect()
578 }
579
580 pub fn get_active_loans(&self) -> &[ICLoan] {
582 &self.active_loans
583 }
584
585 pub fn generate_transactions_for_period(
587 &mut self,
588 start_date: NaiveDate,
589 end_date: NaiveDate,
590 transactions_per_day: usize,
591 ) -> Vec<ICMatchedPair> {
592 debug!(%start_date, %end_date, transactions_per_day, "Generating intercompany transactions");
593 let mut pairs = Vec::new();
594 let mut current_date = start_date;
595
596 while current_date <= end_date {
597 let fiscal_period = format!("{}{:02}", current_date.year(), current_date.month());
598
599 for _ in 0..transactions_per_day {
600 if let Some(pair) = self.generate_ic_transaction(current_date, &fiscal_period) {
601 pairs.push(pair);
602 }
603 }
604
605 current_date = current_date.succ_opt().unwrap_or(current_date);
606 }
607
608 pairs
609 }
610
611 pub fn reset_counters(&mut self) {
613 self.ic_counter = 0;
614 self.doc_counter = 0;
615 self.matched_pairs.clear();
616 }
617}
618
619#[cfg(test)]
620#[allow(clippy::unwrap_used)]
621mod tests {
622 use super::*;
623 use chrono::NaiveDate;
624 use datasynth_core::models::intercompany::IntercompanyRelationship;
625 use rust_decimal_macros::dec;
626
627 fn create_test_ownership_structure() -> OwnershipStructure {
628 let mut structure = OwnershipStructure::new("1000".to_string());
629 structure.add_relationship(IntercompanyRelationship::new(
630 "REL001".to_string(),
631 "1000".to_string(),
632 "1100".to_string(),
633 dec!(100),
634 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
635 ));
636 structure.add_relationship(IntercompanyRelationship::new(
637 "REL002".to_string(),
638 "1000".to_string(),
639 "1200".to_string(),
640 dec!(100),
641 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
642 ));
643 structure
644 }
645
646 #[test]
647 fn test_ic_generator_creation() {
648 let config = ICGeneratorConfig::default();
649 let structure = create_test_ownership_structure();
650 let generator = ICGenerator::new(config, structure, 12345);
651
652 assert!(generator.matched_pairs.is_empty());
653 assert!(generator.active_loans.is_empty());
654 }
655
656 #[test]
657 fn test_generate_ic_transaction() {
658 let config = ICGeneratorConfig {
659 ic_transaction_rate: 1.0, ..Default::default()
661 };
662
663 let structure = create_test_ownership_structure();
664 let mut generator = ICGenerator::new(config, structure, 12345);
665
666 let date = NaiveDate::from_ymd_opt(2022, 6, 15).unwrap();
667 let pair = generator.generate_ic_transaction(date, "202206");
668
669 assert!(pair.is_some());
670 let pair = pair.unwrap();
671 assert!(!pair.ic_reference.is_empty());
672 assert!(pair.amount > Decimal::ZERO);
673 }
674
675 #[test]
676 fn test_generate_journal_entries() {
677 let config = ICGeneratorConfig {
678 ic_transaction_rate: 1.0,
679 ..Default::default()
680 };
681
682 let structure = create_test_ownership_structure();
683 let mut generator = ICGenerator::new(config, structure, 12345);
684
685 let date = NaiveDate::from_ymd_opt(2022, 6, 15).unwrap();
686 let pair = generator.generate_ic_transaction(date, "202206").unwrap();
687
688 let (seller_je, buyer_je) = generator.generate_journal_entries(&pair, 2022, 6);
689
690 assert_eq!(seller_je.company_code(), pair.seller_company);
691 assert_eq!(buyer_je.company_code(), pair.buyer_company);
692 assert_eq!(seller_je.header.reference, Some(pair.ic_reference.clone()));
693 assert_eq!(buyer_je.header.reference, Some(pair.ic_reference));
694 }
695
696 #[test]
697 fn test_generate_ic_loan() {
698 let config = ICGeneratorConfig::default();
699 let structure = create_test_ownership_structure();
700 let mut generator = ICGenerator::new(config, structure, 12345);
701
702 let loan = generator.generate_ic_loan(
703 "1000".to_string(),
704 "1100".to_string(),
705 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
706 24,
707 );
708
709 assert!(!loan.loan_id.is_empty());
710 assert!(loan.principal > Decimal::ZERO);
711 assert!(loan.interest_rate > Decimal::ZERO);
712 assert_eq!(generator.active_loans.len(), 1);
713 }
714
715 #[test]
716 fn test_generate_transactions_for_period() {
717 let config = ICGeneratorConfig {
718 ic_transaction_rate: 1.0,
719 ..Default::default()
720 };
721
722 let structure = create_test_ownership_structure();
723 let mut generator = ICGenerator::new(config, structure, 12345);
724
725 let start = NaiveDate::from_ymd_opt(2022, 6, 1).unwrap();
726 let end = NaiveDate::from_ymd_opt(2022, 6, 5).unwrap();
727
728 let pairs = generator.generate_transactions_for_period(start, end, 2);
729
730 assert_eq!(pairs.len(), 10);
732 }
733}