1use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone)]
16pub struct CashFlowReconciliationData {
17 pub opening_cash: Decimal,
19 pub net_operating: Decimal,
21 pub net_investing: Decimal,
23 pub net_financing: Decimal,
25 pub closing_cash_gl: Decimal,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CashFlowReconciliationEvaluation {
32 pub reconciled: bool,
34 pub expected_closing: Decimal,
36 pub difference: Decimal,
38 pub passes: bool,
40 pub failures: Vec<String>,
42}
43
44pub struct CashFlowReconciliationEvaluator {
46 tolerance: Decimal,
47}
48
49impl CashFlowReconciliationEvaluator {
50 pub fn new(tolerance: Decimal) -> Self {
52 Self { tolerance }
53 }
54
55 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)) }
82}
83
84#[derive(Debug, Clone)]
88pub struct EquityRollforwardData {
89 pub opening_equity: Decimal,
91 pub net_income: Decimal,
93 pub oci_movements: Decimal,
95 pub dividends_declared: Decimal,
97 pub stock_comp: Decimal,
99 pub closing_equity: Decimal,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct EquityRollforwardEvaluation {
106 pub reconciled: bool,
108 pub expected_closing: Decimal,
110 pub difference: Decimal,
112 pub passes: bool,
114 pub failures: Vec<String>,
116}
117
118pub struct EquityRollforwardEvaluator {
120 tolerance: Decimal,
121}
122
123impl EquityRollforwardEvaluator {
124 pub fn new(tolerance: Decimal) -> Self {
126 Self { tolerance }
127 }
128
129 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)) }
157}
158
159#[derive(Debug, Clone)]
163pub struct SegmentReconciliationData {
164 pub sum_segment_revenue: Decimal,
166 pub ic_eliminations: Decimal,
168 pub consolidated_revenue: Decimal,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct SegmentReconciliationEvaluation {
175 pub reconciled: bool,
177 pub expected_consolidated: Decimal,
179 pub difference: Decimal,
181 pub passes: bool,
183 pub failures: Vec<String>,
185}
186
187pub struct SegmentReconciliationEvaluator {
189 tolerance: Decimal,
190}
191
192impl SegmentReconciliationEvaluator {
193 pub fn new(tolerance: Decimal) -> Self {
195 Self { tolerance }
196 }
197
198 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)) }
224}
225
226#[derive(Debug, Clone)]
233pub struct TrialBalanceMasterProofData {
234 pub sum_opening_debits: Decimal,
236 pub sum_opening_credits: Decimal,
238 pub sum_je_debits: Decimal,
240 pub sum_je_credits: Decimal,
242 pub closing_tb_debits: Decimal,
244 pub closing_tb_credits: Decimal,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct TrialBalanceMasterProofEvaluation {
251 pub debits_reconciled: bool,
253 pub credits_reconciled: bool,
255 pub debit_difference: Decimal,
257 pub credit_difference: Decimal,
259 pub passes: bool,
261 pub failures: Vec<String>,
263}
264
265pub struct TrialBalanceMasterProofEvaluator {
268 tolerance: Decimal,
269}
270
271impl TrialBalanceMasterProofEvaluator {
272 pub fn new(tolerance: Decimal) -> Self {
274 Self { tolerance }
275 }
276
277 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)) }
320}
321
322#[cfg(test)]
325#[allow(clippy::unwrap_used)]
326mod tests {
327 use super::*;
328 use rust_decimal_macros::dec;
329
330 #[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), };
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 #[test]
366 fn test_equity_rollforward_balanced() {
367 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), };
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 #[test]
402 fn test_segment_reconciliation_balanced() {
403 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), };
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 #[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), 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), };
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), closing_tb_credits: dec!(700_000), };
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}