Skip to main content

datasynth_generators/tax/
tax_provision_generator.rs

1//! Tax Provision Generator (ASC 740 / IAS 12).
2//!
3//! Computes current and deferred income tax provisions for a reporting period.
4//! Generates rate reconciliation items that bridge from the statutory rate to
5//! the effective rate, and produces realistic deferred tax asset/liability
6//! balances from temporary differences.
7
8use chrono::NaiveDate;
9use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11use rust_decimal::Decimal;
12use rust_decimal_macros::dec;
13
14use datasynth_core::models::TaxProvision;
15
16// ---------------------------------------------------------------------------
17// Rate reconciliation catalogue
18// ---------------------------------------------------------------------------
19
20/// A candidate reconciliation item with its description and rate-impact range.
21struct ReconciliationCandidate {
22    description: &'static str,
23    /// Minimum rate impact (inclusive).
24    min_impact: Decimal,
25    /// Maximum rate impact (inclusive).
26    max_impact: Decimal,
27}
28
29/// Pool of possible rate reconciliation items.
30const CANDIDATES: &[ReconciliationCandidate] = &[
31    ReconciliationCandidate {
32        description: "State and local taxes",
33        min_impact: dec!(0.01),
34        max_impact: dec!(0.04),
35    },
36    ReconciliationCandidate {
37        description: "Permanent differences",
38        min_impact: dec!(-0.01),
39        max_impact: dec!(0.02),
40    },
41    ReconciliationCandidate {
42        description: "R&D tax credits",
43        min_impact: dec!(-0.02),
44        max_impact: dec!(-0.005),
45    },
46    ReconciliationCandidate {
47        description: "Foreign rate differential",
48        min_impact: dec!(-0.03),
49        max_impact: dec!(0.03),
50    },
51    ReconciliationCandidate {
52        description: "Stock compensation",
53        min_impact: dec!(-0.01),
54        max_impact: dec!(0.01),
55    },
56    ReconciliationCandidate {
57        description: "Valuation allowance change",
58        min_impact: dec!(-0.02),
59        max_impact: dec!(0.05),
60    },
61];
62
63// ---------------------------------------------------------------------------
64// Generator
65// ---------------------------------------------------------------------------
66
67/// Generates income tax provisions under ASC 740 / IAS 12.
68///
69/// Given pre-tax income and a statutory rate, the generator:
70/// 1. Selects 2-5 rate reconciliation items from the candidate pool.
71/// 2. Computes the effective rate as `statutory_rate + sum(reconciliation impacts)`.
72/// 3. Computes `current_tax_expense = pre_tax_income * effective_rate`.
73/// 4. Generates realistic deferred tax asset and liability balances.
74pub struct TaxProvisionGenerator {
75    rng: ChaCha8Rng,
76    counter: u64,
77}
78
79impl TaxProvisionGenerator {
80    /// Creates a new tax provision generator with the given deterministic seed.
81    pub fn new(seed: u64) -> Self {
82        Self {
83            rng: ChaCha8Rng::seed_from_u64(seed),
84            counter: 0,
85        }
86    }
87
88    /// Generate a tax provision for a period.
89    ///
90    /// # Arguments
91    ///
92    /// * `entity_id` - Legal entity identifier.
93    /// * `period` - Period end date.
94    /// * `pre_tax_income` - Pre-tax income from financial statements.
95    /// * `statutory_rate` - Statutory corporate tax rate (e.g., `0.21` for US).
96    pub fn generate(
97        &mut self,
98        entity_id: &str,
99        period: NaiveDate,
100        pre_tax_income: Decimal,
101        statutory_rate: Decimal,
102    ) -> TaxProvision {
103        self.counter += 1;
104        let provision_id = format!("TXPROV-{:06}", self.counter);
105
106        // Select 2-5 reconciliation items
107        let num_items = self.rng.gen_range(2..=5);
108        let mut selected_indices: Vec<usize> = (0..CANDIDATES.len()).collect();
109        selected_indices.shuffle(&mut self.rng);
110        selected_indices.truncate(num_items);
111        selected_indices.sort(); // stable ordering for determinism after shuffle
112
113        let mut total_impact = Decimal::ZERO;
114        let mut reconciliation_items: Vec<(&str, Decimal)> = Vec::new();
115
116        for &idx in &selected_indices {
117            let candidate = &CANDIDATES[idx];
118            let impact = self.random_decimal(candidate.min_impact, candidate.max_impact);
119            total_impact += impact;
120            reconciliation_items.push((candidate.description, impact));
121        }
122
123        let effective_rate = (statutory_rate + total_impact).round_dp(6);
124        let current_tax_expense = (pre_tax_income * effective_rate).round_dp(2);
125
126        // Generate deferred tax balances (random realistic amounts)
127        // DTA: typically 1-8% of pre_tax_income (from timing differences, NOL carryforwards)
128        let dta_pct = self.random_decimal(dec!(0.01), dec!(0.08));
129        let deferred_tax_asset = (pre_tax_income.abs() * dta_pct).round_dp(2);
130
131        // DTL: typically 1-6% of pre_tax_income (from depreciation timing, etc.)
132        let dtl_pct = self.random_decimal(dec!(0.01), dec!(0.06));
133        let deferred_tax_liability = (pre_tax_income.abs() * dtl_pct).round_dp(2);
134
135        let mut provision = TaxProvision::new(
136            provision_id,
137            entity_id,
138            period,
139            current_tax_expense,
140            deferred_tax_asset,
141            deferred_tax_liability,
142            statutory_rate,
143            effective_rate,
144        );
145
146        for (desc, impact) in &reconciliation_items {
147            provision = provision.with_reconciliation_item(*desc, *impact);
148        }
149
150        provision
151    }
152
153    /// Generates a random decimal between `min` and `max` (inclusive).
154    fn random_decimal(&mut self, min: Decimal, max: Decimal) -> Decimal {
155        let range_f64 = (max - min).to_string().parse::<f64>().unwrap_or(0.0);
156        let min_f64 = min.to_string().parse::<f64>().unwrap_or(0.0);
157        let val = min_f64 + self.rng.gen::<f64>() * range_f64;
158        Decimal::try_from(val).unwrap_or(min).round_dp(6)
159    }
160}
161
162// ---------------------------------------------------------------------------
163// Tests
164// ---------------------------------------------------------------------------
165
166#[cfg(test)]
167#[allow(clippy::unwrap_used)]
168mod tests {
169    use super::*;
170
171    fn period_end() -> NaiveDate {
172        NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()
173    }
174
175    #[test]
176    fn test_provision_calculation() {
177        let mut gen = TaxProvisionGenerator::new(42);
178        let provision = gen.generate("ENT-001", period_end(), dec!(1000000), dec!(0.21));
179
180        // Effective rate should be statutory_rate + sum of reconciliation impacts
181        let total_impact: Decimal = provision
182            .rate_reconciliation
183            .iter()
184            .map(|r| r.rate_impact)
185            .sum();
186        let expected_effective = (dec!(0.21) + total_impact).round_dp(6);
187        assert_eq!(provision.effective_rate, expected_effective);
188
189        // Current tax expense = pre_tax_income * effective_rate
190        let expected_expense = (dec!(1000000) * provision.effective_rate).round_dp(2);
191        assert_eq!(provision.current_tax_expense, expected_expense);
192
193        // Statutory rate preserved
194        assert_eq!(provision.statutory_rate, dec!(0.21));
195    }
196
197    #[test]
198    fn test_rate_reconciliation() {
199        let mut gen = TaxProvisionGenerator::new(42);
200        let provision = gen.generate("ENT-001", period_end(), dec!(500000), dec!(0.21));
201
202        // Should have 2-5 reconciliation items
203        assert!(
204            provision.rate_reconciliation.len() >= 2,
205            "Should have at least 2 items, got {}",
206            provision.rate_reconciliation.len()
207        );
208        assert!(
209            provision.rate_reconciliation.len() <= 5,
210            "Should have at most 5 items, got {}",
211            provision.rate_reconciliation.len()
212        );
213
214        // Sum of impacts should equal effective_rate - statutory_rate
215        let total_impact: Decimal = provision
216            .rate_reconciliation
217            .iter()
218            .map(|r| r.rate_impact)
219            .sum();
220        let diff = (provision.effective_rate - provision.statutory_rate).round_dp(6);
221        let impact_rounded = total_impact.round_dp(6);
222
223        // Allow small tolerance for floating-point → decimal conversion
224        let tolerance = dec!(0.000002);
225        assert!(
226            (diff - impact_rounded).abs() <= tolerance,
227            "Reconciliation items should sum to effective - statutory: diff={}, impact={}",
228            diff,
229            impact_rounded
230        );
231    }
232
233    #[test]
234    fn test_deferred_tax() {
235        let mut gen = TaxProvisionGenerator::new(42);
236        let provision = gen.generate("ENT-001", period_end(), dec!(2000000), dec!(0.21));
237
238        // Deferred tax asset and liability should both be positive
239        assert!(
240            provision.deferred_tax_asset > Decimal::ZERO,
241            "DTA should be positive: {}",
242            provision.deferred_tax_asset
243        );
244        assert!(
245            provision.deferred_tax_liability > Decimal::ZERO,
246            "DTL should be positive: {}",
247            provision.deferred_tax_liability
248        );
249
250        // DTA should be between 1-8% of pre_tax_income
251        let pti = dec!(2000000);
252        assert!(
253            provision.deferred_tax_asset >= (pti * dec!(0.01)).round_dp(2),
254            "DTA too small"
255        );
256        assert!(
257            provision.deferred_tax_asset <= (pti * dec!(0.08)).round_dp(2),
258            "DTA too large"
259        );
260
261        // DTL should be between 1-6% of pre_tax_income
262        assert!(
263            provision.deferred_tax_liability >= (pti * dec!(0.01)).round_dp(2),
264            "DTL too small"
265        );
266        assert!(
267            provision.deferred_tax_liability <= (pti * dec!(0.06)).round_dp(2),
268            "DTL too large"
269        );
270    }
271
272    #[test]
273    fn test_deterministic() {
274        let mut gen1 = TaxProvisionGenerator::new(999);
275        let p1 = gen1.generate("ENT-001", period_end(), dec!(750000), dec!(0.21));
276
277        let mut gen2 = TaxProvisionGenerator::new(999);
278        let p2 = gen2.generate("ENT-001", period_end(), dec!(750000), dec!(0.21));
279
280        assert_eq!(p1.id, p2.id);
281        assert_eq!(p1.current_tax_expense, p2.current_tax_expense);
282        assert_eq!(p1.effective_rate, p2.effective_rate);
283        assert_eq!(p1.statutory_rate, p2.statutory_rate);
284        assert_eq!(p1.deferred_tax_asset, p2.deferred_tax_asset);
285        assert_eq!(p1.deferred_tax_liability, p2.deferred_tax_liability);
286        assert_eq!(p1.rate_reconciliation.len(), p2.rate_reconciliation.len());
287        for (r1, r2) in p1
288            .rate_reconciliation
289            .iter()
290            .zip(p2.rate_reconciliation.iter())
291        {
292            assert_eq!(r1.description, r2.description);
293            assert_eq!(r1.rate_impact, r2.rate_impact);
294        }
295    }
296}