use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct InterestExpenseProofData {
pub total_interest_expense_gl: Decimal,
pub sum_instrument_interest: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InterestExpenseProofEvaluation {
pub reconciled: bool,
pub difference: Decimal,
pub passes: bool,
pub failures: Vec<String>,
}
pub struct InterestExpenseProofEvaluator {
tolerance: Decimal,
}
impl InterestExpenseProofEvaluator {
pub fn new(tolerance: Decimal) -> Self {
Self { tolerance }
}
pub fn evaluate(&self, data: &InterestExpenseProofData) -> InterestExpenseProofEvaluation {
let difference = (data.total_interest_expense_gl - data.sum_instrument_interest).abs();
let reconciled = difference <= self.tolerance;
let mut failures = Vec::new();
if !reconciled {
failures.push(format!(
"Interest expense GL {} vs instruments {} (diff {})",
data.total_interest_expense_gl, data.sum_instrument_interest, difference
));
}
InterestExpenseProofEvaluation {
reconciled,
difference,
passes: reconciled,
failures,
}
}
}
impl Default for InterestExpenseProofEvaluator {
fn default() -> Self {
Self::new(Decimal::new(1, 2)) }
}
#[derive(Debug, Clone)]
pub struct ETRReconciliationData {
pub pre_tax_income: Decimal,
pub statutory_rate: Decimal,
pub actual_tax_expense: Decimal,
pub sum_reconciling_items: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ETRReconciliationEvaluation {
pub reconciled: bool,
pub expected_tax: Decimal,
pub difference: Decimal,
pub passes: bool,
pub failures: Vec<String>,
}
pub struct ETRReconciliationEvaluator {
tolerance: Decimal,
}
impl ETRReconciliationEvaluator {
pub fn new(tolerance: Decimal) -> Self {
Self { tolerance }
}
pub fn evaluate(&self, data: &ETRReconciliationData) -> ETRReconciliationEvaluation {
let expected_tax = data.pre_tax_income * data.statutory_rate + data.sum_reconciling_items;
let difference = (expected_tax - data.actual_tax_expense).abs();
let reconciled = difference <= self.tolerance;
let mut failures = Vec::new();
if !reconciled {
failures.push(format!(
"ETR reconciliation failed: expected tax {} vs actual {} (diff {})",
expected_tax, data.actual_tax_expense, difference
));
}
ETRReconciliationEvaluation {
reconciled,
expected_tax,
difference,
passes: reconciled,
failures,
}
}
}
impl Default for ETRReconciliationEvaluator {
fn default() -> Self {
Self::new(Decimal::new(1, 2)) }
}
#[derive(Debug, Clone)]
pub struct HedgeEffectivenessData {
pub total_hedges: usize,
pub effective_hedges: usize,
pub discontinued_hedges: usize,
pub discontinued_with_pl_entries: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HedgeEffectivenessEvaluation {
pub effectiveness_rate: f64,
pub all_discontinued_have_pl: bool,
pub passes: bool,
pub failures: Vec<String>,
}
pub struct HedgeEffectivenessEvaluator;
impl HedgeEffectivenessEvaluator {
pub fn evaluate(&self, data: &HedgeEffectivenessData) -> HedgeEffectivenessEvaluation {
let effectiveness_rate = if data.total_hedges == 0 {
1.0
} else {
data.effective_hedges as f64 / data.total_hedges as f64
};
let all_discontinued_have_pl =
data.discontinued_with_pl_entries >= data.discontinued_hedges;
let mut failures = Vec::new();
if !all_discontinued_have_pl {
failures.push(format!(
"Hedge discontinuation incomplete: {}/{} discontinued hedges have P&L reclassification entries",
data.discontinued_with_pl_entries, data.discontinued_hedges
));
}
let passes = failures.is_empty();
HedgeEffectivenessEvaluation {
effectiveness_rate,
all_discontinued_have_pl,
passes,
failures,
}
}
}
#[derive(Debug, Clone)]
pub struct PayrollHRReconciliationData {
pub salary_change_count: usize,
pub payroll_variance_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PayrollHRReconciliationEvaluation {
pub changes_traced: bool,
pub passes: bool,
pub failures: Vec<String>,
}
pub struct PayrollHRReconciliationEvaluator;
impl PayrollHRReconciliationEvaluator {
pub fn evaluate(
&self,
data: &PayrollHRReconciliationData,
) -> PayrollHRReconciliationEvaluation {
let changes_traced = data.payroll_variance_count >= data.salary_change_count;
let mut failures = Vec::new();
if !changes_traced {
failures.push(format!(
"Payroll/HR reconciliation failed: {} salary changes but only {} payroll variance entries",
data.salary_change_count, data.payroll_variance_count
));
}
PayrollHRReconciliationEvaluation {
changes_traced,
passes: changes_traced,
failures,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_interest_expense_proof_reconciled() {
let data = InterestExpenseProofData {
total_interest_expense_gl: dec!(50_000),
sum_instrument_interest: dec!(50_000),
};
let result = InterestExpenseProofEvaluator::new(dec!(100)).evaluate(&data);
assert!(result.passes);
assert!(result.reconciled);
assert!(result.failures.is_empty());
}
#[test]
fn test_interest_expense_proof_unreconciled() {
let data = InterestExpenseProofData {
total_interest_expense_gl: dec!(50_000),
sum_instrument_interest: dec!(30_000),
};
let result = InterestExpenseProofEvaluator::new(dec!(100)).evaluate(&data);
assert!(!result.passes);
assert!(!result.failures.is_empty());
}
#[test]
fn test_etr_reconciliation_reconciled() {
let data = ETRReconciliationData {
pre_tax_income: dec!(1_000_000),
statutory_rate: dec!(0.21),
actual_tax_expense: dec!(230_000),
sum_reconciling_items: dec!(20_000),
};
let result = ETRReconciliationEvaluator::new(dec!(1_000)).evaluate(&data);
assert!(result.passes);
assert_eq!(result.expected_tax, dec!(230_000));
}
#[test]
fn test_etr_reconciliation_unreconciled() {
let data = ETRReconciliationData {
pre_tax_income: dec!(1_000_000),
statutory_rate: dec!(0.21),
actual_tax_expense: dec!(999_000), sum_reconciling_items: dec!(0),
};
let result = ETRReconciliationEvaluator::new(dec!(1_000)).evaluate(&data);
assert!(!result.passes);
assert!(!result.failures.is_empty());
}
#[test]
fn test_hedge_effectiveness_all_compliant() {
let data = HedgeEffectivenessData {
total_hedges: 10,
effective_hedges: 9,
discontinued_hedges: 1,
discontinued_with_pl_entries: 1,
};
let result = HedgeEffectivenessEvaluator.evaluate(&data);
assert!(result.passes);
assert!(result.all_discontinued_have_pl);
assert!((result.effectiveness_rate - 0.9).abs() < f64::EPSILON);
}
#[test]
fn test_hedge_effectiveness_missing_pl_entry() {
let data = HedgeEffectivenessData {
total_hedges: 10,
effective_hedges: 8,
discontinued_hedges: 2,
discontinued_with_pl_entries: 1, };
let result = HedgeEffectivenessEvaluator.evaluate(&data);
assert!(!result.passes);
assert!(!result.all_discontinued_have_pl);
}
#[test]
fn test_payroll_hr_changes_traced() {
let data = PayrollHRReconciliationData {
salary_change_count: 5,
payroll_variance_count: 5,
};
let result = PayrollHRReconciliationEvaluator.evaluate(&data);
assert!(result.passes);
assert!(result.changes_traced);
}
#[test]
fn test_payroll_hr_changes_missing_variances() {
let data = PayrollHRReconciliationData {
salary_change_count: 5,
payroll_variance_count: 3,
};
let result = PayrollHRReconciliationEvaluator.evaluate(&data);
assert!(!result.passes);
assert!(!result.changes_traced);
assert!(!result.failures.is_empty());
}
}