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)]
252#[allow(clippy::unwrap_used)]
253mod tests {
254    use super::*;
255    use rust_decimal_macros::dec;
256
257    #[test]
258    fn test_interest_expense_proof_reconciled() {
259        let data = InterestExpenseProofData {
260            total_interest_expense_gl: dec!(50_000),
261            sum_instrument_interest: dec!(50_000),
262        };
263        let result = InterestExpenseProofEvaluator::new(dec!(100)).evaluate(&data);
264        assert!(result.passes);
265        assert!(result.reconciled);
266        assert!(result.failures.is_empty());
267    }
268
269    #[test]
270    fn test_interest_expense_proof_unreconciled() {
271        let data = InterestExpenseProofData {
272            total_interest_expense_gl: dec!(50_000),
273            sum_instrument_interest: dec!(30_000),
274        };
275        let result = InterestExpenseProofEvaluator::new(dec!(100)).evaluate(&data);
276        assert!(!result.passes);
277        assert!(!result.failures.is_empty());
278    }
279
280    #[test]
281    fn test_etr_reconciliation_reconciled() {
282        // PTI=1_000_000, rate=21%, reconciling=+20_000 → expected=230_000
283        let data = ETRReconciliationData {
284            pre_tax_income: dec!(1_000_000),
285            statutory_rate: dec!(0.21),
286            actual_tax_expense: dec!(230_000),
287            sum_reconciling_items: dec!(20_000),
288        };
289        let result = ETRReconciliationEvaluator::new(dec!(1_000)).evaluate(&data);
290        assert!(result.passes);
291        assert_eq!(result.expected_tax, dec!(230_000));
292    }
293
294    #[test]
295    fn test_etr_reconciliation_unreconciled() {
296        let data = ETRReconciliationData {
297            pre_tax_income: dec!(1_000_000),
298            statutory_rate: dec!(0.21),
299            actual_tax_expense: dec!(999_000), // Way off
300            sum_reconciling_items: dec!(0),
301        };
302        let result = ETRReconciliationEvaluator::new(dec!(1_000)).evaluate(&data);
303        assert!(!result.passes);
304        assert!(!result.failures.is_empty());
305    }
306
307    #[test]
308    fn test_hedge_effectiveness_all_compliant() {
309        let data = HedgeEffectivenessData {
310            total_hedges: 10,
311            effective_hedges: 9,
312            discontinued_hedges: 1,
313            discontinued_with_pl_entries: 1,
314        };
315        let result = HedgeEffectivenessEvaluator.evaluate(&data);
316        assert!(result.passes);
317        assert!(result.all_discontinued_have_pl);
318        assert!((result.effectiveness_rate - 0.9).abs() < f64::EPSILON);
319    }
320
321    #[test]
322    fn test_hedge_effectiveness_missing_pl_entry() {
323        let data = HedgeEffectivenessData {
324            total_hedges: 10,
325            effective_hedges: 8,
326            discontinued_hedges: 2,
327            discontinued_with_pl_entries: 1, // Missing one
328        };
329        let result = HedgeEffectivenessEvaluator.evaluate(&data);
330        assert!(!result.passes);
331        assert!(!result.all_discontinued_have_pl);
332    }
333
334    #[test]
335    fn test_payroll_hr_changes_traced() {
336        let data = PayrollHRReconciliationData {
337            salary_change_count: 5,
338            payroll_variance_count: 5,
339        };
340        let result = PayrollHRReconciliationEvaluator.evaluate(&data);
341        assert!(result.passes);
342        assert!(result.changes_traced);
343    }
344
345    #[test]
346    fn test_payroll_hr_changes_missing_variances() {
347        let data = PayrollHRReconciliationData {
348            salary_change_count: 5,
349            payroll_variance_count: 3,
350        };
351        let result = PayrollHRReconciliationEvaluator.evaluate(&data);
352        assert!(!result.passes);
353        assert!(!result.changes_traced);
354        assert!(!result.failures.is_empty());
355    }
356}