Skip to main content

datasynth_generators/intercompany/
elimination_generator.rs

1//! Consolidation elimination entry generator.
2//!
3//! Generates elimination entries for intercompany balances, revenue/expense,
4//! unrealized profits, and investment/equity eliminations.
5
6use chrono::NaiveDate;
7use rust_decimal::Decimal;
8use rust_decimal_macros::dec;
9use std::collections::HashMap;
10
11use datasynth_core::models::intercompany::{
12    ConsolidationJournal, ConsolidationMethod, ConsolidationStatus, EliminationEntry,
13    EliminationType, ICAggregatedBalance, ICMatchedPair, ICTransactionType, OwnershipStructure,
14};
15
16/// Configuration for elimination generation.
17#[derive(Debug, Clone)]
18pub struct EliminationConfig {
19    /// Consolidation entity code.
20    pub consolidation_entity: String,
21    /// Base currency for eliminations.
22    pub base_currency: String,
23    /// Generate IC balance eliminations.
24    pub eliminate_ic_balances: bool,
25    /// Generate IC revenue/expense eliminations.
26    pub eliminate_ic_revenue_expense: bool,
27    /// Generate unrealized profit eliminations.
28    pub eliminate_unrealized_profit: bool,
29    /// Generate investment/equity eliminations.
30    pub eliminate_investment_equity: bool,
31    /// Average markup rate for unrealized profit calculation.
32    pub average_markup_rate: Decimal,
33    /// Percentage of IC inventory remaining at period end.
34    pub ic_inventory_percent: Decimal,
35}
36
37impl Default for EliminationConfig {
38    fn default() -> Self {
39        Self {
40            consolidation_entity: "GROUP".to_string(),
41            base_currency: "USD".to_string(),
42            eliminate_ic_balances: true,
43            eliminate_ic_revenue_expense: true,
44            eliminate_unrealized_profit: true,
45            eliminate_investment_equity: true,
46            average_markup_rate: dec!(0.05),
47            ic_inventory_percent: dec!(0.20),
48        }
49    }
50}
51
52/// Generator for consolidation elimination entries.
53pub struct EliminationGenerator {
54    /// Configuration.
55    config: EliminationConfig,
56    /// Ownership structure.
57    ownership_structure: OwnershipStructure,
58    /// Entry counter.
59    entry_counter: u64,
60    /// Generated elimination journals.
61    journals: HashMap<String, ConsolidationJournal>,
62}
63
64impl EliminationGenerator {
65    /// Create a new elimination generator.
66    pub fn new(config: EliminationConfig, ownership_structure: OwnershipStructure) -> Self {
67        Self {
68            config,
69            ownership_structure,
70            entry_counter: 0,
71            journals: HashMap::new(),
72        }
73    }
74
75    /// Generate entry ID.
76    fn generate_entry_id(&mut self, elim_type: EliminationType) -> String {
77        self.entry_counter += 1;
78        let prefix = match elim_type {
79            EliminationType::ICBalances => "EB",
80            EliminationType::ICRevenueExpense => "ER",
81            EliminationType::ICProfitInInventory => "EP",
82            EliminationType::ICProfitInFixedAssets => "EA",
83            EliminationType::InvestmentEquity => "EI",
84            EliminationType::ICDividends => "ED",
85            EliminationType::ICLoans => "EL",
86            EliminationType::ICInterest => "EN",
87            EliminationType::MinorityInterest => "EM",
88            EliminationType::Goodwill => "EG",
89            EliminationType::CurrencyTranslation => "EC",
90        };
91        format!("{}{:06}", prefix, self.entry_counter)
92    }
93
94    /// Get or create consolidation journal for a period.
95    fn get_or_create_journal(
96        &mut self,
97        fiscal_period: &str,
98        entry_date: NaiveDate,
99    ) -> &mut ConsolidationJournal {
100        self.journals
101            .entry(fiscal_period.to_string())
102            .or_insert_with(|| {
103                ConsolidationJournal::new(
104                    self.config.consolidation_entity.clone(),
105                    fiscal_period.to_string(),
106                    entry_date,
107                )
108            })
109    }
110
111    /// Generate all eliminations for a period.
112    pub fn generate_eliminations(
113        &mut self,
114        fiscal_period: &str,
115        entry_date: NaiveDate,
116        ic_balances: &[ICAggregatedBalance],
117        ic_transactions: &[ICMatchedPair],
118        investment_amounts: &HashMap<String, Decimal>,
119        equity_amounts: &HashMap<String, HashMap<String, Decimal>>,
120    ) -> &ConsolidationJournal {
121        // Generate IC balance eliminations
122        if self.config.eliminate_ic_balances {
123            self.generate_ic_balance_eliminations(fiscal_period, entry_date, ic_balances);
124        }
125
126        // Generate IC revenue/expense eliminations
127        if self.config.eliminate_ic_revenue_expense {
128            self.generate_ic_revenue_expense_eliminations(
129                fiscal_period,
130                entry_date,
131                ic_transactions,
132            );
133        }
134
135        // Generate unrealized profit eliminations
136        if self.config.eliminate_unrealized_profit {
137            self.generate_unrealized_profit_eliminations(
138                fiscal_period,
139                entry_date,
140                ic_transactions,
141            );
142        }
143
144        // Generate investment/equity eliminations
145        if self.config.eliminate_investment_equity {
146            self.generate_investment_equity_eliminations(
147                fiscal_period,
148                entry_date,
149                investment_amounts,
150                equity_amounts,
151            );
152        }
153
154        self.get_or_create_journal(fiscal_period, entry_date)
155    }
156
157    /// Generate IC balance eliminations (receivables vs payables).
158    pub fn generate_ic_balance_eliminations(
159        &mut self,
160        fiscal_period: &str,
161        entry_date: NaiveDate,
162        balances: &[ICAggregatedBalance],
163    ) {
164        for balance in balances {
165            if balance.elimination_amount() == Decimal::ZERO {
166                continue;
167            }
168
169            let entry = EliminationEntry::create_ic_balance_elimination(
170                self.generate_entry_id(EliminationType::ICBalances),
171                self.config.consolidation_entity.clone(),
172                fiscal_period.to_string(),
173                entry_date,
174                &balance.creditor_company,
175                &balance.debtor_company,
176                &balance.receivable_account,
177                &balance.payable_account,
178                balance.elimination_amount(),
179                balance.currency.clone(),
180            );
181
182            let journal = self.get_or_create_journal(fiscal_period, entry_date);
183            journal.add_entry(entry);
184        }
185    }
186
187    /// Generate IC revenue/expense eliminations.
188    pub fn generate_ic_revenue_expense_eliminations(
189        &mut self,
190        fiscal_period: &str,
191        entry_date: NaiveDate,
192        transactions: &[ICMatchedPair],
193    ) {
194        // Aggregate by seller/buyer pair and transaction type
195        let mut aggregated: HashMap<(String, String, ICTransactionType), Decimal> = HashMap::new();
196
197        for tx in transactions {
198            if tx.transaction_type.affects_pnl() {
199                let key = (
200                    tx.seller_company.clone(),
201                    tx.buyer_company.clone(),
202                    tx.transaction_type,
203                );
204                *aggregated.entry(key).or_insert(Decimal::ZERO) += tx.amount;
205            }
206        }
207
208        for ((seller, buyer, tx_type), amount) in aggregated {
209            if amount == Decimal::ZERO {
210                continue;
211            }
212
213            let revenue_account = match tx_type {
214                ICTransactionType::GoodsSale => "4100",
215                ICTransactionType::ServiceProvided => "4200",
216                ICTransactionType::ManagementFee => "4300",
217                ICTransactionType::Royalty => "4400",
218                ICTransactionType::LoanInterest => "4500",
219                _ => "4900",
220            };
221
222            let expense_account = match tx_type {
223                ICTransactionType::GoodsSale => "5100",
224                ICTransactionType::ServiceProvided => "5200",
225                ICTransactionType::ManagementFee => "5300",
226                ICTransactionType::Royalty => "5400",
227                ICTransactionType::LoanInterest => "5500",
228                _ => "5900",
229            };
230
231            let entry = EliminationEntry::create_ic_revenue_expense_elimination(
232                self.generate_entry_id(EliminationType::ICRevenueExpense),
233                self.config.consolidation_entity.clone(),
234                fiscal_period.to_string(),
235                entry_date,
236                &seller,
237                &buyer,
238                revenue_account,
239                expense_account,
240                amount,
241                self.config.base_currency.clone(),
242            );
243
244            let journal = self.get_or_create_journal(fiscal_period, entry_date);
245            journal.add_entry(entry);
246        }
247    }
248
249    /// Generate unrealized profit in inventory eliminations.
250    pub fn generate_unrealized_profit_eliminations(
251        &mut self,
252        fiscal_period: &str,
253        entry_date: NaiveDate,
254        transactions: &[ICMatchedPair],
255    ) {
256        // Calculate unrealized profit from goods sales
257        let mut unrealized_by_pair: HashMap<(String, String), Decimal> = HashMap::new();
258
259        for tx in transactions {
260            if tx.transaction_type == ICTransactionType::GoodsSale {
261                let key = (tx.seller_company.clone(), tx.buyer_company.clone());
262
263                // Unrealized profit = IC sales amount * markup rate * % in inventory
264                let unrealized =
265                    tx.amount * self.config.average_markup_rate * self.config.ic_inventory_percent;
266
267                *unrealized_by_pair.entry(key).or_insert(Decimal::ZERO) += unrealized;
268            }
269        }
270
271        for ((seller, buyer), unrealized_profit) in unrealized_by_pair {
272            if unrealized_profit < dec!(0.01) {
273                continue;
274            }
275
276            let entry = EliminationEntry::create_unrealized_profit_elimination(
277                self.generate_entry_id(EliminationType::ICProfitInInventory),
278                self.config.consolidation_entity.clone(),
279                fiscal_period.to_string(),
280                entry_date,
281                &seller,
282                &buyer,
283                unrealized_profit.round_dp(2),
284                self.config.base_currency.clone(),
285            );
286
287            let journal = self.get_or_create_journal(fiscal_period, entry_date);
288            journal.add_entry(entry);
289        }
290    }
291
292    /// Generate investment/equity eliminations.
293    pub fn generate_investment_equity_eliminations(
294        &mut self,
295        fiscal_period: &str,
296        entry_date: NaiveDate,
297        investment_amounts: &HashMap<String, Decimal>,
298        equity_amounts: &HashMap<String, HashMap<String, Decimal>>,
299    ) {
300        // Collect relationships that need processing to avoid borrow issues
301        let relationships_to_process: Vec<_> = self
302            .ownership_structure
303            .relationships
304            .iter()
305            .filter(|r| r.consolidation_method == ConsolidationMethod::Full)
306            .map(|r| {
307                (
308                    r.parent_company.clone(),
309                    r.subsidiary_company.clone(),
310                    r.ownership_percentage,
311                )
312            })
313            .collect();
314
315        for (parent, subsidiary, ownership_pct) in relationships_to_process {
316            let investment = investment_amounts
317                .get(&format!("{parent}_{subsidiary}"))
318                .copied()
319                .unwrap_or(Decimal::ZERO);
320
321            if investment == Decimal::ZERO {
322                continue;
323            }
324
325            // Get equity components
326            let equity_components: Vec<(String, Decimal)> = equity_amounts
327                .get(&subsidiary)
328                .map(|eq| eq.iter().map(|(k, v)| (k.clone(), *v)).collect())
329                .unwrap_or_else(|| {
330                    // Default equity components if not provided
331                    vec![
332                        ("3100".to_string(), investment * dec!(0.10)), // Common stock
333                        ("3200".to_string(), investment * dec!(0.30)), // APIC
334                        ("3300".to_string(), investment * dec!(0.60)), // Retained earnings
335                    ]
336                });
337
338            let total_equity: Decimal = equity_components.iter().map(|(_, v)| v).sum();
339
340            // Calculate goodwill (investment > equity) or bargain purchase (investment < equity)
341            let goodwill = if investment > total_equity {
342                Some(investment - total_equity)
343            } else {
344                None
345            };
346
347            // Calculate minority interest for non-100% ownership
348            let minority_interest = if ownership_pct < dec!(100) {
349                let minority_pct = (dec!(100) - ownership_pct) / dec!(100);
350                Some(total_equity * minority_pct)
351            } else {
352                None
353            };
354
355            let entry_id = self.generate_entry_id(EliminationType::InvestmentEquity);
356            let consolidation_entity = self.config.consolidation_entity.clone();
357            let base_currency = self.config.base_currency.clone();
358
359            let entry = EliminationEntry::create_investment_equity_elimination(
360                entry_id,
361                consolidation_entity,
362                fiscal_period.to_string(),
363                entry_date,
364                &parent,
365                &subsidiary,
366                investment,
367                equity_components,
368                goodwill,
369                minority_interest,
370                base_currency,
371            );
372
373            let journal = self.get_or_create_journal(fiscal_period, entry_date);
374            journal.add_entry(entry);
375        }
376    }
377
378    /// Generate dividend elimination entry.
379    pub fn generate_dividend_elimination(
380        &mut self,
381        fiscal_period: &str,
382        entry_date: NaiveDate,
383        paying_company: &str,
384        receiving_company: &str,
385        dividend_amount: Decimal,
386    ) -> EliminationEntry {
387        let mut entry = EliminationEntry::new(
388            self.generate_entry_id(EliminationType::ICDividends),
389            EliminationType::ICDividends,
390            self.config.consolidation_entity.clone(),
391            fiscal_period.to_string(),
392            entry_date,
393            self.config.base_currency.clone(),
394        );
395
396        entry.related_companies = vec![paying_company.to_string(), receiving_company.to_string()];
397        entry.description =
398            format!("Eliminate IC dividend from {paying_company} to {receiving_company}");
399
400        // Debit dividend income (reduce income)
401        entry.add_line(datasynth_core::models::intercompany::EliminationLine {
402            line_number: 1,
403            company: receiving_company.to_string(),
404            account: "4600".to_string(), // Dividend income
405            is_debit: true,
406            amount: dividend_amount,
407            currency: self.config.base_currency.clone(),
408            description: "Eliminate dividend income".to_string(),
409        });
410
411        // Credit retained earnings (restore to subsidiary)
412        entry.add_line(datasynth_core::models::intercompany::EliminationLine {
413            line_number: 2,
414            company: paying_company.to_string(),
415            account: "3300".to_string(), // Retained earnings
416            is_debit: false,
417            amount: dividend_amount,
418            currency: self.config.base_currency.clone(),
419            description: "Restore retained earnings".to_string(),
420        });
421
422        let journal = self.get_or_create_journal(fiscal_period, entry_date);
423        journal.add_entry(entry.clone());
424
425        entry
426    }
427
428    /// Generate minority interest allocation for period profit/loss.
429    pub fn generate_minority_interest_allocation(
430        &mut self,
431        fiscal_period: &str,
432        entry_date: NaiveDate,
433        subsidiary: &str,
434        net_income: Decimal,
435        minority_percentage: Decimal,
436    ) -> Option<EliminationEntry> {
437        if minority_percentage <= Decimal::ZERO || minority_percentage >= dec!(100) {
438            return None;
439        }
440
441        let minority_share = net_income * minority_percentage / dec!(100);
442
443        if minority_share.abs() < dec!(0.01) {
444            return None;
445        }
446
447        let mut entry = EliminationEntry::new(
448            self.generate_entry_id(EliminationType::MinorityInterest),
449            EliminationType::MinorityInterest,
450            self.config.consolidation_entity.clone(),
451            fiscal_period.to_string(),
452            entry_date,
453            self.config.base_currency.clone(),
454        );
455
456        entry.related_companies = vec![subsidiary.to_string()];
457        entry.description = format!("Minority interest share of {subsidiary} profit/loss");
458
459        if net_income > Decimal::ZERO {
460            // Profit: DR consolidated income, CR NCI
461            entry.add_line(datasynth_core::models::intercompany::EliminationLine {
462                line_number: 1,
463                company: self.config.consolidation_entity.clone(),
464                account: "3400".to_string(), // NCI share of income
465                is_debit: true,
466                amount: minority_share,
467                currency: self.config.base_currency.clone(),
468                description: "NCI share of net income".to_string(),
469            });
470
471            entry.add_line(datasynth_core::models::intercompany::EliminationLine {
472                line_number: 2,
473                company: self.config.consolidation_entity.clone(),
474                account: "3500".to_string(), // Non-controlling interest
475                is_debit: false,
476                amount: minority_share,
477                currency: self.config.base_currency.clone(),
478                description: "Increase NCI for share of income".to_string(),
479            });
480        } else {
481            // Loss: DR NCI, CR consolidated loss
482            entry.add_line(datasynth_core::models::intercompany::EliminationLine {
483                line_number: 1,
484                company: self.config.consolidation_entity.clone(),
485                account: "3500".to_string(), // Non-controlling interest
486                is_debit: true,
487                amount: minority_share.abs(),
488                currency: self.config.base_currency.clone(),
489                description: "Decrease NCI for share of loss".to_string(),
490            });
491
492            entry.add_line(datasynth_core::models::intercompany::EliminationLine {
493                line_number: 2,
494                company: self.config.consolidation_entity.clone(),
495                account: "3400".to_string(), // NCI share of income
496                is_debit: false,
497                amount: minority_share.abs(),
498                currency: self.config.base_currency.clone(),
499                description: "NCI share of net loss".to_string(),
500            });
501        }
502
503        let journal = self.get_or_create_journal(fiscal_period, entry_date);
504        journal.add_entry(entry.clone());
505
506        Some(entry)
507    }
508
509    /// Get consolidation journal for a period.
510    pub fn get_journal(&self, fiscal_period: &str) -> Option<&ConsolidationJournal> {
511        self.journals.get(fiscal_period)
512    }
513
514    /// Get all journals.
515    pub fn get_all_journals(&self) -> &HashMap<String, ConsolidationJournal> {
516        &self.journals
517    }
518
519    /// Finalize and approve a journal.
520    pub fn finalize_journal(
521        &mut self,
522        fiscal_period: &str,
523        approved_by: String,
524    ) -> Option<&ConsolidationJournal> {
525        if let Some(journal) = self.journals.get_mut(fiscal_period) {
526            journal.submit();
527            journal.approve(approved_by);
528            Some(journal)
529        } else {
530            None
531        }
532    }
533
534    /// Post a journal.
535    pub fn post_journal(&mut self, fiscal_period: &str) -> Option<&ConsolidationJournal> {
536        if let Some(journal) = self.journals.get_mut(fiscal_period) {
537            journal.post();
538            Some(journal)
539        } else {
540            None
541        }
542    }
543
544    /// Get elimination summary for a period.
545    pub fn get_summary(&self, fiscal_period: &str) -> Option<EliminationSummaryReport> {
546        self.journals.get(fiscal_period).map(|journal| {
547            let mut by_type: HashMap<EliminationType, (usize, Decimal)> = HashMap::new();
548
549            for entry in &journal.entries {
550                let stats = by_type
551                    .entry(entry.elimination_type)
552                    .or_insert((0, Decimal::ZERO));
553                stats.0 += 1;
554                stats.1 += entry.total_debit;
555            }
556
557            EliminationSummaryReport {
558                fiscal_period: fiscal_period.to_string(),
559                consolidation_entity: journal.consolidation_entity.clone(),
560                total_entries: journal.entries.len(),
561                total_debit: journal.total_debits,
562                total_credit: journal.total_credits,
563                is_balanced: journal.is_balanced,
564                status: journal.status,
565                by_type,
566            }
567        })
568    }
569
570    /// Reset counters and clear journals.
571    pub fn reset(&mut self) {
572        self.entry_counter = 0;
573        self.journals.clear();
574    }
575}
576
577/// Summary report for elimination entries.
578#[derive(Debug, Clone)]
579pub struct EliminationSummaryReport {
580    /// Fiscal period.
581    pub fiscal_period: String,
582    /// Consolidation entity.
583    pub consolidation_entity: String,
584    /// Total number of entries.
585    pub total_entries: usize,
586    /// Total debit amount.
587    pub total_debit: Decimal,
588    /// Total credit amount.
589    pub total_credit: Decimal,
590    /// Is the journal balanced?
591    pub is_balanced: bool,
592    /// Journal status.
593    pub status: ConsolidationStatus,
594    /// Breakdown by elimination type (count, amount).
595    pub by_type: HashMap<EliminationType, (usize, Decimal)>,
596}
597
598#[cfg(test)]
599#[allow(clippy::unwrap_used)]
600mod tests {
601    use super::*;
602    use chrono::NaiveDate;
603    use datasynth_core::models::intercompany::IntercompanyRelationship;
604    use rust_decimal_macros::dec;
605
606    fn create_test_ownership_structure() -> OwnershipStructure {
607        let mut structure = OwnershipStructure::new("1000".to_string());
608        structure.add_relationship(IntercompanyRelationship::new(
609            "REL001".to_string(),
610            "1000".to_string(),
611            "1100".to_string(),
612            dec!(100),
613            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
614        ));
615        structure.add_relationship(IntercompanyRelationship::new(
616            "REL002".to_string(),
617            "1000".to_string(),
618            "1200".to_string(),
619            dec!(80),
620            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
621        ));
622        structure
623    }
624
625    #[test]
626    fn test_elimination_generator_creation() {
627        let config = EliminationConfig::default();
628        let structure = create_test_ownership_structure();
629        let generator = EliminationGenerator::new(config, structure);
630
631        assert!(generator.journals.is_empty());
632    }
633
634    #[test]
635    fn test_generate_ic_balance_eliminations() {
636        let config = EliminationConfig::default();
637        let structure = create_test_ownership_structure();
638        let mut generator = EliminationGenerator::new(config, structure);
639
640        let balances = vec![ICAggregatedBalance {
641            creditor_company: "1000".to_string(),
642            debtor_company: "1100".to_string(),
643            receivable_account: "1310".to_string(),
644            payable_account: "2110".to_string(),
645            receivable_balance: dec!(50000),
646            payable_balance: dec!(50000),
647            difference: Decimal::ZERO,
648            currency: "USD".to_string(),
649            as_of_date: NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
650            is_matched: true,
651        }];
652
653        generator.generate_ic_balance_eliminations(
654            "202206",
655            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
656            &balances,
657        );
658
659        let journal = generator.get_journal("202206").unwrap();
660        assert_eq!(journal.entries.len(), 1);
661        assert!(journal.is_balanced);
662    }
663
664    #[test]
665    fn test_generate_ic_revenue_expense_eliminations() {
666        let config = EliminationConfig::default();
667        let structure = create_test_ownership_structure();
668        let mut generator = EliminationGenerator::new(config, structure);
669
670        let transactions = vec![ICMatchedPair::new(
671            "IC001".to_string(),
672            ICTransactionType::ServiceProvided,
673            "1000".to_string(),
674            "1100".to_string(),
675            dec!(25000),
676            "USD".to_string(),
677            NaiveDate::from_ymd_opt(2022, 6, 15).unwrap(),
678        )];
679
680        generator.generate_ic_revenue_expense_eliminations(
681            "202206",
682            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
683            &transactions,
684        );
685
686        let journal = generator.get_journal("202206").unwrap();
687        assert_eq!(journal.entries.len(), 1);
688        assert!(journal.is_balanced);
689    }
690
691    #[test]
692    fn test_generate_unrealized_profit_eliminations() {
693        let config = EliminationConfig::default();
694        let structure = create_test_ownership_structure();
695        let mut generator = EliminationGenerator::new(config, structure);
696
697        let transactions = vec![ICMatchedPair::new(
698            "IC001".to_string(),
699            ICTransactionType::GoodsSale,
700            "1000".to_string(),
701            "1100".to_string(),
702            dec!(100000),
703            "USD".to_string(),
704            NaiveDate::from_ymd_opt(2022, 6, 15).unwrap(),
705        )];
706
707        generator.generate_unrealized_profit_eliminations(
708            "202206",
709            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
710            &transactions,
711        );
712
713        let journal = generator.get_journal("202206").unwrap();
714        assert_eq!(journal.entries.len(), 1);
715        // Unrealized profit = 100000 * 0.05 * 0.20 = 1000
716        assert!(journal.is_balanced);
717    }
718
719    #[test]
720    fn test_generate_dividend_elimination() {
721        let config = EliminationConfig::default();
722        let structure = create_test_ownership_structure();
723        let mut generator = EliminationGenerator::new(config, structure);
724
725        let entry = generator.generate_dividend_elimination(
726            "202206",
727            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
728            "1100",
729            "1000",
730            dec!(50000),
731        );
732
733        assert!(entry.is_balanced());
734        assert_eq!(entry.elimination_type, EliminationType::ICDividends);
735    }
736
737    #[test]
738    fn test_generate_minority_interest_allocation() {
739        let config = EliminationConfig::default();
740        let structure = create_test_ownership_structure();
741        let mut generator = EliminationGenerator::new(config, structure);
742
743        let entry = generator.generate_minority_interest_allocation(
744            "202206",
745            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
746            "1200",
747            dec!(100000),
748            dec!(20), // 20% minority
749        );
750
751        assert!(entry.is_some());
752        let entry = entry.unwrap();
753        assert!(entry.is_balanced());
754        // Minority share = 100000 * 20% = 20000
755        assert_eq!(entry.total_debit, dec!(20000));
756    }
757
758    #[test]
759    fn test_finalize_and_post_journal() {
760        let config = EliminationConfig::default();
761        let structure = create_test_ownership_structure();
762        let mut generator = EliminationGenerator::new(config, structure);
763
764        let balances = vec![ICAggregatedBalance {
765            creditor_company: "1000".to_string(),
766            debtor_company: "1100".to_string(),
767            receivable_account: "1310".to_string(),
768            payable_account: "2110".to_string(),
769            receivable_balance: dec!(50000),
770            payable_balance: dec!(50000),
771            difference: Decimal::ZERO,
772            currency: "USD".to_string(),
773            as_of_date: NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
774            is_matched: true,
775        }];
776
777        generator.generate_ic_balance_eliminations(
778            "202206",
779            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
780            &balances,
781        );
782
783        generator.finalize_journal("202206", "ADMIN".to_string());
784        let journal = generator.get_journal("202206").unwrap();
785        assert_eq!(journal.status, ConsolidationStatus::Approved);
786
787        generator.post_journal("202206");
788        let journal = generator.get_journal("202206").unwrap();
789        assert_eq!(journal.status, ConsolidationStatus::Posted);
790    }
791}