Skip to main content

datasynth_eval/coherence/
intercompany.rs

1//! Intercompany matching evaluation.
2//!
3//! Validates that intercompany transactions are properly matched
4//! between company pairs.
5
6use crate::error::EvalResult;
7use rust_decimal::prelude::ToPrimitive;
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10
11/// Results of intercompany matching evaluation.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ICMatchingEvaluation {
14    /// Total company pairs with IC transactions.
15    pub total_pairs: usize,
16    /// Number of pairs fully matched.
17    pub matched_pairs: usize,
18    /// Match rate (0.0-1.0).
19    pub match_rate: f64,
20    /// Total intercompany receivables.
21    pub total_receivables: Decimal,
22    /// Total intercompany payables.
23    pub total_payables: Decimal,
24    /// Total unmatched amount.
25    pub total_unmatched: Decimal,
26    /// Net position (receivables - payables).
27    pub net_position: Decimal,
28    /// Number of discrepancies.
29    pub discrepancy_count: usize,
30    /// Netting efficiency if applicable.
31    pub netting_efficiency: Option<f64>,
32}
33
34/// Input for IC matching evaluation.
35#[derive(Debug, Clone)]
36pub struct ICMatchingData {
37    /// Total company pairs.
38    pub total_pairs: usize,
39    /// Matched company pairs.
40    pub matched_pairs: usize,
41    /// Total receivables amount.
42    pub total_receivables: Decimal,
43    /// Total payables amount.
44    pub total_payables: Decimal,
45    /// Unmatched items details.
46    pub unmatched_items: Vec<UnmatchedICItem>,
47    /// Gross IC volume (for netting calculation).
48    pub gross_volume: Option<Decimal>,
49    /// Net settlement amount (for netting calculation).
50    pub net_settlement: Option<Decimal>,
51}
52
53/// An unmatched IC item.
54#[derive(Debug, Clone)]
55pub struct UnmatchedICItem {
56    /// Company code.
57    pub company: String,
58    /// Counterparty company code.
59    pub counterparty: String,
60    /// Amount.
61    pub amount: Decimal,
62    /// Whether this is a receivable (true) or payable (false).
63    pub is_receivable: bool,
64}
65
66/// Evaluator for intercompany matching.
67pub struct ICMatchingEvaluator {
68    /// Tolerance for matching.
69    #[allow(dead_code)] // Reserved for tolerance-based matching
70    tolerance: Decimal,
71}
72
73impl ICMatchingEvaluator {
74    /// Create a new evaluator with the specified tolerance.
75    pub fn new(tolerance: Decimal) -> Self {
76        Self { tolerance }
77    }
78
79    /// Evaluate IC matching results.
80    pub fn evaluate(&self, data: &ICMatchingData) -> EvalResult<ICMatchingEvaluation> {
81        let match_rate = if data.total_pairs > 0 {
82            data.matched_pairs as f64 / data.total_pairs as f64
83        } else {
84            1.0
85        };
86
87        let total_unmatched: Decimal = data.unmatched_items.iter().map(|i| i.amount.abs()).sum();
88        let net_position = data.total_receivables - data.total_payables;
89        let discrepancy_count = data.unmatched_items.len();
90
91        // Calculate netting efficiency if data available
92        let netting_efficiency = match (data.gross_volume, data.net_settlement) {
93            (Some(gross), Some(net)) if gross > Decimal::ZERO => {
94                Some(1.0 - (net / gross).to_f64().unwrap_or(0.0))
95            }
96            _ => None,
97        };
98
99        Ok(ICMatchingEvaluation {
100            total_pairs: data.total_pairs,
101            matched_pairs: data.matched_pairs,
102            match_rate,
103            total_receivables: data.total_receivables,
104            total_payables: data.total_payables,
105            total_unmatched,
106            net_position,
107            discrepancy_count,
108            netting_efficiency,
109        })
110    }
111}
112
113impl Default for ICMatchingEvaluator {
114    fn default() -> Self {
115        Self::new(Decimal::new(1, 2)) // 0.01 tolerance
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_fully_matched_ic() {
125        let data = ICMatchingData {
126            total_pairs: 5,
127            matched_pairs: 5,
128            total_receivables: Decimal::new(100000, 2),
129            total_payables: Decimal::new(100000, 2),
130            unmatched_items: vec![],
131            gross_volume: Some(Decimal::new(200000, 2)),
132            net_settlement: Some(Decimal::new(20000, 2)),
133        };
134
135        let evaluator = ICMatchingEvaluator::default();
136        let result = evaluator.evaluate(&data).unwrap();
137
138        assert_eq!(result.match_rate, 1.0);
139        assert_eq!(result.total_unmatched, Decimal::ZERO);
140        assert_eq!(result.net_position, Decimal::ZERO);
141        assert!(result.netting_efficiency.unwrap() > 0.8);
142    }
143
144    #[test]
145    fn test_partial_match() {
146        let data = ICMatchingData {
147            total_pairs: 10,
148            matched_pairs: 8,
149            total_receivables: Decimal::new(100000, 2),
150            total_payables: Decimal::new(95000, 2),
151            unmatched_items: vec![UnmatchedICItem {
152                company: "1000".to_string(),
153                counterparty: "2000".to_string(),
154                amount: Decimal::new(5000, 2),
155                is_receivable: true,
156            }],
157            gross_volume: None,
158            net_settlement: None,
159        };
160
161        let evaluator = ICMatchingEvaluator::default();
162        let result = evaluator.evaluate(&data).unwrap();
163
164        assert_eq!(result.match_rate, 0.8);
165        assert_eq!(result.discrepancy_count, 1);
166        assert_eq!(result.net_position, Decimal::new(5000, 2));
167    }
168
169    #[test]
170    fn test_no_ic_transactions() {
171        let data = ICMatchingData {
172            total_pairs: 0,
173            matched_pairs: 0,
174            total_receivables: Decimal::ZERO,
175            total_payables: Decimal::ZERO,
176            unmatched_items: vec![],
177            gross_volume: None,
178            net_settlement: None,
179        };
180
181        let evaluator = ICMatchingEvaluator::default();
182        let result = evaluator.evaluate(&data).unwrap();
183
184        assert_eq!(result.match_rate, 1.0); // No IC = 100% matched
185    }
186}