Skip to main content

datasynth_generators/intercompany/
ic_generator.rs

1//! Intercompany transaction generator.
2//!
3//! Generates matched pairs of intercompany journal entries that offset
4//! between related entities.
5
6use 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/// Configuration for IC transaction generation.
23#[derive(Debug, Clone)]
24pub struct ICGeneratorConfig {
25    /// Probability of generating an IC transaction (0.0 to 1.0).
26    pub ic_transaction_rate: f64,
27    /// Transfer pricing method to use.
28    pub transfer_pricing_method: TransferPricingMethod,
29    /// Markup percentage for cost-plus method.
30    pub markup_percent: Decimal,
31    /// Generate matched pairs (both sides of IC transaction).
32    pub generate_matched_pairs: bool,
33    /// Transaction type distribution.
34    pub transaction_type_weights: HashMap<ICTransactionType, f64>,
35    /// Generate netting settlements.
36    pub generate_netting: bool,
37    /// Netting frequency (if enabled).
38    pub netting_frequency: RecurringFrequency,
39    /// Generate IC loans.
40    pub generate_loans: bool,
41    /// Typical loan amount range.
42    pub loan_amount_range: (Decimal, Decimal),
43    /// Loan interest rate range.
44    pub loan_interest_rate_range: (Decimal, Decimal),
45    /// Default currency for IC transactions.
46    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
76/// Generator for intercompany transactions.
77pub struct ICGenerator {
78    /// Configuration.
79    config: ICGeneratorConfig,
80    /// Random number generator.
81    rng: ChaCha8Rng,
82    /// Ownership structure.
83    ownership_structure: OwnershipStructure,
84    /// Transfer pricing policies by relationship.
85    transfer_pricing_policies: HashMap<String, TransferPricingPolicy>,
86    /// Active IC loans.
87    active_loans: Vec<ICLoan>,
88    /// Generated IC matched pairs.
89    matched_pairs: Vec<ICMatchedPair>,
90    /// IC reference counter.
91    ic_counter: u64,
92    /// Document counter.
93    doc_counter: u64,
94    /// Framework-aware account mappings.
95    framework_accounts: FrameworkAccounts,
96}
97
98impl ICGenerator {
99    /// Create a new IC generator for a specific accounting framework.
100    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    /// Create a new IC generator (defaults to US GAAP).
120    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    /// Add a transfer pricing policy.
129    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    /// Generate IC reference number.
139    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    /// Generate document number.
145    fn generate_doc_number(&mut self, prefix: &str) -> String {
146        self.doc_counter += 1;
147        format!("{}{:08}", prefix, self.doc_counter)
148    }
149
150    /// Select a random IC transaction type based on weights.
151    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    /// Select a random pair of related companies.
167    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        // Randomly decide direction (parent sells to sub, or sub sells to parent)
176        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    /// Generate a base amount for IC transaction.
184    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    /// Apply transfer pricing markup to base amount.
203    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            // Use default config markup
208            base_amount * (Decimal::ONE + self.config.markup_percent / dec!(100))
209        }
210    }
211
212    /// Generate a single IC matched pair.
213    pub fn generate_ic_transaction(
214        &mut self,
215        date: NaiveDate,
216        _fiscal_period: &str,
217    ) -> Option<ICMatchedPair> {
218        // Check if we should generate an IC transaction
219        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        // Find relationship for transfer pricing
228        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        // Assign document numbers
246        pair.seller_document = seller_doc;
247        pair.buyer_document = buyer_doc;
248
249        // Calculate withholding tax if applicable
250        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    /// Generate IC journal entries from a matched pair.
259    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        // Seller entry: DR IC Receivable, CR Revenue/Income
269        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        // Buyer entry: DR Expense/Asset, CR IC Payable
278        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    /// Create seller-side journal entry.
290    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        // Debit line: IC Receivable
316        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        // Credit line: Revenue/Income
332        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        // Add withholding tax line if applicable
342        // WHT is withheld by the buyer and remitted to the tax authority on
343        // behalf of the seller.  From the seller's perspective this is a tax
344        // receivable (debit) — the seller can reclaim it as a tax credit.
345        // DR WHT Receivable + DR IC Receivable(net) = CR Revenue(gross)
346        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(), // WHT receivable
350                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    /// Create buyer-side journal entry.
361    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        // Debit line: Expense/Asset
387        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        // Credit line: IC Payable
399        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    /// Get IC receivable account for seller.
412    ///
413    /// Uses the framework's IC AR clearing account as a base, appending the
414    /// first two characters of the buyer company code as a sub-account suffix.
415    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    /// Get IC revenue account for seller.
421    ///
422    /// Maps the IC transaction type to the appropriate framework-specific
423    /// revenue account code.
424    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    /// Get IC expense account for buyer.
438    ///
439    /// Maps the IC transaction type to the appropriate framework-specific
440    /// expense (or equity) account code.
441    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(), // general operating expense
446            ICTransactionType::ManagementFee => fa.rent.clone(),   // general operating expense
447            ICTransactionType::Royalty => fa.rent.clone(),         // general operating expense
448            ICTransactionType::LoanInterest => fa.interest_expense.clone(),
449            ICTransactionType::Dividend => fa.retained_earnings.clone(),
450            _ => fa.cogs.clone(),
451        }
452    }
453
454    /// Get IC payable account for buyer.
455    ///
456    /// Uses the framework's IC AP clearing account as a base, appending the
457    /// first two characters of the seller company code as a sub-account suffix.
458    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    /// Generate an IC loan.
464    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    /// Generate interest entries for active loans.
507    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        // Collect loan data to avoid borrow issues
514        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    /// Get all generated matched pairs.
575    pub fn get_matched_pairs(&self) -> &[ICMatchedPair] {
576        &self.matched_pairs
577    }
578
579    /// Get open (unsettled) matched pairs.
580    pub fn get_open_pairs(&self) -> Vec<&ICMatchedPair> {
581        self.matched_pairs.iter().filter(|p| p.is_open()).collect()
582    }
583
584    /// Get active loans.
585    pub fn get_active_loans(&self) -> &[ICLoan] {
586        &self.active_loans
587    }
588
589    /// Generate multiple IC transactions for a date range.
590    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    /// Reset counters (for testing).
616    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, // Always generate
664            ..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        // 5 days * 2 transactions per day = 10 transactions
735        assert_eq!(pairs.len(), 10);
736    }
737}