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        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(), // WHT payable
346                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    /// Create buyer-side journal entry.
357    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        // Debit line: Expense/Asset
383        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        // Credit line: IC Payable
395        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    /// Get IC receivable account for seller.
408    ///
409    /// Uses the framework's IC AR clearing account as a base, appending the
410    /// first two characters of the buyer company code as a sub-account suffix.
411    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    /// Get IC revenue account for seller.
417    ///
418    /// Maps the IC transaction type to the appropriate framework-specific
419    /// revenue account code.
420    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    /// Get IC expense account for buyer.
434    ///
435    /// Maps the IC transaction type to the appropriate framework-specific
436    /// expense (or equity) account code.
437    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(), // general operating expense
442            ICTransactionType::ManagementFee => fa.rent.clone(),   // general operating expense
443            ICTransactionType::Royalty => fa.rent.clone(),         // general operating expense
444            ICTransactionType::LoanInterest => fa.interest_expense.clone(),
445            ICTransactionType::Dividend => fa.retained_earnings.clone(),
446            _ => fa.cogs.clone(),
447        }
448    }
449
450    /// Get IC payable account for buyer.
451    ///
452    /// Uses the framework's IC AP clearing account as a base, appending the
453    /// first two characters of the seller company code as a sub-account suffix.
454    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    /// Generate an IC loan.
460    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    /// Generate interest entries for active loans.
503    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        // Collect loan data to avoid borrow issues
510        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    /// Get all generated matched pairs.
571    pub fn get_matched_pairs(&self) -> &[ICMatchedPair] {
572        &self.matched_pairs
573    }
574
575    /// Get open (unsettled) matched pairs.
576    pub fn get_open_pairs(&self) -> Vec<&ICMatchedPair> {
577        self.matched_pairs.iter().filter(|p| p.is_open()).collect()
578    }
579
580    /// Get active loans.
581    pub fn get_active_loans(&self) -> &[ICLoan] {
582        &self.active_loans
583    }
584
585    /// Generate multiple IC transactions for a date range.
586    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    /// Reset counters (for testing).
612    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, // Always generate
660            ..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        // 5 days * 2 transactions per day = 10 transactions
731        assert_eq!(pairs.len(), 10);
732    }
733}