datasynth_generators/tax/
tax_provision_generator.rs1use chrono::NaiveDate;
9use datasynth_core::utils::seeded_rng;
10use rand::prelude::*;
11use rand_chacha::ChaCha8Rng;
12use rust_decimal::Decimal;
13use rust_decimal_macros::dec;
14
15use datasynth_core::models::TaxProvision;
16
17struct ReconciliationCandidate {
23 description: &'static str,
24 min_impact: Decimal,
26 max_impact: Decimal,
28}
29
30const CANDIDATES: &[ReconciliationCandidate] = &[
32 ReconciliationCandidate {
33 description: "State and local taxes",
34 min_impact: dec!(0.01),
35 max_impact: dec!(0.04),
36 },
37 ReconciliationCandidate {
38 description: "Permanent differences",
39 min_impact: dec!(-0.01),
40 max_impact: dec!(0.02),
41 },
42 ReconciliationCandidate {
43 description: "R&D tax credits",
44 min_impact: dec!(-0.02),
45 max_impact: dec!(-0.005),
46 },
47 ReconciliationCandidate {
48 description: "Foreign rate differential",
49 min_impact: dec!(-0.03),
50 max_impact: dec!(0.03),
51 },
52 ReconciliationCandidate {
53 description: "Stock compensation",
54 min_impact: dec!(-0.01),
55 max_impact: dec!(0.01),
56 },
57 ReconciliationCandidate {
58 description: "Valuation allowance change",
59 min_impact: dec!(-0.02),
60 max_impact: dec!(0.05),
61 },
62];
63
64pub struct TaxProvisionGenerator {
76 rng: ChaCha8Rng,
77 counter: u64,
78}
79
80impl TaxProvisionGenerator {
81 pub fn new(seed: u64) -> Self {
83 Self {
84 rng: seeded_rng(seed, 0),
85 counter: 0,
86 }
87 }
88
89 pub fn generate(
98 &mut self,
99 entity_id: &str,
100 period: NaiveDate,
101 pre_tax_income: Decimal,
102 statutory_rate: Decimal,
103 ) -> TaxProvision {
104 self.counter += 1;
105 let provision_id = format!("TXPROV-{:06}", self.counter);
106
107 let num_items = self.rng.gen_range(2..=5);
109 let mut selected_indices: Vec<usize> = (0..CANDIDATES.len()).collect();
110 selected_indices.shuffle(&mut self.rng);
111 selected_indices.truncate(num_items);
112 selected_indices.sort(); let mut total_impact = Decimal::ZERO;
115 let mut reconciliation_items: Vec<(&str, Decimal)> = Vec::new();
116
117 for &idx in &selected_indices {
118 let candidate = &CANDIDATES[idx];
119 let impact = self.random_decimal(candidate.min_impact, candidate.max_impact);
120 total_impact += impact;
121 reconciliation_items.push((candidate.description, impact));
122 }
123
124 let effective_rate = (statutory_rate + total_impact).round_dp(6);
125 let current_tax_expense = (pre_tax_income * effective_rate).round_dp(2);
126
127 let dta_pct = self.random_decimal(dec!(0.01), dec!(0.08));
130 let deferred_tax_asset = (pre_tax_income.abs() * dta_pct).round_dp(2);
131
132 let dtl_pct = self.random_decimal(dec!(0.01), dec!(0.06));
134 let deferred_tax_liability = (pre_tax_income.abs() * dtl_pct).round_dp(2);
135
136 let mut provision = TaxProvision::new(
137 provision_id,
138 entity_id,
139 period,
140 current_tax_expense,
141 deferred_tax_asset,
142 deferred_tax_liability,
143 statutory_rate,
144 effective_rate,
145 );
146
147 for (desc, impact) in &reconciliation_items {
148 provision = provision.with_reconciliation_item(*desc, *impact);
149 }
150
151 provision
152 }
153
154 fn random_decimal(&mut self, min: Decimal, max: Decimal) -> Decimal {
156 let range_f64 = (max - min).to_string().parse::<f64>().unwrap_or(0.0);
157 let min_f64 = min.to_string().parse::<f64>().unwrap_or(0.0);
158 let val = min_f64 + self.rng.gen::<f64>() * range_f64;
159 Decimal::try_from(val).unwrap_or(min).round_dp(6)
160 }
161}
162
163#[cfg(test)]
168#[allow(clippy::unwrap_used)]
169mod tests {
170 use super::*;
171
172 fn period_end() -> NaiveDate {
173 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()
174 }
175
176 #[test]
177 fn test_provision_calculation() {
178 let mut gen = TaxProvisionGenerator::new(42);
179 let provision = gen.generate("ENT-001", period_end(), dec!(1000000), dec!(0.21));
180
181 let total_impact: Decimal = provision
183 .rate_reconciliation
184 .iter()
185 .map(|r| r.rate_impact)
186 .sum();
187 let expected_effective = (dec!(0.21) + total_impact).round_dp(6);
188 assert_eq!(provision.effective_rate, expected_effective);
189
190 let expected_expense = (dec!(1000000) * provision.effective_rate).round_dp(2);
192 assert_eq!(provision.current_tax_expense, expected_expense);
193
194 assert_eq!(provision.statutory_rate, dec!(0.21));
196 }
197
198 #[test]
199 fn test_rate_reconciliation() {
200 let mut gen = TaxProvisionGenerator::new(42);
201 let provision = gen.generate("ENT-001", period_end(), dec!(500000), dec!(0.21));
202
203 assert!(
205 provision.rate_reconciliation.len() >= 2,
206 "Should have at least 2 items, got {}",
207 provision.rate_reconciliation.len()
208 );
209 assert!(
210 provision.rate_reconciliation.len() <= 5,
211 "Should have at most 5 items, got {}",
212 provision.rate_reconciliation.len()
213 );
214
215 let total_impact: Decimal = provision
217 .rate_reconciliation
218 .iter()
219 .map(|r| r.rate_impact)
220 .sum();
221 let diff = (provision.effective_rate - provision.statutory_rate).round_dp(6);
222 let impact_rounded = total_impact.round_dp(6);
223
224 let tolerance = dec!(0.000002);
226 assert!(
227 (diff - impact_rounded).abs() <= tolerance,
228 "Reconciliation items should sum to effective - statutory: diff={}, impact={}",
229 diff,
230 impact_rounded
231 );
232 }
233
234 #[test]
235 fn test_deferred_tax() {
236 let mut gen = TaxProvisionGenerator::new(42);
237 let provision = gen.generate("ENT-001", period_end(), dec!(2000000), dec!(0.21));
238
239 assert!(
241 provision.deferred_tax_asset > Decimal::ZERO,
242 "DTA should be positive: {}",
243 provision.deferred_tax_asset
244 );
245 assert!(
246 provision.deferred_tax_liability > Decimal::ZERO,
247 "DTL should be positive: {}",
248 provision.deferred_tax_liability
249 );
250
251 let pti = dec!(2000000);
253 assert!(
254 provision.deferred_tax_asset >= (pti * dec!(0.01)).round_dp(2),
255 "DTA too small"
256 );
257 assert!(
258 provision.deferred_tax_asset <= (pti * dec!(0.08)).round_dp(2),
259 "DTA too large"
260 );
261
262 assert!(
264 provision.deferred_tax_liability >= (pti * dec!(0.01)).round_dp(2),
265 "DTL too small"
266 );
267 assert!(
268 provision.deferred_tax_liability <= (pti * dec!(0.06)).round_dp(2),
269 "DTL too large"
270 );
271 }
272
273 #[test]
274 fn test_deterministic() {
275 let mut gen1 = TaxProvisionGenerator::new(999);
276 let p1 = gen1.generate("ENT-001", period_end(), dec!(750000), dec!(0.21));
277
278 let mut gen2 = TaxProvisionGenerator::new(999);
279 let p2 = gen2.generate("ENT-001", period_end(), dec!(750000), dec!(0.21));
280
281 assert_eq!(p1.id, p2.id);
282 assert_eq!(p1.current_tax_expense, p2.current_tax_expense);
283 assert_eq!(p1.effective_rate, p2.effective_rate);
284 assert_eq!(p1.statutory_rate, p2.statutory_rate);
285 assert_eq!(p1.deferred_tax_asset, p2.deferred_tax_asset);
286 assert_eq!(p1.deferred_tax_liability, p2.deferred_tax_liability);
287 assert_eq!(p1.rate_reconciliation.len(), p2.rate_reconciliation.len());
288 for (r1, r2) in p1
289 .rate_reconciliation
290 .iter()
291 .zip(p2.rate_reconciliation.iter())
292 {
293 assert_eq!(r1.description, r2.description);
294 assert_eq!(r1.rate_impact, r2.rate_impact);
295 }
296 }
297}