Skip to main content

datasynth_eval/coherence/
treasury_tax.rs

1//! Treasury, tax, and payroll coherence validators.
2//!
3//! Validates cross-domain accounting integrity including:
4//! - Interest expense reconciliation (GL vs instrument-level)
5//! - Effective tax rate reconciliation
6//! - Hedge effectiveness corridor compliance
7//! - Payroll/HR salary change traceability
8
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12// ─── InterestExpenseProofEvaluator ────────────────────────────────────────────
13
14/// Input data for interest expense reconciliation.
15#[derive(Debug, Clone)]
16pub struct InterestExpenseProofData {
17    /// Total interest expense posted to the GL.
18    pub total_interest_expense_gl: Decimal,
19    /// Sum of interest computed from individual debt instruments.
20    pub sum_instrument_interest: Decimal,
21}
22
23/// Results of interest expense proof evaluation.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct InterestExpenseProofEvaluation {
26    /// Whether GL interest expense reconciles to instrument-level interest.
27    pub reconciled: bool,
28    /// Absolute difference between GL and instrument totals.
29    pub difference: Decimal,
30    /// Overall pass/fail status.
31    pub passes: bool,
32    /// Human-readable descriptions of failed checks.
33    pub failures: Vec<String>,
34}
35
36/// Evaluator that verifies GL interest expense equals the sum of instrument-level interest.
37pub struct InterestExpenseProofEvaluator {
38    tolerance: Decimal,
39}
40
41impl InterestExpenseProofEvaluator {
42    /// Create a new evaluator with the given absolute tolerance.
43    pub fn new(tolerance: Decimal) -> Self {
44        Self { tolerance }
45    }
46
47    /// Run the interest expense proof check against `data`.
48    pub fn evaluate(&self, data: &InterestExpenseProofData) -> InterestExpenseProofEvaluation {
49        let difference = (data.total_interest_expense_gl - data.sum_instrument_interest).abs();
50        let reconciled = difference <= self.tolerance;
51        let mut failures = Vec::new();
52        if !reconciled {
53            failures.push(format!(
54                "Interest expense GL {} vs instruments {} (diff {})",
55                data.total_interest_expense_gl, data.sum_instrument_interest, difference
56            ));
57        }
58        InterestExpenseProofEvaluation {
59            reconciled,
60            difference,
61            passes: reconciled,
62            failures,
63        }
64    }
65}
66
67impl Default for InterestExpenseProofEvaluator {
68    fn default() -> Self {
69        Self::new(Decimal::new(1, 2)) // 0.01 tolerance
70    }
71}
72
73// ─── ETRReconciliationEvaluator ───────────────────────────────────────────────
74
75/// Input data for effective tax rate reconciliation.
76#[derive(Debug, Clone)]
77pub struct ETRReconciliationData {
78    /// Pre-tax income (PTI) for the period.
79    pub pre_tax_income: Decimal,
80    /// Statutory tax rate as a decimal fraction (e.g. 0.21 for 21%).
81    pub statutory_rate: Decimal,
82    /// Actual income tax expense recognised in the P&L.
83    pub actual_tax_expense: Decimal,
84    /// Sum of all ETR reconciling items (permanent differences, credits, etc.).
85    pub sum_reconciling_items: Decimal,
86}
87
88/// Results of ETR reconciliation evaluation.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ETRReconciliationEvaluation {
91    /// Whether expected tax reconciles to actual tax expense.
92    pub reconciled: bool,
93    /// Expected tax expense: PTI × statutory_rate + reconciling items.
94    pub expected_tax: Decimal,
95    /// Absolute difference between expected and actual tax expense.
96    pub difference: Decimal,
97    /// Overall pass/fail status.
98    pub passes: bool,
99    /// Human-readable descriptions of failed checks.
100    pub failures: Vec<String>,
101}
102
103/// Evaluator that verifies statutory rate × PTI + reconciling items ≈ actual tax expense.
104pub struct ETRReconciliationEvaluator {
105    tolerance: Decimal,
106}
107
108impl ETRReconciliationEvaluator {
109    /// Create a new evaluator with the given absolute tolerance.
110    pub fn new(tolerance: Decimal) -> Self {
111        Self { tolerance }
112    }
113
114    /// Run the ETR reconciliation check against `data`.
115    pub fn evaluate(&self, data: &ETRReconciliationData) -> ETRReconciliationEvaluation {
116        let expected_tax = data.pre_tax_income * data.statutory_rate + data.sum_reconciling_items;
117        let difference = (expected_tax - data.actual_tax_expense).abs();
118        let reconciled = difference <= self.tolerance;
119        let mut failures = Vec::new();
120        if !reconciled {
121            failures.push(format!(
122                "ETR reconciliation failed: expected tax {} vs actual {} (diff {})",
123                expected_tax, data.actual_tax_expense, difference
124            ));
125        }
126        ETRReconciliationEvaluation {
127            reconciled,
128            expected_tax,
129            difference,
130            passes: reconciled,
131            failures,
132        }
133    }
134}
135
136impl Default for ETRReconciliationEvaluator {
137    fn default() -> Self {
138        Self::new(Decimal::new(1, 2)) // 0.01 tolerance
139    }
140}
141
142// ─── HedgeEffectivenessEvaluator ──────────────────────────────────────────────
143
144/// Input data for hedge effectiveness checks.
145#[derive(Debug, Clone)]
146pub struct HedgeEffectivenessData {
147    /// Total number of hedge relationships in the population.
148    pub total_hedges: usize,
149    /// Number of hedges that remain within the 80–125% effectiveness corridor.
150    pub effective_hedges: usize,
151    /// Number of hedges that have been discontinued.
152    pub discontinued_hedges: usize,
153    /// Number of discontinued hedges that have a P&L reclassification entry.
154    pub discontinued_with_pl_entries: usize,
155}
156
157/// Results of hedge effectiveness evaluation.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct HedgeEffectivenessEvaluation {
160    /// Fraction of total hedges classified as effective (0.0–1.0).
161    pub effectiveness_rate: f64,
162    /// Whether every discontinued hedge has a corresponding P&L reclassification entry.
163    pub all_discontinued_have_pl: bool,
164    /// Overall pass/fail status.
165    pub passes: bool,
166    /// Human-readable descriptions of failed checks.
167    pub failures: Vec<String>,
168}
169
170/// Evaluator for hedge effectiveness corridor compliance and discontinuation accounting.
171pub struct HedgeEffectivenessEvaluator;
172
173impl HedgeEffectivenessEvaluator {
174    /// Run hedge effectiveness checks against `data`.
175    pub fn evaluate(&self, data: &HedgeEffectivenessData) -> HedgeEffectivenessEvaluation {
176        let effectiveness_rate = if data.total_hedges == 0 {
177            1.0
178        } else {
179            data.effective_hedges as f64 / data.total_hedges as f64
180        };
181
182        let all_discontinued_have_pl =
183            data.discontinued_with_pl_entries >= data.discontinued_hedges;
184        let mut failures = Vec::new();
185        if !all_discontinued_have_pl {
186            failures.push(format!(
187                "Hedge discontinuation incomplete: {}/{} discontinued hedges have P&L reclassification entries",
188                data.discontinued_with_pl_entries, data.discontinued_hedges
189            ));
190        }
191
192        let passes = failures.is_empty();
193        HedgeEffectivenessEvaluation {
194            effectiveness_rate,
195            all_discontinued_have_pl,
196            passes,
197            failures,
198        }
199    }
200}
201
202// ─── PayrollHRReconciliationEvaluator ─────────────────────────────────────────
203
204/// Input data for payroll/HR reconciliation.
205#[derive(Debug, Clone)]
206pub struct PayrollHRReconciliationData {
207    /// Number of salary change events recorded in the HR master data.
208    pub salary_change_count: usize,
209    /// Number of payroll variance entries that can be traced to those changes.
210    pub payroll_variance_count: usize,
211}
212
213/// Results of payroll/HR reconciliation evaluation.
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct PayrollHRReconciliationEvaluation {
216    /// Whether every salary change has a corresponding payroll variance.
217    pub changes_traced: bool,
218    /// Overall pass/fail status.
219    pub passes: bool,
220    /// Human-readable descriptions of failed checks.
221    pub failures: Vec<String>,
222}
223
224/// Evaluator that verifies salary changes trace to payroll variances.
225pub struct PayrollHRReconciliationEvaluator;
226
227impl PayrollHRReconciliationEvaluator {
228    /// Run the payroll/HR reconciliation check against `data`.
229    pub fn evaluate(
230        &self,
231        data: &PayrollHRReconciliationData,
232    ) -> PayrollHRReconciliationEvaluation {
233        let changes_traced = data.payroll_variance_count >= data.salary_change_count;
234        let mut failures = Vec::new();
235        if !changes_traced {
236            failures.push(format!(
237                "Payroll/HR reconciliation failed: {} salary changes but only {} payroll variance entries",
238                data.salary_change_count, data.payroll_variance_count
239            ));
240        }
241        PayrollHRReconciliationEvaluation {
242            changes_traced,
243            passes: changes_traced,
244            failures,
245        }
246    }
247}
248
249// ─── Unit tests ──────────────────────────────────────────────────────────────
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use rust_decimal_macros::dec;
255
256    #[test]
257    fn test_interest_expense_proof_reconciled() {
258        let data = InterestExpenseProofData {
259            total_interest_expense_gl: dec!(50_000),
260            sum_instrument_interest: dec!(50_000),
261        };
262        let result = InterestExpenseProofEvaluator::new(dec!(100)).evaluate(&data);
263        assert!(result.passes);
264        assert!(result.reconciled);
265        assert!(result.failures.is_empty());
266    }
267
268    #[test]
269    fn test_interest_expense_proof_unreconciled() {
270        let data = InterestExpenseProofData {
271            total_interest_expense_gl: dec!(50_000),
272            sum_instrument_interest: dec!(30_000),
273        };
274        let result = InterestExpenseProofEvaluator::new(dec!(100)).evaluate(&data);
275        assert!(!result.passes);
276        assert!(!result.failures.is_empty());
277    }
278
279    #[test]
280    fn test_etr_reconciliation_reconciled() {
281        // PTI=1_000_000, rate=21%, reconciling=+20_000 → expected=230_000
282        let data = ETRReconciliationData {
283            pre_tax_income: dec!(1_000_000),
284            statutory_rate: dec!(0.21),
285            actual_tax_expense: dec!(230_000),
286            sum_reconciling_items: dec!(20_000),
287        };
288        let result = ETRReconciliationEvaluator::new(dec!(1_000)).evaluate(&data);
289        assert!(result.passes);
290        assert_eq!(result.expected_tax, dec!(230_000));
291    }
292
293    #[test]
294    fn test_etr_reconciliation_unreconciled() {
295        let data = ETRReconciliationData {
296            pre_tax_income: dec!(1_000_000),
297            statutory_rate: dec!(0.21),
298            actual_tax_expense: dec!(999_000), // Way off
299            sum_reconciling_items: dec!(0),
300        };
301        let result = ETRReconciliationEvaluator::new(dec!(1_000)).evaluate(&data);
302        assert!(!result.passes);
303        assert!(!result.failures.is_empty());
304    }
305
306    #[test]
307    fn test_hedge_effectiveness_all_compliant() {
308        let data = HedgeEffectivenessData {
309            total_hedges: 10,
310            effective_hedges: 9,
311            discontinued_hedges: 1,
312            discontinued_with_pl_entries: 1,
313        };
314        let result = HedgeEffectivenessEvaluator.evaluate(&data);
315        assert!(result.passes);
316        assert!(result.all_discontinued_have_pl);
317        assert!((result.effectiveness_rate - 0.9).abs() < f64::EPSILON);
318    }
319
320    #[test]
321    fn test_hedge_effectiveness_missing_pl_entry() {
322        let data = HedgeEffectivenessData {
323            total_hedges: 10,
324            effective_hedges: 8,
325            discontinued_hedges: 2,
326            discontinued_with_pl_entries: 1, // Missing one
327        };
328        let result = HedgeEffectivenessEvaluator.evaluate(&data);
329        assert!(!result.passes);
330        assert!(!result.all_discontinued_have_pl);
331    }
332
333    #[test]
334    fn test_payroll_hr_changes_traced() {
335        let data = PayrollHRReconciliationData {
336            salary_change_count: 5,
337            payroll_variance_count: 5,
338        };
339        let result = PayrollHRReconciliationEvaluator.evaluate(&data);
340        assert!(result.passes);
341        assert!(result.changes_traced);
342    }
343
344    #[test]
345    fn test_payroll_hr_changes_missing_variances() {
346        let data = PayrollHRReconciliationData {
347            salary_change_count: 5,
348            payroll_variance_count: 3,
349        };
350        let result = PayrollHRReconciliationEvaluator.evaluate(&data);
351        assert!(!result.passes);
352        assert!(!result.changes_traced);
353        assert!(!result.failures.is_empty());
354    }
355}