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