1use datasynth_core::models::JournalEntry;
4use rust_decimal::Decimal;
5
6#[macro_export]
8macro_rules! assert_balanced {
9 ($entry:expr) => {{
10 let entry = &$entry;
11 let total_debits: rust_decimal::Decimal = entry.lines.iter().map(|l| l.debit_amount).sum();
12 let total_credits: rust_decimal::Decimal =
13 entry.lines.iter().map(|l| l.credit_amount).sum();
14 assert_eq!(
15 total_debits, total_credits,
16 "Journal entry is not balanced: debits={}, credits={}",
17 total_debits, total_credits
18 );
19 }};
20}
21
22#[macro_export]
24macro_rules! assert_all_balanced {
25 ($entries:expr) => {{
26 for (i, entry) in $entries.iter().enumerate() {
27 let total_debits: rust_decimal::Decimal =
28 entry.lines.iter().map(|l| l.debit_amount).sum();
29 let total_credits: rust_decimal::Decimal =
30 entry.lines.iter().map(|l| l.credit_amount).sum();
31 assert_eq!(
32 total_debits, total_credits,
33 "Journal entry {} is not balanced: debits={}, credits={}",
34 i, total_debits, total_credits
35 );
36 }
37 }};
38}
39
40#[macro_export]
43macro_rules! assert_benford_compliant {
44 ($amounts:expr, $tolerance:expr) => {{
45 let amounts = &$amounts;
46 let expected = [0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046];
47 let mut counts = [0u64; 9];
48 let mut total = 0u64;
49
50 for amount in amounts.iter() {
51 if *amount > rust_decimal::Decimal::ZERO {
52 let first_digit = amount
53 .to_string()
54 .chars()
55 .find(|c| c.is_ascii_digit() && *c != '0')
56 .map(|c| c.to_digit(10).unwrap() as usize);
57
58 if let Some(d) = first_digit {
59 if d >= 1 && d <= 9 {
60 counts[d - 1] += 1;
61 total += 1;
62 }
63 }
64 }
65 }
66
67 if total > 0 {
68 for (i, (count, exp)) in counts.iter().zip(expected.iter()).enumerate() {
69 let observed = *count as f64 / total as f64;
70 let diff = (observed - exp).abs();
71 assert!(
72 diff < $tolerance,
73 "Benford's Law violation for digit {}: observed={:.4}, expected={:.4}, diff={:.4}",
74 i + 1,
75 observed,
76 exp,
77 diff
78 );
79 }
80 }
81 }};
82}
83
84pub fn is_balanced(entry: &JournalEntry) -> bool {
86 let total_debits: Decimal = entry.lines.iter().map(|l| l.debit_amount).sum();
87 let total_credits: Decimal = entry.lines.iter().map(|l| l.credit_amount).sum();
88 total_debits == total_credits
89}
90
91pub fn calculate_imbalance(entry: &JournalEntry) -> Decimal {
93 let total_debits: Decimal = entry.lines.iter().map(|l| l.debit_amount).sum();
94 let total_credits: Decimal = entry.lines.iter().map(|l| l.credit_amount).sum();
95 total_debits - total_credits
96}
97
98pub fn check_benford_distribution(amounts: &[Decimal]) -> (f64, bool) {
101 let expected = [
102 0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046,
103 ];
104 let mut counts = [0u64; 9];
105 let mut total = 0u64;
106
107 for amount in amounts.iter() {
108 if *amount > Decimal::ZERO {
109 let first_digit = amount
110 .to_string()
111 .chars()
112 .find(|c| c.is_ascii_digit() && *c != '0')
113 .map(|c| c.to_digit(10).unwrap() as usize);
114
115 if let Some(d) = first_digit {
116 if (1..=9).contains(&d) {
117 counts[d - 1] += 1;
118 total += 1;
119 }
120 }
121 }
122 }
123
124 if total == 0 {
125 return (0.0, true);
126 }
127
128 let mut chi_squared = 0.0;
130 for (count, exp) in counts.iter().zip(expected.iter()) {
131 let expected_count = exp * total as f64;
132 if expected_count > 0.0 {
133 let diff = *count as f64 - expected_count;
134 chi_squared += diff * diff / expected_count;
135 }
136 }
137
138 let passes = chi_squared < 20.090;
141
142 (chi_squared, passes)
143}
144
145pub fn check_accounting_equation(
147 total_assets: Decimal,
148 total_liabilities: Decimal,
149 total_equity: Decimal,
150) -> bool {
151 total_assets == total_liabilities + total_equity
152}
153
154pub fn check_trial_balance(debit_balances: &[Decimal], credit_balances: &[Decimal]) -> bool {
156 let total_debits: Decimal = debit_balances.iter().copied().sum();
157 let total_credits: Decimal = credit_balances.iter().copied().sum();
158 total_debits == total_credits
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::fixtures::*;
165
166 #[test]
167 fn test_is_balanced() {
168 let entry = balanced_journal_entry(Decimal::new(10000, 2));
169 assert!(is_balanced(&entry));
170 }
171
172 #[test]
173 fn test_is_not_balanced() {
174 let entry = unbalanced_journal_entry();
175 assert!(!is_balanced(&entry));
176 }
177
178 #[test]
179 fn test_calculate_imbalance_balanced() {
180 let entry = balanced_journal_entry(Decimal::new(10000, 2));
181 assert_eq!(calculate_imbalance(&entry), Decimal::ZERO);
182 }
183
184 #[test]
185 fn test_calculate_imbalance_unbalanced() {
186 let entry = unbalanced_journal_entry();
187 let imbalance = calculate_imbalance(&entry);
188 assert_ne!(imbalance, Decimal::ZERO);
189 }
190
191 #[test]
192 fn test_check_accounting_equation() {
193 assert!(check_accounting_equation(
195 Decimal::new(1000, 0),
196 Decimal::new(600, 0),
197 Decimal::new(400, 0)
198 ));
199
200 assert!(!check_accounting_equation(
202 Decimal::new(1000, 0),
203 Decimal::new(600, 0),
204 Decimal::new(300, 0)
205 ));
206 }
207
208 #[test]
209 fn test_check_trial_balance() {
210 let debits = vec![Decimal::new(1000, 0), Decimal::new(500, 0)];
211 let credits = vec![Decimal::new(1500, 0)];
212 assert!(check_trial_balance(&debits, &credits));
213
214 let unbalanced_credits = vec![Decimal::new(1000, 0)];
215 assert!(!check_trial_balance(&debits, &unbalanced_credits));
216 }
217
218 #[test]
219 fn test_benford_distribution_perfect() {
220 let mut amounts = Vec::new();
222 let expected_counts = [301, 176, 125, 97, 79, 67, 58, 51, 46]; for (digit, count) in expected_counts.iter().enumerate() {
225 let base = Decimal::new((digit + 1) as i64, 0);
226 for _ in 0..*count {
227 amounts.push(base);
228 }
229 }
230
231 let (chi_squared, passes) = check_benford_distribution(&amounts);
232 assert!(passes, "Chi-squared: {}", chi_squared);
233 }
234
235 #[test]
236 fn test_assert_balanced_macro() {
237 let entry = balanced_journal_entry(Decimal::new(10000, 2));
238 assert_balanced!(entry); }
240
241 #[test]
242 fn test_assert_all_balanced_macro() {
243 let entries = [
244 balanced_journal_entry(Decimal::new(10000, 2)),
245 balanced_journal_entry(Decimal::new(20000, 2)),
246 balanced_journal_entry(Decimal::new(30000, 2)),
247 ];
248 assert_all_balanced!(entries); }
250}