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