use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct CashFlowReconciliationData {
pub opening_cash: Decimal,
pub net_operating: Decimal,
pub net_investing: Decimal,
pub net_financing: Decimal,
pub closing_cash_gl: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CashFlowReconciliationEvaluation {
pub reconciled: bool,
pub expected_closing: Decimal,
pub difference: Decimal,
pub passes: bool,
pub failures: Vec<String>,
}
pub struct CashFlowReconciliationEvaluator {
tolerance: Decimal,
}
impl CashFlowReconciliationEvaluator {
pub fn new(tolerance: Decimal) -> Self {
Self { tolerance }
}
pub fn evaluate(&self, data: &CashFlowReconciliationData) -> CashFlowReconciliationEvaluation {
let expected_closing =
data.opening_cash + data.net_operating + data.net_investing + data.net_financing;
let difference = (expected_closing - data.closing_cash_gl).abs();
let reconciled = difference <= self.tolerance;
let mut failures = Vec::new();
if !reconciled {
failures.push(format!(
"Cash flow reconciliation failed: expected closing cash {} vs GL {} (diff {})",
expected_closing, data.closing_cash_gl, difference
));
}
CashFlowReconciliationEvaluation {
reconciled,
expected_closing,
difference,
passes: reconciled,
failures,
}
}
}
impl Default for CashFlowReconciliationEvaluator {
fn default() -> Self {
Self::new(Decimal::new(1, 2)) }
}
#[derive(Debug, Clone)]
pub struct EquityRollforwardData {
pub opening_equity: Decimal,
pub net_income: Decimal,
pub oci_movements: Decimal,
pub dividends_declared: Decimal,
pub stock_comp: Decimal,
pub closing_equity: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EquityRollforwardEvaluation {
pub reconciled: bool,
pub expected_closing: Decimal,
pub difference: Decimal,
pub passes: bool,
pub failures: Vec<String>,
}
pub struct EquityRollforwardEvaluator {
tolerance: Decimal,
}
impl EquityRollforwardEvaluator {
pub fn new(tolerance: Decimal) -> Self {
Self { tolerance }
}
pub fn evaluate(&self, data: &EquityRollforwardData) -> EquityRollforwardEvaluation {
let expected_closing = data.opening_equity + data.net_income + data.oci_movements
- data.dividends_declared
+ data.stock_comp;
let difference = (expected_closing - data.closing_equity).abs();
let reconciled = difference <= self.tolerance;
let mut failures = Vec::new();
if !reconciled {
failures.push(format!(
"Equity roll-forward reconciliation failed: expected closing equity {} vs balance sheet {} (diff {})",
expected_closing, data.closing_equity, difference
));
}
EquityRollforwardEvaluation {
reconciled,
expected_closing,
difference,
passes: reconciled,
failures,
}
}
}
impl Default for EquityRollforwardEvaluator {
fn default() -> Self {
Self::new(Decimal::new(1, 2)) }
}
#[derive(Debug, Clone)]
pub struct SegmentReconciliationData {
pub sum_segment_revenue: Decimal,
pub ic_eliminations: Decimal,
pub consolidated_revenue: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SegmentReconciliationEvaluation {
pub reconciled: bool,
pub expected_consolidated: Decimal,
pub difference: Decimal,
pub passes: bool,
pub failures: Vec<String>,
}
pub struct SegmentReconciliationEvaluator {
tolerance: Decimal,
}
impl SegmentReconciliationEvaluator {
pub fn new(tolerance: Decimal) -> Self {
Self { tolerance }
}
pub fn evaluate(&self, data: &SegmentReconciliationData) -> SegmentReconciliationEvaluation {
let expected_consolidated = data.sum_segment_revenue - data.ic_eliminations;
let difference = (expected_consolidated - data.consolidated_revenue).abs();
let reconciled = difference <= self.tolerance;
let mut failures = Vec::new();
if !reconciled {
failures.push(format!(
"Segment reconciliation failed: expected consolidated revenue {} vs reported {} (diff {})",
expected_consolidated, data.consolidated_revenue, difference
));
}
SegmentReconciliationEvaluation {
reconciled,
expected_consolidated,
difference,
passes: reconciled,
failures,
}
}
}
impl Default for SegmentReconciliationEvaluator {
fn default() -> Self {
Self::new(Decimal::new(1, 2)) }
}
#[derive(Debug, Clone)]
pub struct TrialBalanceMasterProofData {
pub sum_opening_debits: Decimal,
pub sum_opening_credits: Decimal,
pub sum_je_debits: Decimal,
pub sum_je_credits: Decimal,
pub closing_tb_debits: Decimal,
pub closing_tb_credits: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrialBalanceMasterProofEvaluation {
pub debits_reconciled: bool,
pub credits_reconciled: bool,
pub debit_difference: Decimal,
pub credit_difference: Decimal,
pub passes: bool,
pub failures: Vec<String>,
}
pub struct TrialBalanceMasterProofEvaluator {
tolerance: Decimal,
}
impl TrialBalanceMasterProofEvaluator {
pub fn new(tolerance: Decimal) -> Self {
Self { tolerance }
}
pub fn evaluate(
&self,
data: &TrialBalanceMasterProofData,
) -> TrialBalanceMasterProofEvaluation {
let expected_closing_debits = data.sum_opening_debits + data.sum_je_debits;
let expected_closing_credits = data.sum_opening_credits + data.sum_je_credits;
let debit_difference = (expected_closing_debits - data.closing_tb_debits).abs();
let credit_difference = (expected_closing_credits - data.closing_tb_credits).abs();
let debits_reconciled = debit_difference <= self.tolerance;
let credits_reconciled = credit_difference <= self.tolerance;
let mut failures = Vec::new();
if !debits_reconciled {
failures.push(format!(
"TB master proof (debits): expected {} vs closing TB {} (diff {})",
expected_closing_debits, data.closing_tb_debits, debit_difference
));
}
if !credits_reconciled {
failures.push(format!(
"TB master proof (credits): expected {} vs closing TB {} (diff {})",
expected_closing_credits, data.closing_tb_credits, credit_difference
));
}
TrialBalanceMasterProofEvaluation {
debits_reconciled,
credits_reconciled,
debit_difference,
credit_difference,
passes: debits_reconciled && credits_reconciled,
failures,
}
}
}
impl Default for TrialBalanceMasterProofEvaluator {
fn default() -> Self {
Self::new(Decimal::new(1, 2)) }
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_cash_flow_reconciliation_balanced() {
let data = CashFlowReconciliationData {
opening_cash: dec!(100_000),
net_operating: dec!(50_000),
net_investing: dec!(-20_000),
net_financing: dec!(-10_000),
closing_cash_gl: dec!(120_000),
};
let result = CashFlowReconciliationEvaluator::new(dec!(1)).evaluate(&data);
assert!(result.passes);
assert!(result.reconciled);
assert_eq!(result.expected_closing, dec!(120_000));
assert!(result.failures.is_empty());
}
#[test]
fn test_cash_flow_reconciliation_imbalanced() {
let data = CashFlowReconciliationData {
opening_cash: dec!(100_000),
net_operating: dec!(50_000),
net_investing: dec!(-20_000),
net_financing: dec!(-10_000),
closing_cash_gl: dec!(200_000), };
let result = CashFlowReconciliationEvaluator::new(dec!(1)).evaluate(&data);
assert!(!result.passes);
assert!(!result.reconciled);
assert!(!result.failures.is_empty());
}
#[test]
fn test_equity_rollforward_balanced() {
let data = EquityRollforwardData {
opening_equity: dec!(500_000),
net_income: dec!(80_000),
oci_movements: dec!(10_000),
dividends_declared: dec!(20_000),
stock_comp: dec!(5_000),
closing_equity: dec!(575_000),
};
let result = EquityRollforwardEvaluator::new(dec!(1)).evaluate(&data);
assert!(result.passes);
assert!(result.reconciled);
assert_eq!(result.expected_closing, dec!(575_000));
assert!(result.failures.is_empty());
}
#[test]
fn test_equity_rollforward_imbalanced() {
let data = EquityRollforwardData {
opening_equity: dec!(500_000),
net_income: dec!(80_000),
oci_movements: dec!(10_000),
dividends_declared: dec!(20_000),
stock_comp: dec!(5_000),
closing_equity: dec!(999_999), };
let result = EquityRollforwardEvaluator::new(dec!(1)).evaluate(&data);
assert!(!result.passes);
assert!(!result.reconciled);
assert!(!result.failures.is_empty());
}
#[test]
fn test_segment_reconciliation_balanced() {
let data = SegmentReconciliationData {
sum_segment_revenue: dec!(1_200_000),
ic_eliminations: dec!(200_000),
consolidated_revenue: dec!(1_000_000),
};
let result = SegmentReconciliationEvaluator::new(dec!(1)).evaluate(&data);
assert!(result.passes);
assert!(result.reconciled);
assert_eq!(result.expected_consolidated, dec!(1_000_000));
assert!(result.failures.is_empty());
}
#[test]
fn test_segment_reconciliation_imbalanced() {
let data = SegmentReconciliationData {
sum_segment_revenue: dec!(1_200_000),
ic_eliminations: dec!(200_000),
consolidated_revenue: dec!(850_000), };
let result = SegmentReconciliationEvaluator::new(dec!(1)).evaluate(&data);
assert!(!result.passes);
assert!(!result.reconciled);
assert!(!result.failures.is_empty());
}
#[test]
fn test_tb_master_proof_both_balanced() {
let data = TrialBalanceMasterProofData {
sum_opening_debits: dec!(500_000),
sum_opening_credits: dec!(500_000),
sum_je_debits: dec!(100_000),
sum_je_credits: dec!(100_000),
closing_tb_debits: dec!(600_000),
closing_tb_credits: dec!(600_000),
};
let result = TrialBalanceMasterProofEvaluator::new(dec!(1)).evaluate(&data);
assert!(result.passes);
assert!(result.debits_reconciled);
assert!(result.credits_reconciled);
assert!(result.failures.is_empty());
}
#[test]
fn test_tb_master_proof_debits_imbalanced() {
let data = TrialBalanceMasterProofData {
sum_opening_debits: dec!(500_000),
sum_opening_credits: dec!(500_000),
sum_je_debits: dec!(100_000),
sum_je_credits: dec!(100_000),
closing_tb_debits: dec!(550_000), closing_tb_credits: dec!(600_000),
};
let result = TrialBalanceMasterProofEvaluator::new(dec!(1)).evaluate(&data);
assert!(!result.passes);
assert!(!result.debits_reconciled);
assert!(result.credits_reconciled);
assert_eq!(result.failures.len(), 1);
}
#[test]
fn test_tb_master_proof_credits_imbalanced() {
let data = TrialBalanceMasterProofData {
sum_opening_debits: dec!(500_000),
sum_opening_credits: dec!(500_000),
sum_je_debits: dec!(100_000),
sum_je_credits: dec!(100_000),
closing_tb_debits: dec!(600_000),
closing_tb_credits: dec!(550_000), };
let result = TrialBalanceMasterProofEvaluator::new(dec!(1)).evaluate(&data);
assert!(!result.passes);
assert!(result.debits_reconciled);
assert!(!result.credits_reconciled);
assert_eq!(result.failures.len(), 1);
}
#[test]
fn test_tb_master_proof_both_imbalanced() {
let data = TrialBalanceMasterProofData {
sum_opening_debits: dec!(500_000),
sum_opening_credits: dec!(500_000),
sum_je_debits: dec!(100_000),
sum_je_credits: dec!(100_000),
closing_tb_debits: dec!(400_000), closing_tb_credits: dec!(700_000), };
let result = TrialBalanceMasterProofEvaluator::new(dec!(1)).evaluate(&data);
assert!(!result.passes);
assert!(!result.debits_reconciled);
assert!(!result.credits_reconciled);
assert_eq!(result.failures.len(), 2);
}
}