Skip to main content

datasynth_eval/coherence/
subledger.rs

1//! Subledger-to-GL reconciliation evaluation.
2//!
3//! Validates that subledger balances (AR, AP, FA, Inventory) reconcile
4//! to their corresponding GL control accounts.
5
6use crate::error::EvalResult;
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9
10/// Results of subledger reconciliation evaluation.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SubledgerReconciliationEvaluation {
13    /// AR reconciliation status.
14    pub ar_reconciled: bool,
15    /// AR GL balance.
16    pub ar_gl_balance: Decimal,
17    /// AR subledger balance.
18    pub ar_subledger_balance: Decimal,
19    /// AR difference.
20    pub ar_difference: Decimal,
21    /// AP reconciliation status.
22    pub ap_reconciled: bool,
23    /// AP GL balance.
24    pub ap_gl_balance: Decimal,
25    /// AP subledger balance.
26    pub ap_subledger_balance: Decimal,
27    /// AP difference.
28    pub ap_difference: Decimal,
29    /// FA asset reconciliation status.
30    pub fa_asset_reconciled: bool,
31    /// FA asset GL balance.
32    pub fa_asset_gl_balance: Decimal,
33    /// FA asset subledger balance.
34    pub fa_asset_subledger_balance: Decimal,
35    /// FA asset difference.
36    pub fa_asset_difference: Decimal,
37    /// FA accumulated depreciation reconciliation status.
38    pub fa_accum_depr_reconciled: bool,
39    /// FA accumulated depreciation GL balance.
40    pub fa_accum_depr_gl_balance: Decimal,
41    /// FA accumulated depreciation subledger balance.
42    pub fa_accum_depr_subledger_balance: Decimal,
43    /// FA accumulated depreciation difference.
44    pub fa_accum_depr_difference: Decimal,
45    /// Inventory reconciliation status.
46    pub inventory_reconciled: bool,
47    /// Inventory GL balance.
48    pub inventory_gl_balance: Decimal,
49    /// Inventory subledger balance.
50    pub inventory_subledger_balance: Decimal,
51    /// Inventory difference.
52    pub inventory_difference: Decimal,
53    /// Overall completeness score (0.0-1.0).
54    pub completeness_score: f64,
55    /// Number of subledgers reconciled.
56    pub subledgers_reconciled: usize,
57    /// Total subledgers checked.
58    pub subledgers_total: usize,
59}
60
61/// Input for subledger reconciliation.
62#[derive(Debug, Clone, Default)]
63pub struct SubledgerData {
64    /// AR GL control account balance.
65    pub ar_gl_balance: Option<Decimal>,
66    /// Sum of AR invoice balances.
67    pub ar_subledger_balance: Option<Decimal>,
68    /// AP GL control account balance.
69    pub ap_gl_balance: Option<Decimal>,
70    /// Sum of AP invoice balances.
71    pub ap_subledger_balance: Option<Decimal>,
72    /// FA asset GL control account balance.
73    pub fa_asset_gl_balance: Option<Decimal>,
74    /// Sum of FA asset values.
75    pub fa_asset_subledger_balance: Option<Decimal>,
76    /// FA accumulated depreciation GL balance.
77    pub fa_accum_depr_gl_balance: Option<Decimal>,
78    /// Sum of FA accumulated depreciation.
79    pub fa_accum_depr_subledger_balance: Option<Decimal>,
80    /// Inventory GL control account balance.
81    pub inventory_gl_balance: Option<Decimal>,
82    /// Sum of inventory position values.
83    pub inventory_subledger_balance: Option<Decimal>,
84}
85
86/// Evaluator for subledger reconciliation.
87pub struct SubledgerEvaluator {
88    /// Tolerance for reconciliation differences.
89    tolerance: Decimal,
90}
91
92impl SubledgerEvaluator {
93    /// Create a new evaluator with the specified tolerance.
94    pub fn new(tolerance: Decimal) -> Self {
95        Self { tolerance }
96    }
97
98    /// Evaluate subledger reconciliation.
99    pub fn evaluate(&self, data: &SubledgerData) -> EvalResult<SubledgerReconciliationEvaluation> {
100        let mut subledgers_reconciled = 0;
101        let mut subledgers_total = 0;
102
103        // AR reconciliation
104        let (ar_reconciled, ar_gl, ar_sub, ar_diff) = self.check_reconciliation(
105            data.ar_gl_balance,
106            data.ar_subledger_balance,
107            &mut subledgers_reconciled,
108            &mut subledgers_total,
109        );
110
111        // AP reconciliation
112        let (ap_reconciled, ap_gl, ap_sub, ap_diff) = self.check_reconciliation(
113            data.ap_gl_balance,
114            data.ap_subledger_balance,
115            &mut subledgers_reconciled,
116            &mut subledgers_total,
117        );
118
119        // FA asset reconciliation
120        let (fa_asset_reconciled, fa_asset_gl, fa_asset_sub, fa_asset_diff) = self
121            .check_reconciliation(
122                data.fa_asset_gl_balance,
123                data.fa_asset_subledger_balance,
124                &mut subledgers_reconciled,
125                &mut subledgers_total,
126            );
127
128        // FA accumulated depreciation reconciliation
129        let (fa_accum_reconciled, fa_accum_gl, fa_accum_sub, fa_accum_diff) = self
130            .check_reconciliation(
131                data.fa_accum_depr_gl_balance,
132                data.fa_accum_depr_subledger_balance,
133                &mut subledgers_reconciled,
134                &mut subledgers_total,
135            );
136
137        // Inventory reconciliation
138        let (inv_reconciled, inv_gl, inv_sub, inv_diff) = self.check_reconciliation(
139            data.inventory_gl_balance,
140            data.inventory_subledger_balance,
141            &mut subledgers_reconciled,
142            &mut subledgers_total,
143        );
144
145        let completeness_score = if subledgers_total > 0 {
146            subledgers_reconciled as f64 / subledgers_total as f64
147        } else {
148            1.0 // No subledgers to reconcile = 100% complete
149        };
150
151        Ok(SubledgerReconciliationEvaluation {
152            ar_reconciled,
153            ar_gl_balance: ar_gl,
154            ar_subledger_balance: ar_sub,
155            ar_difference: ar_diff,
156            ap_reconciled,
157            ap_gl_balance: ap_gl,
158            ap_subledger_balance: ap_sub,
159            ap_difference: ap_diff,
160            fa_asset_reconciled,
161            fa_asset_gl_balance: fa_asset_gl,
162            fa_asset_subledger_balance: fa_asset_sub,
163            fa_asset_difference: fa_asset_diff,
164            fa_accum_depr_reconciled: fa_accum_reconciled,
165            fa_accum_depr_gl_balance: fa_accum_gl,
166            fa_accum_depr_subledger_balance: fa_accum_sub,
167            fa_accum_depr_difference: fa_accum_diff,
168            inventory_reconciled: inv_reconciled,
169            inventory_gl_balance: inv_gl,
170            inventory_subledger_balance: inv_sub,
171            inventory_difference: inv_diff,
172            completeness_score,
173            subledgers_reconciled,
174            subledgers_total,
175        })
176    }
177
178    /// Check reconciliation for a single subledger.
179    fn check_reconciliation(
180        &self,
181        gl_balance: Option<Decimal>,
182        subledger_balance: Option<Decimal>,
183        reconciled_count: &mut usize,
184        total_count: &mut usize,
185    ) -> (bool, Decimal, Decimal, Decimal) {
186        match (gl_balance, subledger_balance) {
187            (Some(gl), Some(sub)) => {
188                *total_count += 1;
189                let diff = gl - sub;
190                let is_reconciled = diff.abs() <= self.tolerance;
191                if is_reconciled {
192                    *reconciled_count += 1;
193                }
194                (is_reconciled, gl, sub, diff)
195            }
196            _ => (true, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
197        }
198    }
199}
200
201impl Default for SubledgerEvaluator {
202    fn default() -> Self {
203        Self::new(Decimal::new(1, 2)) // 0.01 tolerance
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_reconciled_subledgers() {
213        let data = SubledgerData {
214            ar_gl_balance: Some(Decimal::new(100000, 2)),
215            ar_subledger_balance: Some(Decimal::new(100000, 2)),
216            ap_gl_balance: Some(Decimal::new(50000, 2)),
217            ap_subledger_balance: Some(Decimal::new(50000, 2)),
218            ..Default::default()
219        };
220
221        let evaluator = SubledgerEvaluator::default();
222        let result = evaluator.evaluate(&data).unwrap();
223
224        assert!(result.ar_reconciled);
225        assert!(result.ap_reconciled);
226        assert_eq!(result.completeness_score, 1.0);
227    }
228
229    #[test]
230    fn test_unreconciled_subledger() {
231        let data = SubledgerData {
232            ar_gl_balance: Some(Decimal::new(100000, 2)),
233            ar_subledger_balance: Some(Decimal::new(99000, 2)), // 10.00 difference
234            ..Default::default()
235        };
236
237        let evaluator = SubledgerEvaluator::default();
238        let result = evaluator.evaluate(&data).unwrap();
239
240        assert!(!result.ar_reconciled);
241        assert_eq!(result.ar_difference, Decimal::new(1000, 2));
242    }
243
244    #[test]
245    fn test_no_subledger_data() {
246        let data = SubledgerData::default();
247        let evaluator = SubledgerEvaluator::default();
248        let result = evaluator.evaluate(&data).unwrap();
249
250        // With no data, should be considered complete
251        assert_eq!(result.completeness_score, 1.0);
252    }
253}