Skip to main content

datasynth_eval/coherence/
financial_package.rs

1//! Capstone financial statement package coherence validators.
2//!
3//! Validates the financial statements as a coherent whole, including:
4//! - Cash flow statement reconciliation (opening → closing cash)
5//! - Equity roll-forward (opening → closing equity via NI, OCI, dividends, stock comp)
6//! - Segment revenue reconciliation (segment totals net of IC eliminations → consolidated)
7//! - Trial balance master proof (opening TB + period JEs = closing TB, debits and credits)
8
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12// ─── CashFlowReconciliationEvaluator ─────────────────────────────────────────
13
14/// Input data for cash flow statement reconciliation.
15#[derive(Debug, Clone)]
16pub struct CashFlowReconciliationData {
17    /// Opening cash balance at the beginning of the period.
18    pub opening_cash: Decimal,
19    /// Net cash from operating activities.
20    pub net_operating: Decimal,
21    /// Net cash from investing activities.
22    pub net_investing: Decimal,
23    /// Net cash from financing activities.
24    pub net_financing: Decimal,
25    /// Closing cash balance per the GL / balance sheet.
26    pub closing_cash_gl: Decimal,
27}
28
29/// Results of cash flow statement reconciliation evaluation.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CashFlowReconciliationEvaluation {
32    /// Whether the computed closing cash reconciles to the GL balance.
33    pub reconciled: bool,
34    /// Expected closing cash: opening + operating + investing + financing.
35    pub expected_closing: Decimal,
36    /// Absolute difference between expected and GL closing cash.
37    pub difference: Decimal,
38    /// Overall pass/fail status.
39    pub passes: bool,
40    /// Human-readable descriptions of failed checks.
41    pub failures: Vec<String>,
42}
43
44/// Evaluator that verifies the cash flow statement reconciles end-to-end.
45pub struct CashFlowReconciliationEvaluator {
46    tolerance: Decimal,
47}
48
49impl CashFlowReconciliationEvaluator {
50    /// Create a new evaluator with the given absolute tolerance.
51    pub fn new(tolerance: Decimal) -> Self {
52        Self { tolerance }
53    }
54
55    /// Run the cash flow reconciliation check against `data`.
56    pub fn evaluate(&self, data: &CashFlowReconciliationData) -> CashFlowReconciliationEvaluation {
57        let expected_closing =
58            data.opening_cash + data.net_operating + data.net_investing + data.net_financing;
59        let difference = (expected_closing - data.closing_cash_gl).abs();
60        let reconciled = difference <= self.tolerance;
61        let mut failures = Vec::new();
62        if !reconciled {
63            failures.push(format!(
64                "Cash flow reconciliation failed: expected closing cash {} vs GL {} (diff {})",
65                expected_closing, data.closing_cash_gl, difference
66            ));
67        }
68        CashFlowReconciliationEvaluation {
69            reconciled,
70            expected_closing,
71            difference,
72            passes: reconciled,
73            failures,
74        }
75    }
76}
77
78impl Default for CashFlowReconciliationEvaluator {
79    fn default() -> Self {
80        Self::new(Decimal::new(1, 2)) // 0.01 tolerance
81    }
82}
83
84// ─── EquityRollforwardEvaluator ───────────────────────────────────────────────
85
86/// Input data for equity roll-forward reconciliation.
87#[derive(Debug, Clone)]
88pub struct EquityRollforwardData {
89    /// Opening total equity at the beginning of the period.
90    pub opening_equity: Decimal,
91    /// Net income (loss) for the period.
92    pub net_income: Decimal,
93    /// Other comprehensive income (OCI) movements for the period.
94    pub oci_movements: Decimal,
95    /// Dividends declared during the period (positive = reduction in equity).
96    pub dividends_declared: Decimal,
97    /// Stock-based compensation recognised during the period.
98    pub stock_comp: Decimal,
99    /// Closing total equity per the balance sheet.
100    pub closing_equity: Decimal,
101}
102
103/// Results of equity roll-forward reconciliation evaluation.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct EquityRollforwardEvaluation {
106    /// Whether the computed closing equity reconciles to the balance sheet.
107    pub reconciled: bool,
108    /// Expected closing equity: opening + NI + OCI − dividends + stock_comp.
109    pub expected_closing: Decimal,
110    /// Absolute difference between expected and balance sheet closing equity.
111    pub difference: Decimal,
112    /// Overall pass/fail status.
113    pub passes: bool,
114    /// Human-readable descriptions of failed checks.
115    pub failures: Vec<String>,
116}
117
118/// Evaluator that verifies the statement of changes in equity rolls forward correctly.
119pub struct EquityRollforwardEvaluator {
120    tolerance: Decimal,
121}
122
123impl EquityRollforwardEvaluator {
124    /// Create a new evaluator with the given absolute tolerance.
125    pub fn new(tolerance: Decimal) -> Self {
126        Self { tolerance }
127    }
128
129    /// Run the equity roll-forward check against `data`.
130    pub fn evaluate(&self, data: &EquityRollforwardData) -> EquityRollforwardEvaluation {
131        let expected_closing = data.opening_equity + data.net_income + data.oci_movements
132            - data.dividends_declared
133            + data.stock_comp;
134        let difference = (expected_closing - data.closing_equity).abs();
135        let reconciled = difference <= self.tolerance;
136        let mut failures = Vec::new();
137        if !reconciled {
138            failures.push(format!(
139                "Equity roll-forward reconciliation failed: expected closing equity {} vs balance sheet {} (diff {})",
140                expected_closing, data.closing_equity, difference
141            ));
142        }
143        EquityRollforwardEvaluation {
144            reconciled,
145            expected_closing,
146            difference,
147            passes: reconciled,
148            failures,
149        }
150    }
151}
152
153impl Default for EquityRollforwardEvaluator {
154    fn default() -> Self {
155        Self::new(Decimal::new(1, 2)) // 0.01 tolerance
156    }
157}
158
159// ─── SegmentReconciliationEvaluator ──────────────────────────────────────────
160
161/// Input data for segment revenue reconciliation.
162#[derive(Debug, Clone)]
163pub struct SegmentReconciliationData {
164    /// Sum of revenue reported across all operating segments.
165    pub sum_segment_revenue: Decimal,
166    /// Intercompany eliminations to be netted from segment totals.
167    pub ic_eliminations: Decimal,
168    /// Consolidated revenue per the group income statement.
169    pub consolidated_revenue: Decimal,
170}
171
172/// Results of segment revenue reconciliation evaluation.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct SegmentReconciliationEvaluation {
175    /// Whether the segment totals less eliminations reconcile to consolidated revenue.
176    pub reconciled: bool,
177    /// Expected consolidated revenue: sum_segment_revenue − ic_eliminations.
178    pub expected_consolidated: Decimal,
179    /// Absolute difference between expected and reported consolidated revenue.
180    pub difference: Decimal,
181    /// Overall pass/fail status.
182    pub passes: bool,
183    /// Human-readable descriptions of failed checks.
184    pub failures: Vec<String>,
185}
186
187/// Evaluator that verifies segment revenues net of eliminations equal consolidated revenue.
188pub struct SegmentReconciliationEvaluator {
189    tolerance: Decimal,
190}
191
192impl SegmentReconciliationEvaluator {
193    /// Create a new evaluator with the given absolute tolerance.
194    pub fn new(tolerance: Decimal) -> Self {
195        Self { tolerance }
196    }
197
198    /// Run the segment reconciliation check against `data`.
199    pub fn evaluate(&self, data: &SegmentReconciliationData) -> SegmentReconciliationEvaluation {
200        let expected_consolidated = data.sum_segment_revenue - data.ic_eliminations;
201        let difference = (expected_consolidated - data.consolidated_revenue).abs();
202        let reconciled = difference <= self.tolerance;
203        let mut failures = Vec::new();
204        if !reconciled {
205            failures.push(format!(
206                "Segment reconciliation failed: expected consolidated revenue {} vs reported {} (diff {})",
207                expected_consolidated, data.consolidated_revenue, difference
208            ));
209        }
210        SegmentReconciliationEvaluation {
211            reconciled,
212            expected_consolidated,
213            difference,
214            passes: reconciled,
215            failures,
216        }
217    }
218}
219
220impl Default for SegmentReconciliationEvaluator {
221    fn default() -> Self {
222        Self::new(Decimal::new(1, 2)) // 0.01 tolerance
223    }
224}
225
226// ─── TrialBalanceMasterProofEvaluator ─────────────────────────────────────────
227
228/// Input data for the trial balance master proof.
229///
230/// The master proof asserts: opening TB + period JEs = closing TB, for both
231/// debits and credits independently.
232#[derive(Debug, Clone)]
233pub struct TrialBalanceMasterProofData {
234    /// Sum of all debit balances on the opening trial balance.
235    pub sum_opening_debits: Decimal,
236    /// Sum of all credit balances on the opening trial balance.
237    pub sum_opening_credits: Decimal,
238    /// Sum of all debit sides of journal entries posted during the period.
239    pub sum_je_debits: Decimal,
240    /// Sum of all credit sides of journal entries posted during the period.
241    pub sum_je_credits: Decimal,
242    /// Sum of all debit balances on the closing trial balance.
243    pub closing_tb_debits: Decimal,
244    /// Sum of all credit balances on the closing trial balance.
245    pub closing_tb_credits: Decimal,
246}
247
248/// Results of the trial balance master proof evaluation.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct TrialBalanceMasterProofEvaluation {
251    /// Whether the debit side of the closing TB reconciles to opening + JE debits.
252    pub debits_reconciled: bool,
253    /// Whether the credit side of the closing TB reconciles to opening + JE credits.
254    pub credits_reconciled: bool,
255    /// Absolute difference on the debit side.
256    pub debit_difference: Decimal,
257    /// Absolute difference on the credit side.
258    pub credit_difference: Decimal,
259    /// Overall pass/fail status (both sides must reconcile).
260    pub passes: bool,
261    /// Human-readable descriptions of failed checks.
262    pub failures: Vec<String>,
263}
264
265/// Evaluator that performs the master trial balance proof — the capstone check that
266/// every generator's GL output is fully accounted for.
267pub struct TrialBalanceMasterProofEvaluator {
268    tolerance: Decimal,
269}
270
271impl TrialBalanceMasterProofEvaluator {
272    /// Create a new evaluator with the given absolute tolerance.
273    pub fn new(tolerance: Decimal) -> Self {
274        Self { tolerance }
275    }
276
277    /// Run the trial balance master proof against `data`.
278    pub fn evaluate(
279        &self,
280        data: &TrialBalanceMasterProofData,
281    ) -> TrialBalanceMasterProofEvaluation {
282        let expected_closing_debits = data.sum_opening_debits + data.sum_je_debits;
283        let expected_closing_credits = data.sum_opening_credits + data.sum_je_credits;
284
285        let debit_difference = (expected_closing_debits - data.closing_tb_debits).abs();
286        let credit_difference = (expected_closing_credits - data.closing_tb_credits).abs();
287
288        let debits_reconciled = debit_difference <= self.tolerance;
289        let credits_reconciled = credit_difference <= self.tolerance;
290
291        let mut failures = Vec::new();
292        if !debits_reconciled {
293            failures.push(format!(
294                "TB master proof (debits): expected {} vs closing TB {} (diff {})",
295                expected_closing_debits, data.closing_tb_debits, debit_difference
296            ));
297        }
298        if !credits_reconciled {
299            failures.push(format!(
300                "TB master proof (credits): expected {} vs closing TB {} (diff {})",
301                expected_closing_credits, data.closing_tb_credits, credit_difference
302            ));
303        }
304
305        TrialBalanceMasterProofEvaluation {
306            debits_reconciled,
307            credits_reconciled,
308            debit_difference,
309            credit_difference,
310            passes: debits_reconciled && credits_reconciled,
311            failures,
312        }
313    }
314}
315
316impl Default for TrialBalanceMasterProofEvaluator {
317    fn default() -> Self {
318        Self::new(Decimal::new(1, 2)) // 0.01 tolerance
319    }
320}
321
322// ─── Unit tests ──────────────────────────────────────────────────────────────
323
324#[cfg(test)]
325#[allow(clippy::unwrap_used)]
326mod tests {
327    use super::*;
328    use rust_decimal_macros::dec;
329
330    // Cash flow tests
331
332    #[test]
333    fn test_cash_flow_reconciliation_balanced() {
334        let data = CashFlowReconciliationData {
335            opening_cash: dec!(100_000),
336            net_operating: dec!(50_000),
337            net_investing: dec!(-20_000),
338            net_financing: dec!(-10_000),
339            closing_cash_gl: dec!(120_000),
340        };
341        let result = CashFlowReconciliationEvaluator::new(dec!(1)).evaluate(&data);
342        assert!(result.passes);
343        assert!(result.reconciled);
344        assert_eq!(result.expected_closing, dec!(120_000));
345        assert!(result.failures.is_empty());
346    }
347
348    #[test]
349    fn test_cash_flow_reconciliation_imbalanced() {
350        let data = CashFlowReconciliationData {
351            opening_cash: dec!(100_000),
352            net_operating: dec!(50_000),
353            net_investing: dec!(-20_000),
354            net_financing: dec!(-10_000),
355            closing_cash_gl: dec!(200_000), // Way off
356        };
357        let result = CashFlowReconciliationEvaluator::new(dec!(1)).evaluate(&data);
358        assert!(!result.passes);
359        assert!(!result.reconciled);
360        assert!(!result.failures.is_empty());
361    }
362
363    // Equity roll-forward tests
364
365    #[test]
366    fn test_equity_rollforward_balanced() {
367        // 500_000 + 80_000 + 10_000 - 20_000 + 5_000 = 575_000
368        let data = EquityRollforwardData {
369            opening_equity: dec!(500_000),
370            net_income: dec!(80_000),
371            oci_movements: dec!(10_000),
372            dividends_declared: dec!(20_000),
373            stock_comp: dec!(5_000),
374            closing_equity: dec!(575_000),
375        };
376        let result = EquityRollforwardEvaluator::new(dec!(1)).evaluate(&data);
377        assert!(result.passes);
378        assert!(result.reconciled);
379        assert_eq!(result.expected_closing, dec!(575_000));
380        assert!(result.failures.is_empty());
381    }
382
383    #[test]
384    fn test_equity_rollforward_imbalanced() {
385        let data = EquityRollforwardData {
386            opening_equity: dec!(500_000),
387            net_income: dec!(80_000),
388            oci_movements: dec!(10_000),
389            dividends_declared: dec!(20_000),
390            stock_comp: dec!(5_000),
391            closing_equity: dec!(999_999), // Wrong
392        };
393        let result = EquityRollforwardEvaluator::new(dec!(1)).evaluate(&data);
394        assert!(!result.passes);
395        assert!(!result.reconciled);
396        assert!(!result.failures.is_empty());
397    }
398
399    // Segment reconciliation tests
400
401    #[test]
402    fn test_segment_reconciliation_balanced() {
403        // 1_200_000 - 200_000 = 1_000_000
404        let data = SegmentReconciliationData {
405            sum_segment_revenue: dec!(1_200_000),
406            ic_eliminations: dec!(200_000),
407            consolidated_revenue: dec!(1_000_000),
408        };
409        let result = SegmentReconciliationEvaluator::new(dec!(1)).evaluate(&data);
410        assert!(result.passes);
411        assert!(result.reconciled);
412        assert_eq!(result.expected_consolidated, dec!(1_000_000));
413        assert!(result.failures.is_empty());
414    }
415
416    #[test]
417    fn test_segment_reconciliation_imbalanced() {
418        let data = SegmentReconciliationData {
419            sum_segment_revenue: dec!(1_200_000),
420            ic_eliminations: dec!(200_000),
421            consolidated_revenue: dec!(850_000), // Missing 150_000
422        };
423        let result = SegmentReconciliationEvaluator::new(dec!(1)).evaluate(&data);
424        assert!(!result.passes);
425        assert!(!result.reconciled);
426        assert!(!result.failures.is_empty());
427    }
428
429    // Trial balance master proof tests
430
431    #[test]
432    fn test_tb_master_proof_both_balanced() {
433        let data = TrialBalanceMasterProofData {
434            sum_opening_debits: dec!(500_000),
435            sum_opening_credits: dec!(500_000),
436            sum_je_debits: dec!(100_000),
437            sum_je_credits: dec!(100_000),
438            closing_tb_debits: dec!(600_000),
439            closing_tb_credits: dec!(600_000),
440        };
441        let result = TrialBalanceMasterProofEvaluator::new(dec!(1)).evaluate(&data);
442        assert!(result.passes);
443        assert!(result.debits_reconciled);
444        assert!(result.credits_reconciled);
445        assert!(result.failures.is_empty());
446    }
447
448    #[test]
449    fn test_tb_master_proof_debits_imbalanced() {
450        let data = TrialBalanceMasterProofData {
451            sum_opening_debits: dec!(500_000),
452            sum_opening_credits: dec!(500_000),
453            sum_je_debits: dec!(100_000),
454            sum_je_credits: dec!(100_000),
455            closing_tb_debits: dec!(550_000), // 50_000 short
456            closing_tb_credits: dec!(600_000),
457        };
458        let result = TrialBalanceMasterProofEvaluator::new(dec!(1)).evaluate(&data);
459        assert!(!result.passes);
460        assert!(!result.debits_reconciled);
461        assert!(result.credits_reconciled);
462        assert_eq!(result.failures.len(), 1);
463    }
464
465    #[test]
466    fn test_tb_master_proof_credits_imbalanced() {
467        let data = TrialBalanceMasterProofData {
468            sum_opening_debits: dec!(500_000),
469            sum_opening_credits: dec!(500_000),
470            sum_je_debits: dec!(100_000),
471            sum_je_credits: dec!(100_000),
472            closing_tb_debits: dec!(600_000),
473            closing_tb_credits: dec!(550_000), // 50_000 short
474        };
475        let result = TrialBalanceMasterProofEvaluator::new(dec!(1)).evaluate(&data);
476        assert!(!result.passes);
477        assert!(result.debits_reconciled);
478        assert!(!result.credits_reconciled);
479        assert_eq!(result.failures.len(), 1);
480    }
481
482    #[test]
483    fn test_tb_master_proof_both_imbalanced() {
484        let data = TrialBalanceMasterProofData {
485            sum_opening_debits: dec!(500_000),
486            sum_opening_credits: dec!(500_000),
487            sum_je_debits: dec!(100_000),
488            sum_je_credits: dec!(100_000),
489            closing_tb_debits: dec!(400_000),  // Wrong
490            closing_tb_credits: dec!(700_000), // Wrong
491        };
492        let result = TrialBalanceMasterProofEvaluator::new(dec!(1)).evaluate(&data);
493        assert!(!result.passes);
494        assert!(!result.debits_reconciled);
495        assert!(!result.credits_reconciled);
496        assert_eq!(result.failures.len(), 2);
497    }
498}