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