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