datasynth_generators/compliance/
filing_generator.rs1use 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#[derive(Debug, Clone)]
15pub struct FilingGeneratorConfig {
16 pub filing_types: Vec<String>,
18 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
31struct 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
99pub struct FilingGenerator {
101 rng: ChaCha8Rng,
102 config: FilingGeneratorConfig,
103}
104
105impl FilingGenerator {
106 pub fn new(seed: u64) -> Self {
108 Self {
109 rng: seeded_rng(seed, 0),
110 config: FilingGeneratorConfig::default(),
111 }
112 }
113
114 pub fn with_config(seed: u64, config: FilingGeneratorConfig) -> Self {
116 Self {
117 rng: seeded_rng(seed, 0),
118 config,
119 }
120 }
121
122 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 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 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 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 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 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}