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.journals.get(fiscal_period).unwrap()
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 = format!(
398            "Eliminate IC dividend from {} to {}",
399            paying_company, receiving_company
400        );
401
402        // Debit dividend income (reduce income)
403        entry.add_line(datasynth_core::models::intercompany::EliminationLine {
404            line_number: 1,
405            company: receiving_company.to_string(),
406            account: "4600".to_string(), // Dividend income
407            is_debit: true,
408            amount: dividend_amount,
409            currency: self.config.base_currency.clone(),
410            description: "Eliminate dividend income".to_string(),
411        });
412
413        // Credit retained earnings (restore to subsidiary)
414        entry.add_line(datasynth_core::models::intercompany::EliminationLine {
415            line_number: 2,
416            company: paying_company.to_string(),
417            account: "3300".to_string(), // Retained earnings
418            is_debit: false,
419            amount: dividend_amount,
420            currency: self.config.base_currency.clone(),
421            description: "Restore retained earnings".to_string(),
422        });
423
424        let journal = self.get_or_create_journal(fiscal_period, entry_date);
425        journal.add_entry(entry.clone());
426
427        entry
428    }
429
430    /// Generate minority interest allocation for period profit/loss.
431    pub fn generate_minority_interest_allocation(
432        &mut self,
433        fiscal_period: &str,
434        entry_date: NaiveDate,
435        subsidiary: &str,
436        net_income: Decimal,
437        minority_percentage: Decimal,
438    ) -> Option<EliminationEntry> {
439        if minority_percentage <= Decimal::ZERO || minority_percentage >= dec!(100) {
440            return None;
441        }
442
443        let minority_share = net_income * minority_percentage / dec!(100);
444
445        if minority_share.abs() < dec!(0.01) {
446            return None;
447        }
448
449        let mut entry = EliminationEntry::new(
450            self.generate_entry_id(EliminationType::MinorityInterest),
451            EliminationType::MinorityInterest,
452            self.config.consolidation_entity.clone(),
453            fiscal_period.to_string(),
454            entry_date,
455            self.config.base_currency.clone(),
456        );
457
458        entry.related_companies = vec![subsidiary.to_string()];
459        entry.description = format!("Minority interest share of {} profit/loss", subsidiary);
460
461        if net_income > Decimal::ZERO {
462            // Profit: DR consolidated income, CR NCI
463            entry.add_line(datasynth_core::models::intercompany::EliminationLine {
464                line_number: 1,
465                company: self.config.consolidation_entity.clone(),
466                account: "3400".to_string(), // NCI share of income
467                is_debit: true,
468                amount: minority_share,
469                currency: self.config.base_currency.clone(),
470                description: "NCI share of net income".to_string(),
471            });
472
473            entry.add_line(datasynth_core::models::intercompany::EliminationLine {
474                line_number: 2,
475                company: self.config.consolidation_entity.clone(),
476                account: "3500".to_string(), // Non-controlling interest
477                is_debit: false,
478                amount: minority_share,
479                currency: self.config.base_currency.clone(),
480                description: "Increase NCI for share of income".to_string(),
481            });
482        } else {
483            // Loss: DR NCI, CR consolidated loss
484            entry.add_line(datasynth_core::models::intercompany::EliminationLine {
485                line_number: 1,
486                company: self.config.consolidation_entity.clone(),
487                account: "3500".to_string(), // Non-controlling interest
488                is_debit: true,
489                amount: minority_share.abs(),
490                currency: self.config.base_currency.clone(),
491                description: "Decrease NCI for share of loss".to_string(),
492            });
493
494            entry.add_line(datasynth_core::models::intercompany::EliminationLine {
495                line_number: 2,
496                company: self.config.consolidation_entity.clone(),
497                account: "3400".to_string(), // NCI share of income
498                is_debit: false,
499                amount: minority_share.abs(),
500                currency: self.config.base_currency.clone(),
501                description: "NCI share of net loss".to_string(),
502            });
503        }
504
505        let journal = self.get_or_create_journal(fiscal_period, entry_date);
506        journal.add_entry(entry.clone());
507
508        Some(entry)
509    }
510
511    /// Get consolidation journal for a period.
512    pub fn get_journal(&self, fiscal_period: &str) -> Option<&ConsolidationJournal> {
513        self.journals.get(fiscal_period)
514    }
515
516    /// Get all journals.
517    pub fn get_all_journals(&self) -> &HashMap<String, ConsolidationJournal> {
518        &self.journals
519    }
520
521    /// Finalize and approve a journal.
522    pub fn finalize_journal(
523        &mut self,
524        fiscal_period: &str,
525        approved_by: String,
526    ) -> Option<&ConsolidationJournal> {
527        if let Some(journal) = self.journals.get_mut(fiscal_period) {
528            journal.submit();
529            journal.approve(approved_by);
530            Some(journal)
531        } else {
532            None
533        }
534    }
535
536    /// Post a journal.
537    pub fn post_journal(&mut self, fiscal_period: &str) -> Option<&ConsolidationJournal> {
538        if let Some(journal) = self.journals.get_mut(fiscal_period) {
539            journal.post();
540            Some(journal)
541        } else {
542            None
543        }
544    }
545
546    /// Get elimination summary for a period.
547    pub fn get_summary(&self, fiscal_period: &str) -> Option<EliminationSummaryReport> {
548        self.journals.get(fiscal_period).map(|journal| {
549            let mut by_type: HashMap<EliminationType, (usize, Decimal)> = HashMap::new();
550
551            for entry in &journal.entries {
552                let stats = by_type
553                    .entry(entry.elimination_type)
554                    .or_insert((0, Decimal::ZERO));
555                stats.0 += 1;
556                stats.1 += entry.total_debit;
557            }
558
559            EliminationSummaryReport {
560                fiscal_period: fiscal_period.to_string(),
561                consolidation_entity: journal.consolidation_entity.clone(),
562                total_entries: journal.entries.len(),
563                total_debit: journal.total_debits,
564                total_credit: journal.total_credits,
565                is_balanced: journal.is_balanced,
566                status: journal.status,
567                by_type,
568            }
569        })
570    }
571
572    /// Reset counters and clear journals.
573    pub fn reset(&mut self) {
574        self.entry_counter = 0;
575        self.journals.clear();
576    }
577}
578
579/// Summary report for elimination entries.
580#[derive(Debug, Clone)]
581pub struct EliminationSummaryReport {
582    /// Fiscal period.
583    pub fiscal_period: String,
584    /// Consolidation entity.
585    pub consolidation_entity: String,
586    /// Total number of entries.
587    pub total_entries: usize,
588    /// Total debit amount.
589    pub total_debit: Decimal,
590    /// Total credit amount.
591    pub total_credit: Decimal,
592    /// Is the journal balanced?
593    pub is_balanced: bool,
594    /// Journal status.
595    pub status: ConsolidationStatus,
596    /// Breakdown by elimination type (count, amount).
597    pub by_type: HashMap<EliminationType, (usize, Decimal)>,
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603    use chrono::NaiveDate;
604    use datasynth_core::models::intercompany::IntercompanyRelationship;
605    use rust_decimal_macros::dec;
606
607    fn create_test_ownership_structure() -> OwnershipStructure {
608        let mut structure = OwnershipStructure::new("1000".to_string());
609        structure.add_relationship(IntercompanyRelationship::new(
610            "REL001".to_string(),
611            "1000".to_string(),
612            "1100".to_string(),
613            dec!(100),
614            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
615        ));
616        structure.add_relationship(IntercompanyRelationship::new(
617            "REL002".to_string(),
618            "1000".to_string(),
619            "1200".to_string(),
620            dec!(80),
621            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
622        ));
623        structure
624    }
625
626    #[test]
627    fn test_elimination_generator_creation() {
628        let config = EliminationConfig::default();
629        let structure = create_test_ownership_structure();
630        let generator = EliminationGenerator::new(config, structure);
631
632        assert!(generator.journals.is_empty());
633    }
634
635    #[test]
636    fn test_generate_ic_balance_eliminations() {
637        let config = EliminationConfig::default();
638        let structure = create_test_ownership_structure();
639        let mut generator = EliminationGenerator::new(config, structure);
640
641        let balances = vec![ICAggregatedBalance {
642            creditor_company: "1000".to_string(),
643            debtor_company: "1100".to_string(),
644            receivable_account: "1310".to_string(),
645            payable_account: "2110".to_string(),
646            receivable_balance: dec!(50000),
647            payable_balance: dec!(50000),
648            difference: Decimal::ZERO,
649            currency: "USD".to_string(),
650            as_of_date: NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
651            is_matched: true,
652        }];
653
654        generator.generate_ic_balance_eliminations(
655            "202206",
656            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
657            &balances,
658        );
659
660        let journal = generator.get_journal("202206").unwrap();
661        assert_eq!(journal.entries.len(), 1);
662        assert!(journal.is_balanced);
663    }
664
665    #[test]
666    fn test_generate_ic_revenue_expense_eliminations() {
667        let config = EliminationConfig::default();
668        let structure = create_test_ownership_structure();
669        let mut generator = EliminationGenerator::new(config, structure);
670
671        let transactions = vec![ICMatchedPair::new(
672            "IC001".to_string(),
673            ICTransactionType::ServiceProvided,
674            "1000".to_string(),
675            "1100".to_string(),
676            dec!(25000),
677            "USD".to_string(),
678            NaiveDate::from_ymd_opt(2022, 6, 15).unwrap(),
679        )];
680
681        generator.generate_ic_revenue_expense_eliminations(
682            "202206",
683            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
684            &transactions,
685        );
686
687        let journal = generator.get_journal("202206").unwrap();
688        assert_eq!(journal.entries.len(), 1);
689        assert!(journal.is_balanced);
690    }
691
692    #[test]
693    fn test_generate_unrealized_profit_eliminations() {
694        let config = EliminationConfig::default();
695        let structure = create_test_ownership_structure();
696        let mut generator = EliminationGenerator::new(config, structure);
697
698        let transactions = vec![ICMatchedPair::new(
699            "IC001".to_string(),
700            ICTransactionType::GoodsSale,
701            "1000".to_string(),
702            "1100".to_string(),
703            dec!(100000),
704            "USD".to_string(),
705            NaiveDate::from_ymd_opt(2022, 6, 15).unwrap(),
706        )];
707
708        generator.generate_unrealized_profit_eliminations(
709            "202206",
710            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
711            &transactions,
712        );
713
714        let journal = generator.get_journal("202206").unwrap();
715        assert_eq!(journal.entries.len(), 1);
716        // Unrealized profit = 100000 * 0.05 * 0.20 = 1000
717        assert!(journal.is_balanced);
718    }
719
720    #[test]
721    fn test_generate_dividend_elimination() {
722        let config = EliminationConfig::default();
723        let structure = create_test_ownership_structure();
724        let mut generator = EliminationGenerator::new(config, structure);
725
726        let entry = generator.generate_dividend_elimination(
727            "202206",
728            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
729            "1100",
730            "1000",
731            dec!(50000),
732        );
733
734        assert!(entry.is_balanced());
735        assert_eq!(entry.elimination_type, EliminationType::ICDividends);
736    }
737
738    #[test]
739    fn test_generate_minority_interest_allocation() {
740        let config = EliminationConfig::default();
741        let structure = create_test_ownership_structure();
742        let mut generator = EliminationGenerator::new(config, structure);
743
744        let entry = generator.generate_minority_interest_allocation(
745            "202206",
746            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
747            "1200",
748            dec!(100000),
749            dec!(20), // 20% minority
750        );
751
752        assert!(entry.is_some());
753        let entry = entry.unwrap();
754        assert!(entry.is_balanced());
755        // Minority share = 100000 * 20% = 20000
756        assert_eq!(entry.total_debit, dec!(20000));
757    }
758
759    #[test]
760    fn test_finalize_and_post_journal() {
761        let config = EliminationConfig::default();
762        let structure = create_test_ownership_structure();
763        let mut generator = EliminationGenerator::new(config, structure);
764
765        let balances = vec![ICAggregatedBalance {
766            creditor_company: "1000".to_string(),
767            debtor_company: "1100".to_string(),
768            receivable_account: "1310".to_string(),
769            payable_account: "2110".to_string(),
770            receivable_balance: dec!(50000),
771            payable_balance: dec!(50000),
772            difference: Decimal::ZERO,
773            currency: "USD".to_string(),
774            as_of_date: NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
775            is_matched: true,
776        }];
777
778        generator.generate_ic_balance_eliminations(
779            "202206",
780            NaiveDate::from_ymd_opt(2022, 6, 30).unwrap(),
781            &balances,
782        );
783
784        generator.finalize_journal("202206", "ADMIN".to_string());
785        let journal = generator.get_journal("202206").unwrap();
786        assert_eq!(journal.status, ConsolidationStatus::Approved);
787
788        generator.post_journal("202206");
789        let journal = generator.get_journal("202206").unwrap();
790        assert_eq!(journal.status, ConsolidationStatus::Posted);
791    }
792}