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::documents::{
17    CustomerInvoice, CustomerInvoiceItem, CustomerInvoiceType, GoodsReceipt, GoodsReceiptItem,
18    PurchaseOrder, PurchaseOrderItem, VendorInvoice, VendorInvoiceItem,
19};
20use datasynth_core::models::intercompany::{
21    ICLoan, ICMatchedPair, ICTransactionType, OwnershipStructure, RecurringFrequency,
22    TransferPricingMethod, TransferPricingPolicy,
23};
24use datasynth_core::models::{JournalEntry, JournalEntryLine};
25
26/// Configuration for IC transaction generation.
27#[derive(Debug, Clone)]
28pub struct ICGeneratorConfig {
29    /// Probability of generating an IC transaction (0.0 to 1.0).
30    pub ic_transaction_rate: f64,
31    /// Transfer pricing method to use.
32    pub transfer_pricing_method: TransferPricingMethod,
33    /// Markup percentage for cost-plus method.
34    pub markup_percent: Decimal,
35    /// Generate matched pairs (both sides of IC transaction).
36    pub generate_matched_pairs: bool,
37    /// Transaction type distribution.
38    pub transaction_type_weights: HashMap<ICTransactionType, f64>,
39    /// Generate netting settlements.
40    pub generate_netting: bool,
41    /// Netting frequency (if enabled).
42    pub netting_frequency: RecurringFrequency,
43    /// Generate IC loans.
44    pub generate_loans: bool,
45    /// Typical loan amount range.
46    pub loan_amount_range: (Decimal, Decimal),
47    /// Loan interest rate range.
48    pub loan_interest_rate_range: (Decimal, Decimal),
49    /// Default currency for IC transactions.
50    pub default_currency: String,
51}
52
53impl Default for ICGeneratorConfig {
54    fn default() -> Self {
55        let mut weights = HashMap::new();
56        weights.insert(ICTransactionType::GoodsSale, 0.35);
57        weights.insert(ICTransactionType::ServiceProvided, 0.20);
58        weights.insert(ICTransactionType::ManagementFee, 0.15);
59        weights.insert(ICTransactionType::Royalty, 0.10);
60        weights.insert(ICTransactionType::CostSharing, 0.10);
61        weights.insert(ICTransactionType::LoanInterest, 0.05);
62        weights.insert(ICTransactionType::ExpenseRecharge, 0.05);
63
64        Self {
65            ic_transaction_rate: 0.15,
66            transfer_pricing_method: TransferPricingMethod::CostPlus,
67            markup_percent: dec!(5),
68            generate_matched_pairs: true,
69            transaction_type_weights: weights,
70            generate_netting: true,
71            netting_frequency: RecurringFrequency::Monthly,
72            generate_loans: true,
73            loan_amount_range: (dec!(100000), dec!(10000000)),
74            loan_interest_rate_range: (dec!(2), dec!(8)),
75            default_currency: "USD".to_string(),
76        }
77    }
78}
79
80/// Generator for intercompany transactions.
81pub struct ICGenerator {
82    /// Configuration.
83    config: ICGeneratorConfig,
84    /// Random number generator.
85    rng: ChaCha8Rng,
86    /// Ownership structure.
87    ownership_structure: OwnershipStructure,
88    /// Transfer pricing policies by relationship.
89    transfer_pricing_policies: HashMap<String, TransferPricingPolicy>,
90    /// Active IC loans.
91    active_loans: Vec<ICLoan>,
92    /// Generated IC matched pairs.
93    matched_pairs: Vec<ICMatchedPair>,
94    /// IC reference counter.
95    ic_counter: u64,
96    /// Document counter.
97    doc_counter: u64,
98    /// Framework-aware account mappings.
99    framework_accounts: FrameworkAccounts,
100}
101
102impl ICGenerator {
103    /// Create a new IC generator for a specific accounting framework.
104    pub fn new_with_framework(
105        config: ICGeneratorConfig,
106        ownership_structure: OwnershipStructure,
107        seed: u64,
108        framework: &str,
109    ) -> Self {
110        Self {
111            config,
112            rng: seeded_rng(seed, 0),
113            ownership_structure,
114            transfer_pricing_policies: HashMap::new(),
115            active_loans: Vec::new(),
116            matched_pairs: Vec::new(),
117            ic_counter: 0,
118            doc_counter: 0,
119            framework_accounts: FrameworkAccounts::for_framework(framework),
120        }
121    }
122
123    /// Create a new IC generator (defaults to US GAAP).
124    pub fn new(
125        config: ICGeneratorConfig,
126        ownership_structure: OwnershipStructure,
127        seed: u64,
128    ) -> Self {
129        Self::new_with_framework(config, ownership_structure, seed, "us_gaap")
130    }
131
132    /// Add a transfer pricing policy.
133    pub fn add_transfer_pricing_policy(
134        &mut self,
135        relationship_id: String,
136        policy: TransferPricingPolicy,
137    ) {
138        self.transfer_pricing_policies
139            .insert(relationship_id, policy);
140    }
141
142    /// Generate IC reference number.
143    fn generate_ic_reference(&mut self, date: NaiveDate) -> String {
144        self.ic_counter += 1;
145        format!("IC{}{:06}", date.format("%Y%m"), self.ic_counter)
146    }
147
148    /// Generate document number.
149    fn generate_doc_number(&mut self, prefix: &str) -> String {
150        self.doc_counter += 1;
151        format!("{}{:08}", prefix, self.doc_counter)
152    }
153
154    /// Select a random IC transaction type based on weights.
155    fn select_transaction_type(&mut self) -> ICTransactionType {
156        let options: Vec<(ICTransactionType, f64)> = self
157            .config
158            .transaction_type_weights
159            .iter()
160            .map(|(&tx_type, &weight)| (tx_type, weight))
161            .collect();
162
163        if options.is_empty() {
164            return ICTransactionType::GoodsSale;
165        }
166
167        *weighted_select(&mut self.rng, &options)
168    }
169
170    /// Select a random pair of related companies.
171    fn select_company_pair(&mut self) -> Option<(String, String)> {
172        let relationships = self.ownership_structure.relationships.clone();
173        if relationships.is_empty() {
174            return None;
175        }
176
177        let rel = relationships.choose(&mut self.rng)?;
178
179        // Randomly decide direction (parent sells to sub, or sub sells to parent)
180        if self.rng.random_bool(0.5) {
181            Some((rel.parent_company.clone(), rel.subsidiary_company.clone()))
182        } else {
183            Some((rel.subsidiary_company.clone(), rel.parent_company.clone()))
184        }
185    }
186
187    /// Generate a base amount for IC transaction.
188    fn generate_base_amount(&mut self, tx_type: ICTransactionType) -> Decimal {
189        let (min, max) = match tx_type {
190            ICTransactionType::GoodsSale => (dec!(1000), dec!(500000)),
191            ICTransactionType::ServiceProvided => (dec!(5000), dec!(200000)),
192            ICTransactionType::ManagementFee => (dec!(10000), dec!(100000)),
193            ICTransactionType::Royalty => (dec!(5000), dec!(150000)),
194            ICTransactionType::CostSharing => (dec!(2000), dec!(50000)),
195            ICTransactionType::LoanInterest => (dec!(1000), dec!(50000)),
196            ICTransactionType::ExpenseRecharge => (dec!(500), dec!(20000)),
197            ICTransactionType::Dividend => (dec!(50000), dec!(1000000)),
198            _ => (dec!(1000), dec!(100000)),
199        };
200
201        let range = max - min;
202        let random_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
203        (min + range * random_factor).round_dp(2)
204    }
205
206    /// Apply transfer pricing markup to base amount.
207    fn apply_transfer_pricing(&self, base_amount: Decimal, relationship_id: &str) -> Decimal {
208        if let Some(policy) = self.transfer_pricing_policies.get(relationship_id) {
209            policy.calculate_transfer_price(base_amount)
210        } else {
211            // Use default config markup
212            base_amount * (Decimal::ONE + self.config.markup_percent / dec!(100))
213        }
214    }
215
216    /// Generate a single IC matched pair.
217    pub fn generate_ic_transaction(
218        &mut self,
219        date: NaiveDate,
220        _fiscal_period: &str,
221    ) -> Option<ICMatchedPair> {
222        // Check if we should generate an IC transaction
223        if !self.rng.random_bool(self.config.ic_transaction_rate) {
224            return None;
225        }
226
227        let (seller, buyer) = self.select_company_pair()?;
228        let tx_type = self.select_transaction_type();
229        let base_amount = self.generate_base_amount(tx_type);
230
231        // Find relationship for transfer pricing
232        let relationship_id = format!("{seller}-{buyer}");
233        let transfer_price = self.apply_transfer_pricing(base_amount, &relationship_id);
234
235        let ic_reference = self.generate_ic_reference(date);
236        let seller_doc = self.generate_doc_number("ICS");
237        let buyer_doc = self.generate_doc_number("ICB");
238
239        let mut pair = ICMatchedPair::new(
240            ic_reference,
241            tx_type,
242            seller.clone(),
243            buyer.clone(),
244            transfer_price,
245            self.config.default_currency.clone(),
246            date,
247        );
248
249        // Assign document numbers
250        pair.seller_document = seller_doc;
251        pair.buyer_document = buyer_doc;
252
253        // Calculate withholding tax if applicable
254        if tx_type.has_withholding_tax() {
255            pair.calculate_withholding_tax();
256        }
257
258        self.matched_pairs.push(pair.clone());
259        Some(pair)
260    }
261
262    /// Generate IC journal entries from a matched pair.
263    pub fn generate_journal_entries(
264        &mut self,
265        pair: &ICMatchedPair,
266        fiscal_year: i32,
267        fiscal_period: u32,
268    ) -> (JournalEntry, JournalEntry) {
269        let (seller_dr_desc, seller_cr_desc) = pair.transaction_type.seller_accounts();
270        let (buyer_dr_desc, buyer_cr_desc) = pair.transaction_type.buyer_accounts();
271
272        // Seller entry: DR IC Receivable, CR Revenue/Income
273        let seller_entry = self.create_seller_entry(
274            pair,
275            fiscal_year,
276            fiscal_period,
277            seller_dr_desc,
278            seller_cr_desc,
279        );
280
281        // Buyer entry: DR Expense/Asset, CR IC Payable
282        let buyer_entry = self.create_buyer_entry(
283            pair,
284            fiscal_year,
285            fiscal_period,
286            buyer_dr_desc,
287            buyer_cr_desc,
288        );
289
290        (seller_entry, buyer_entry)
291    }
292
293    /// Create seller-side journal entry.
294    fn create_seller_entry(
295        &mut self,
296        pair: &ICMatchedPair,
297        _fiscal_year: i32,
298        _fiscal_period: u32,
299        dr_desc: &str,
300        cr_desc: &str,
301    ) -> JournalEntry {
302        let mut je = JournalEntry::new_simple(
303            pair.seller_document.clone(),
304            pair.seller_company.clone(),
305            pair.posting_date,
306            format!(
307                "IC {} to {}",
308                pair.transaction_type.seller_accounts().1,
309                pair.buyer_company
310            ),
311        );
312
313        je.header.reference = Some(pair.ic_reference.clone());
314        je.header.document_type = "IC".to_string();
315        je.header.currency = pair.currency.clone();
316        je.header.exchange_rate = Decimal::ONE;
317        je.header.created_by = "IC_GENERATOR".to_string();
318
319        // Debit line: IC Receivable
320        let mut debit_amount = pair.amount;
321        if pair.withholding_tax.is_some() {
322            debit_amount = pair.net_amount();
323        }
324
325        je.add_line(JournalEntryLine {
326            line_number: 1,
327            gl_account: self.get_seller_receivable_account(&pair.buyer_company),
328            debit_amount,
329            text: Some(format!("{} - {}", dr_desc, pair.description)),
330            assignment: Some(pair.ic_reference.clone()),
331            reference: Some(pair.buyer_document.clone()),
332            ..Default::default()
333        });
334
335        // Credit line: Revenue/Income
336        je.add_line(JournalEntryLine {
337            line_number: 2,
338            gl_account: self.get_seller_revenue_account(pair.transaction_type),
339            credit_amount: pair.amount,
340            text: Some(format!("{} - {}", cr_desc, pair.description)),
341            assignment: Some(pair.ic_reference.clone()),
342            ..Default::default()
343        });
344
345        // Add withholding tax line if applicable
346        // WHT is withheld by the buyer and remitted to the tax authority on
347        // behalf of the seller.  From the seller's perspective this is a tax
348        // receivable (debit) — the seller can reclaim it as a tax credit.
349        // DR WHT Receivable + DR IC Receivable(net) = CR Revenue(gross)
350        if let Some(wht) = pair.withholding_tax {
351            je.add_line(JournalEntryLine {
352                line_number: 3,
353                gl_account: self.framework_accounts.sales_tax_payable.clone(), // WHT receivable
354                debit_amount: wht,
355                text: Some("Withholding tax on IC transaction".to_string()),
356                assignment: Some(pair.ic_reference.clone()),
357                ..Default::default()
358            });
359        }
360
361        je
362    }
363
364    /// Create buyer-side journal entry.
365    fn create_buyer_entry(
366        &mut self,
367        pair: &ICMatchedPair,
368        _fiscal_year: i32,
369        _fiscal_period: u32,
370        dr_desc: &str,
371        cr_desc: &str,
372    ) -> JournalEntry {
373        let mut je = JournalEntry::new_simple(
374            pair.buyer_document.clone(),
375            pair.buyer_company.clone(),
376            pair.posting_date,
377            format!(
378                "IC {} from {}",
379                pair.transaction_type.buyer_accounts().0,
380                pair.seller_company
381            ),
382        );
383
384        je.header.reference = Some(pair.ic_reference.clone());
385        je.header.document_type = "IC".to_string();
386        je.header.currency = pair.currency.clone();
387        je.header.exchange_rate = Decimal::ONE;
388        je.header.created_by = "IC_GENERATOR".to_string();
389
390        // Debit line: Expense/Asset
391        je.add_line(JournalEntryLine {
392            line_number: 1,
393            gl_account: self.get_buyer_expense_account(pair.transaction_type),
394            debit_amount: pair.amount,
395            cost_center: Some("CC100".to_string()),
396            text: Some(format!("{} - {}", dr_desc, pair.description)),
397            assignment: Some(pair.ic_reference.clone()),
398            reference: Some(pair.seller_document.clone()),
399            ..Default::default()
400        });
401
402        // Credit line: IC Payable
403        je.add_line(JournalEntryLine {
404            line_number: 2,
405            gl_account: self.get_buyer_payable_account(&pair.seller_company),
406            credit_amount: pair.amount,
407            text: Some(format!("{} - {}", cr_desc, pair.description)),
408            assignment: Some(pair.ic_reference.clone()),
409            ..Default::default()
410        });
411
412        je
413    }
414
415    /// Get IC receivable account for seller.
416    ///
417    /// Uses the framework's IC AR clearing account as a base, appending the
418    /// first two characters of the buyer company code as a sub-account suffix.
419    fn get_seller_receivable_account(&self, buyer_company: &str) -> String {
420        let suffix: String = buyer_company.chars().take(2).collect();
421        format!("{}{}", self.framework_accounts.ic_ar_clearing, suffix)
422    }
423
424    /// Get IC revenue account for seller.
425    ///
426    /// Maps the IC transaction type to the appropriate framework-specific
427    /// revenue account code.
428    fn get_seller_revenue_account(&self, tx_type: ICTransactionType) -> String {
429        let fa = &self.framework_accounts;
430        match tx_type {
431            ICTransactionType::GoodsSale => fa.product_revenue.clone(),
432            ICTransactionType::ServiceProvided => fa.service_revenue.clone(),
433            ICTransactionType::ManagementFee => fa.ic_revenue.clone(),
434            ICTransactionType::Royalty => fa.other_revenue.clone(),
435            ICTransactionType::LoanInterest => fa.other_revenue.clone(),
436            ICTransactionType::Dividend => fa.other_revenue.clone(),
437            _ => fa.ic_revenue.clone(),
438        }
439    }
440
441    /// Get IC expense account for buyer.
442    ///
443    /// Maps the IC transaction type to the appropriate framework-specific
444    /// expense (or equity) account code.
445    fn get_buyer_expense_account(&self, tx_type: ICTransactionType) -> String {
446        let fa = &self.framework_accounts;
447        match tx_type {
448            ICTransactionType::GoodsSale => fa.cogs.clone(),
449            ICTransactionType::ServiceProvided => fa.rent.clone(), // general operating expense
450            ICTransactionType::ManagementFee => fa.rent.clone(),   // general operating expense
451            ICTransactionType::Royalty => fa.rent.clone(),         // general operating expense
452            ICTransactionType::LoanInterest => fa.interest_expense.clone(),
453            ICTransactionType::Dividend => fa.retained_earnings.clone(),
454            _ => fa.cogs.clone(),
455        }
456    }
457
458    /// Get IC payable account for buyer.
459    ///
460    /// Uses the framework's IC AP clearing account as a base, appending the
461    /// first two characters of the seller company code as a sub-account suffix.
462    fn get_buyer_payable_account(&self, seller_company: &str) -> String {
463        let suffix: String = seller_company.chars().take(2).collect();
464        format!("{}{}", self.framework_accounts.ic_ap_clearing, suffix)
465    }
466
467    /// Generate an IC loan.
468    pub fn generate_ic_loan(
469        &mut self,
470        lender: String,
471        borrower: String,
472        start_date: NaiveDate,
473        term_months: u32,
474    ) -> ICLoan {
475        let (min_amount, max_amount) = self.config.loan_amount_range;
476        let range = max_amount - min_amount;
477        let random_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
478        let principal = (min_amount + range * random_factor).round_dp(0);
479
480        let (min_rate, max_rate) = self.config.loan_interest_rate_range;
481        let rate_range = max_rate - min_rate;
482        let rate_factor = Decimal::from_f64_retain(self.rng.random::<f64>()).unwrap_or(dec!(0.5));
483        let interest_rate = (min_rate + rate_range * rate_factor).round_dp(2);
484
485        let maturity_date = start_date
486            .checked_add_months(chrono::Months::new(term_months))
487            .unwrap_or(start_date);
488
489        let loan_id = format!(
490            "LOAN{}{:04}",
491            start_date.format("%Y"),
492            self.active_loans.len() + 1
493        );
494
495        let loan = ICLoan::new(
496            loan_id,
497            lender,
498            borrower,
499            principal,
500            self.config.default_currency.clone(),
501            interest_rate,
502            start_date,
503            maturity_date,
504        );
505
506        self.active_loans.push(loan.clone());
507        loan
508    }
509
510    /// Generate interest entries for active loans.
511    pub fn generate_loan_interest_entries(
512        &mut self,
513        as_of_date: NaiveDate,
514        fiscal_year: i32,
515        fiscal_period: u32,
516    ) -> Vec<(JournalEntry, JournalEntry)> {
517        // Collect loan data to avoid borrow issues
518        let loans_data: Vec<_> = self
519            .active_loans
520            .iter()
521            .filter(|loan| !loan.is_repaid())
522            .map(|loan| {
523                let period_start = NaiveDate::from_ymd_opt(
524                    if fiscal_period == 1 {
525                        fiscal_year - 1
526                    } else {
527                        fiscal_year
528                    },
529                    if fiscal_period == 1 {
530                        12
531                    } else {
532                        fiscal_period - 1
533                    },
534                    1,
535                )
536                .unwrap_or(as_of_date);
537
538                let interest = loan.calculate_interest(period_start, as_of_date);
539                (
540                    loan.loan_id.clone(),
541                    loan.lender_company.clone(),
542                    loan.borrower_company.clone(),
543                    loan.currency.clone(),
544                    interest,
545                )
546            })
547            .filter(|(_, _, _, _, interest)| *interest > Decimal::ZERO)
548            .collect();
549
550        let mut entries = Vec::new();
551
552        for (loan_id, lender, borrower, currency, interest) in loans_data {
553            let ic_ref = self.generate_ic_reference(as_of_date);
554            let seller_doc = self.generate_doc_number("INT");
555            let buyer_doc = self.generate_doc_number("INT");
556
557            let mut pair = ICMatchedPair::new(
558                ic_ref,
559                ICTransactionType::LoanInterest,
560                lender,
561                borrower,
562                interest,
563                currency,
564                as_of_date,
565            );
566            pair.seller_document = seller_doc;
567            pair.buyer_document = buyer_doc;
568            pair.description = format!("Interest on loan {loan_id}");
569
570            let (seller_je, buyer_je) =
571                self.generate_journal_entries(&pair, fiscal_year, fiscal_period);
572            entries.push((seller_je, buyer_je));
573        }
574
575        entries
576    }
577
578    /// Get all generated matched pairs.
579    pub fn get_matched_pairs(&self) -> &[ICMatchedPair] {
580        &self.matched_pairs
581    }
582
583    /// Get open (unsettled) matched pairs.
584    pub fn get_open_pairs(&self) -> Vec<&ICMatchedPair> {
585        self.matched_pairs.iter().filter(|p| p.is_open()).collect()
586    }
587
588    /// Get active loans.
589    pub fn get_active_loans(&self) -> &[ICLoan] {
590        &self.active_loans
591    }
592
593    /// Generate multiple IC transactions for a date range.
594    pub fn generate_transactions_for_period(
595        &mut self,
596        start_date: NaiveDate,
597        end_date: NaiveDate,
598        transactions_per_day: usize,
599    ) -> Vec<ICMatchedPair> {
600        debug!(%start_date, %end_date, transactions_per_day, "Generating intercompany transactions");
601        let mut pairs = Vec::new();
602        let mut current_date = start_date;
603
604        while current_date <= end_date {
605            let fiscal_period = format!("{}{:02}", current_date.year(), current_date.month());
606
607            for _ in 0..transactions_per_day {
608                if let Some(pair) = self.generate_ic_transaction(current_date, &fiscal_period) {
609                    pairs.push(pair);
610                }
611            }
612
613            current_date = current_date.succ_opt().unwrap_or(current_date);
614        }
615
616        pairs
617    }
618
619    /// Reset counters (for testing).
620    pub fn reset_counters(&mut self) {
621        self.ic_counter = 0;
622        self.doc_counter = 0;
623        self.matched_pairs.clear();
624    }
625
626    /// Generate P2P/O2C source documents from IC matched pairs.
627    ///
628    /// For each eligible pair (GoodsSale, ServiceProvided, ManagementFee,
629    /// Royalty, or ExpenseRecharge) this creates:
630    ///
631    /// - **Seller side**: a [`CustomerInvoice`] billed to the buyer company
632    /// - **Buyer side**: a [`PurchaseOrder`], [`GoodsReceipt`], and
633    ///   [`VendorInvoice`] referencing the seller company
634    ///
635    /// All documents carry the IC reference in `header.reference` so
636    /// downstream processes can trace them back to the originating
637    /// [`ICMatchedPair`].
638    pub fn generate_ic_document_chains(&mut self, pairs: &[ICMatchedPair]) -> ICDocumentChains {
639        let eligible_types = [
640            ICTransactionType::GoodsSale,
641            ICTransactionType::ServiceProvided,
642            ICTransactionType::ManagementFee,
643            ICTransactionType::Royalty,
644            ICTransactionType::ExpenseRecharge,
645        ];
646
647        let mut chains = ICDocumentChains {
648            seller_invoices: Vec::new(),
649            buyer_orders: Vec::new(),
650            buyer_goods_receipts: Vec::new(),
651            buyer_invoices: Vec::new(),
652        };
653
654        for pair in pairs {
655            if !eligible_types.contains(&pair.transaction_type) {
656                continue;
657            }
658
659            let date = pair.posting_date;
660            let fiscal_year = date.year() as u16;
661            let fiscal_period = date.month() as u8;
662
663            // --- Seller side: CustomerInvoice ---
664            let ci_doc_id = self.generate_doc_number("IC-CI");
665            let due_date = date + chrono::Duration::days(30);
666
667            let mut ci = CustomerInvoice::new(
668                &ci_doc_id,
669                &pair.seller_company,
670                &pair.buyer_company,
671                fiscal_year,
672                fiscal_period,
673                date,
674                due_date,
675                "IC_GENERATOR",
676            );
677            ci.invoice_type = CustomerInvoiceType::Intercompany;
678            ci.is_intercompany = true;
679            ci.ic_partner = Some(pair.buyer_company.clone());
680            ci.header.reference = Some(pair.ic_reference.clone());
681            ci.header.currency = pair.currency.clone();
682            ci.header.posting_date = Some(date);
683
684            let description = format!("IC {:?} to {}", pair.transaction_type, pair.buyer_company);
685            ci.add_item(CustomerInvoiceItem::new(
686                1,
687                &description,
688                Decimal::ONE,
689                pair.amount,
690            ));
691
692            chains.seller_invoices.push(ci);
693
694            // --- Buyer side: PurchaseOrder ---
695            let po_doc_id = self.generate_doc_number("IC-PO");
696
697            let mut po = PurchaseOrder::new(
698                &po_doc_id,
699                &pair.buyer_company,
700                &pair.seller_company,
701                fiscal_year,
702                fiscal_period,
703                date,
704                "IC_GENERATOR",
705            );
706            po.header.reference = Some(pair.ic_reference.clone());
707            po.header.currency = pair.currency.clone();
708
709            let po_desc = format!(
710                "IC {:?} from {}",
711                pair.transaction_type, pair.seller_company
712            );
713            po.add_item(PurchaseOrderItem::new(
714                1,
715                &po_desc,
716                Decimal::ONE,
717                pair.amount,
718            ));
719
720            chains.buyer_orders.push(po);
721
722            // --- Buyer side: GoodsReceipt ---
723            let gr_doc_id = self.generate_doc_number("IC-GR");
724
725            let mut gr = GoodsReceipt::from_purchase_order(
726                &gr_doc_id,
727                &pair.buyer_company,
728                &po_doc_id,
729                &pair.seller_company,
730                "1000", // default plant
731                "0001", // default storage location
732                fiscal_year,
733                fiscal_period,
734                date,
735                "IC_GENERATOR",
736            );
737            gr.header.reference = Some(pair.ic_reference.clone());
738            gr.header.currency = pair.currency.clone();
739
740            let gr_desc = format!(
741                "IC {:?} receipt from {}",
742                pair.transaction_type, pair.seller_company
743            );
744            gr.add_item(GoodsReceiptItem::from_po(
745                1,
746                &gr_desc,
747                Decimal::ONE,
748                pair.amount,
749                &po_doc_id,
750                1,
751            ));
752
753            chains.buyer_goods_receipts.push(gr);
754
755            // --- Buyer side: VendorInvoice ---
756            let vi_doc_id = self.generate_doc_number("IC-VI");
757            let vendor_inv_number = format!("EXT-{}", pair.ic_reference);
758
759            let mut vi = VendorInvoice::from_po_gr(
760                &vi_doc_id,
761                &pair.buyer_company,
762                &pair.seller_company,
763                &vendor_inv_number,
764                &po_doc_id,
765                &gr_doc_id,
766                fiscal_year,
767                fiscal_period,
768                date,
769                "IC_GENERATOR",
770            );
771            vi.header.reference = Some(pair.ic_reference.clone());
772            vi.header.currency = pair.currency.clone();
773
774            let vi_desc = format!(
775                "IC {:?} invoice from {}",
776                pair.transaction_type, pair.seller_company
777            );
778            vi.add_item(VendorInvoiceItem::from_po_gr(
779                1,
780                &vi_desc,
781                Decimal::ONE,
782                pair.amount,
783                &po_doc_id,
784                1,
785                Some(gr_doc_id.clone()),
786                Some(1),
787            ));
788
789            chains.buyer_invoices.push(vi);
790        }
791
792        chains
793    }
794}
795
796/// Source documents generated from IC matched pairs.
797///
798/// Each eligible [`ICMatchedPair`] produces a full set of P2P/O2C
799/// documents linking the seller and buyer sides of the intercompany
800/// transaction.
801#[derive(Debug, Clone)]
802pub struct ICDocumentChains {
803    /// Customer invoices issued by the seller company.
804    pub seller_invoices: Vec<CustomerInvoice>,
805    /// Purchase orders created by the buyer company.
806    pub buyer_orders: Vec<PurchaseOrder>,
807    /// Goods receipts posted by the buyer company.
808    pub buyer_goods_receipts: Vec<GoodsReceipt>,
809    /// Vendor invoices received by the buyer company.
810    pub buyer_invoices: Vec<VendorInvoice>,
811}
812
813#[cfg(test)]
814#[allow(clippy::unwrap_used)]
815mod tests {
816    use super::*;
817    use chrono::NaiveDate;
818    use datasynth_core::models::intercompany::IntercompanyRelationship;
819    use rust_decimal_macros::dec;
820
821    fn create_test_ownership_structure() -> OwnershipStructure {
822        let mut structure = OwnershipStructure::new("1000".to_string());
823        structure.add_relationship(IntercompanyRelationship::new(
824            "REL001".to_string(),
825            "1000".to_string(),
826            "1100".to_string(),
827            dec!(100),
828            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
829        ));
830        structure.add_relationship(IntercompanyRelationship::new(
831            "REL002".to_string(),
832            "1000".to_string(),
833            "1200".to_string(),
834            dec!(100),
835            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
836        ));
837        structure
838    }
839
840    #[test]
841    fn test_ic_generator_creation() {
842        let config = ICGeneratorConfig::default();
843        let structure = create_test_ownership_structure();
844        let generator = ICGenerator::new(config, structure, 12345);
845
846        assert!(generator.matched_pairs.is_empty());
847        assert!(generator.active_loans.is_empty());
848    }
849
850    #[test]
851    fn test_generate_ic_transaction() {
852        let config = ICGeneratorConfig {
853            ic_transaction_rate: 1.0, // Always generate
854            ..Default::default()
855        };
856
857        let structure = create_test_ownership_structure();
858        let mut generator = ICGenerator::new(config, structure, 12345);
859
860        let date = NaiveDate::from_ymd_opt(2022, 6, 15).unwrap();
861        let pair = generator.generate_ic_transaction(date, "202206");
862
863        assert!(pair.is_some());
864        let pair = pair.unwrap();
865        assert!(!pair.ic_reference.is_empty());
866        assert!(pair.amount > Decimal::ZERO);
867    }
868
869    #[test]
870    fn test_generate_journal_entries() {
871        let config = ICGeneratorConfig {
872            ic_transaction_rate: 1.0,
873            ..Default::default()
874        };
875
876        let structure = create_test_ownership_structure();
877        let mut generator = ICGenerator::new(config, structure, 12345);
878
879        let date = NaiveDate::from_ymd_opt(2022, 6, 15).unwrap();
880        let pair = generator.generate_ic_transaction(date, "202206").unwrap();
881
882        let (seller_je, buyer_je) = generator.generate_journal_entries(&pair, 2022, 6);
883
884        assert_eq!(seller_je.company_code(), pair.seller_company);
885        assert_eq!(buyer_je.company_code(), pair.buyer_company);
886        assert_eq!(seller_je.header.reference, Some(pair.ic_reference.clone()));
887        assert_eq!(buyer_je.header.reference, Some(pair.ic_reference));
888    }
889
890    #[test]
891    fn test_generate_ic_loan() {
892        let config = ICGeneratorConfig::default();
893        let structure = create_test_ownership_structure();
894        let mut generator = ICGenerator::new(config, structure, 12345);
895
896        let loan = generator.generate_ic_loan(
897            "1000".to_string(),
898            "1100".to_string(),
899            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
900            24,
901        );
902
903        assert!(!loan.loan_id.is_empty());
904        assert!(loan.principal > Decimal::ZERO);
905        assert!(loan.interest_rate > Decimal::ZERO);
906        assert_eq!(generator.active_loans.len(), 1);
907    }
908
909    #[test]
910    fn test_generate_transactions_for_period() {
911        let config = ICGeneratorConfig {
912            ic_transaction_rate: 1.0,
913            ..Default::default()
914        };
915
916        let structure = create_test_ownership_structure();
917        let mut generator = ICGenerator::new(config, structure, 12345);
918
919        let start = NaiveDate::from_ymd_opt(2022, 6, 1).unwrap();
920        let end = NaiveDate::from_ymd_opt(2022, 6, 5).unwrap();
921
922        let pairs = generator.generate_transactions_for_period(start, end, 2);
923
924        // 5 days * 2 transactions per day = 10 transactions
925        assert_eq!(pairs.len(), 10);
926    }
927}