Skip to main content

datasynth_generators/compliance/
filing_generator.rs

1//! Regulatory filing generator.
2//!
3//! Generates regulatory filing records for each company and jurisdiction,
4//! with status progression and deadline tracking.
5
6use chrono::{Datelike, Duration, NaiveDate};
7use rand::Rng;
8use rand_chacha::ChaCha8Rng;
9
10use datasynth_core::models::compliance::{FilingFrequency, FilingType, RegulatoryFiling};
11use datasynth_core::utils::seeded_rng;
12
13/// Configuration for filing generation.
14#[derive(Debug, Clone)]
15pub struct FilingGeneratorConfig {
16    /// Filing types to include (empty = all applicable).
17    pub filing_types: Vec<String>,
18    /// Whether to generate status progression.
19    pub generate_status_progression: bool,
20}
21
22impl Default for FilingGeneratorConfig {
23    fn default() -> Self {
24        Self {
25            filing_types: Vec::new(),
26            generate_status_progression: true,
27        }
28    }
29}
30
31/// Default filing requirements per jurisdiction.
32struct FilingTemplate {
33    filing_type: FilingType,
34    frequency: FilingFrequency,
35    regulator: &'static str,
36    jurisdiction: &'static str,
37    deadline_days: u32,
38}
39
40const FILING_TEMPLATES: &[FilingTemplate] = &[
41    FilingTemplate {
42        filing_type: FilingType::Form10K,
43        frequency: FilingFrequency::Annual,
44        regulator: "SEC",
45        jurisdiction: "US",
46        deadline_days: 60,
47    },
48    FilingTemplate {
49        filing_type: FilingType::Form10Q,
50        frequency: FilingFrequency::Quarterly,
51        regulator: "SEC",
52        jurisdiction: "US",
53        deadline_days: 40,
54    },
55    FilingTemplate {
56        filing_type: FilingType::Jahresabschluss,
57        frequency: FilingFrequency::Annual,
58        regulator: "Bundesanzeiger",
59        jurisdiction: "DE",
60        deadline_days: 365,
61    },
62    FilingTemplate {
63        filing_type: FilingType::EBilanz,
64        frequency: FilingFrequency::Annual,
65        regulator: "Finanzamt",
66        jurisdiction: "DE",
67        deadline_days: 210,
68    },
69    FilingTemplate {
70        filing_type: FilingType::LiasseFiscale,
71        frequency: FilingFrequency::Annual,
72        regulator: "DGFiP",
73        jurisdiction: "FR",
74        deadline_days: 120,
75    },
76    FilingTemplate {
77        filing_type: FilingType::UkAnnualReturn,
78        frequency: FilingFrequency::Annual,
79        regulator: "Companies House",
80        jurisdiction: "GB",
81        deadline_days: 270,
82    },
83    FilingTemplate {
84        filing_type: FilingType::Ct600,
85        frequency: FilingFrequency::Annual,
86        regulator: "HMRC",
87        jurisdiction: "GB",
88        deadline_days: 365,
89    },
90    FilingTemplate {
91        filing_type: FilingType::YukaShokenHokokusho,
92        frequency: FilingFrequency::Annual,
93        regulator: "FSA",
94        jurisdiction: "JP",
95        deadline_days: 90,
96    },
97];
98
99/// Generator for regulatory filing records.
100pub struct FilingGenerator {
101    rng: ChaCha8Rng,
102    config: FilingGeneratorConfig,
103}
104
105impl FilingGenerator {
106    /// Creates a new generator.
107    pub fn new(seed: u64) -> Self {
108        Self {
109            rng: seeded_rng(seed, 0),
110            config: FilingGeneratorConfig::default(),
111        }
112    }
113
114    /// Creates a generator with custom configuration.
115    pub fn with_config(seed: u64, config: FilingGeneratorConfig) -> Self {
116        Self {
117            rng: seeded_rng(seed, 0),
118            config,
119        }
120    }
121
122    /// Generates filing records for companies in specified jurisdictions.
123    pub fn generate_filings(
124        &mut self,
125        company_codes: &[String],
126        jurisdictions: &[String],
127        start_date: NaiveDate,
128        period_months: u32,
129    ) -> Vec<RegulatoryFiling> {
130        let mut filings = Vec::new();
131
132        for company_code in company_codes {
133            for jurisdiction in jurisdictions {
134                let templates: Vec<&FilingTemplate> = FILING_TEMPLATES
135                    .iter()
136                    .filter(|t| t.jurisdiction == jurisdiction)
137                    .filter(|t| {
138                        self.config.filing_types.is_empty()
139                            || self
140                                .config
141                                .filing_types
142                                .iter()
143                                .any(|ft| format!("{}", t.filing_type) == *ft)
144                    })
145                    .collect();
146
147                for template in &templates {
148                    let period_ends =
149                        self.compute_period_ends(template.frequency, start_date, period_months);
150
151                    for period_end in period_ends {
152                        let deadline = period_end + Duration::days(template.deadline_days as i64);
153
154                        let mut filing = RegulatoryFiling::new(
155                            template.filing_type.clone(),
156                            company_code.as_str(),
157                            jurisdiction.as_str(),
158                            period_end,
159                            deadline,
160                            template.regulator,
161                        );
162
163                        if self.config.generate_status_progression {
164                            // Simulate filing date
165                            let days_before_deadline =
166                                self.rng.random_range(1i64..template.deadline_days as i64);
167                            let filing_date = deadline - Duration::days(days_before_deadline);
168
169                            // Small chance of late filing
170                            let filing_date = if self.rng.random::<f64>() < 0.05 {
171                                deadline + Duration::days(self.rng.random_range(1i64..30i64))
172                            } else {
173                                filing_date
174                            };
175
176                            filing = filing.filed_on(filing_date);
177                            filing.filing_reference = Some(format!(
178                                "{}-{}-{}-{}",
179                                jurisdiction,
180                                company_code,
181                                period_end.format("%Y"),
182                                template.filing_type
183                            ));
184                        }
185
186                        filings.push(filing);
187                    }
188                }
189            }
190        }
191
192        filings
193    }
194
195    fn compute_period_ends(
196        &self,
197        frequency: FilingFrequency,
198        start_date: NaiveDate,
199        period_months: u32,
200    ) -> Vec<NaiveDate> {
201        let mut ends = Vec::new();
202        let interval_months: u32 = match frequency {
203            FilingFrequency::Annual => 12,
204            FilingFrequency::SemiAnnual => 6,
205            FilingFrequency::Quarterly => 3,
206            FilingFrequency::Monthly => 1,
207            FilingFrequency::EventDriven => return ends,
208        };
209
210        let mut current_month = start_date.month();
211        let mut current_year = start_date.year();
212        let mut months_elapsed = 0u32;
213
214        while months_elapsed < period_months {
215            // Advance by interval
216            months_elapsed += interval_months;
217            if months_elapsed > period_months {
218                break;
219            }
220
221            current_month += interval_months;
222            while current_month > 12 {
223                current_month -= 12;
224                current_year += 1;
225            }
226
227            // Period end is last day of the month
228            let next_month = if current_month == 12 {
229                NaiveDate::from_ymd_opt(current_year + 1, 1, 1)
230            } else {
231                NaiveDate::from_ymd_opt(current_year, current_month + 1, 1)
232            };
233            if let Some(nm) = next_month {
234                let period_end = nm - Duration::days(1);
235                ends.push(period_end);
236            }
237        }
238
239        ends
240    }
241}
242
243#[cfg(test)]
244#[allow(clippy::unwrap_used)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_generate_us_filings() {
250        let mut gen = FilingGenerator::new(42);
251        let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
252        let filings = gen.generate_filings(&["C001".to_string()], &["US".to_string()], start, 12);
253        // Should have 10-K (1 annual) + 10-Q (4 quarterly, but 3 within 12 months after offset)
254        assert!(!filings.is_empty(), "Should generate US filings");
255
256        for f in &filings {
257            assert_eq!(f.company_code, "C001");
258            assert_eq!(f.jurisdiction, "US");
259        }
260    }
261
262    #[test]
263    fn test_generate_multi_jurisdiction_filings() {
264        let mut gen = FilingGenerator::new(42);
265        let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
266        let filings = gen.generate_filings(
267            &["C001".to_string()],
268            &["US".to_string(), "DE".to_string(), "GB".to_string()],
269            start,
270            12,
271        );
272        assert!(!filings.is_empty());
273
274        let jurisdictions: std::collections::HashSet<&str> =
275            filings.iter().map(|f| f.jurisdiction.as_str()).collect();
276        assert!(jurisdictions.contains("US"));
277        assert!(jurisdictions.contains("DE"));
278        assert!(jurisdictions.contains("GB"));
279    }
280}