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)]
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 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), 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, };
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}