datasynth_eval/coherence/
treasury_tax.rs1use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone)]
16pub struct InterestExpenseProofData {
17 pub total_interest_expense_gl: Decimal,
19 pub sum_instrument_interest: Decimal,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct InterestExpenseProofEvaluation {
26 pub reconciled: bool,
28 pub difference: Decimal,
30 pub passes: bool,
32 pub failures: Vec<String>,
34}
35
36pub struct InterestExpenseProofEvaluator {
38 tolerance: Decimal,
39}
40
41impl InterestExpenseProofEvaluator {
42 pub fn new(tolerance: Decimal) -> Self {
44 Self { tolerance }
45 }
46
47 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)) }
71}
72
73#[derive(Debug, Clone)]
77pub struct ETRReconciliationData {
78 pub pre_tax_income: Decimal,
80 pub statutory_rate: Decimal,
82 pub actual_tax_expense: Decimal,
84 pub sum_reconciling_items: Decimal,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ETRReconciliationEvaluation {
91 pub reconciled: bool,
93 pub expected_tax: Decimal,
95 pub difference: Decimal,
97 pub passes: bool,
99 pub failures: Vec<String>,
101}
102
103pub struct ETRReconciliationEvaluator {
105 tolerance: Decimal,
106}
107
108impl ETRReconciliationEvaluator {
109 pub fn new(tolerance: Decimal) -> Self {
111 Self { tolerance }
112 }
113
114 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)) }
140}
141
142#[derive(Debug, Clone)]
146pub struct HedgeEffectivenessData {
147 pub total_hedges: usize,
149 pub effective_hedges: usize,
151 pub discontinued_hedges: usize,
153 pub discontinued_with_pl_entries: usize,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct HedgeEffectivenessEvaluation {
160 pub effectiveness_rate: f64,
162 pub all_discontinued_have_pl: bool,
164 pub passes: bool,
166 pub failures: Vec<String>,
168}
169
170pub struct HedgeEffectivenessEvaluator;
172
173impl HedgeEffectivenessEvaluator {
174 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#[derive(Debug, Clone)]
206pub struct PayrollHRReconciliationData {
207 pub salary_change_count: usize,
209 pub payroll_variance_count: usize,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct PayrollHRReconciliationEvaluation {
216 pub changes_traced: bool,
218 pub passes: bool,
220 pub failures: Vec<String>,
222}
223
224pub struct PayrollHRReconciliationEvaluator;
226
227impl PayrollHRReconciliationEvaluator {
228 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#[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 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), 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, };
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}