1use chrono::NaiveDate;
7use rand::Rng;
8use rand_chacha::ChaCha8Rng;
9use serde::Serialize;
10
11use datasynth_core::models::compliance::StandardCategory;
12use datasynth_core::utils::seeded_rng;
13use datasynth_standards::registry::StandardRegistry;
14
15#[derive(Debug, Clone, Serialize)]
17pub struct ProcedureStep {
18 pub step_number: u32,
19 pub step_type: String,
20 pub description: String,
21 pub assertion: String,
22}
23
24#[derive(Debug, Clone, Serialize)]
26pub struct AuditProcedureRecord {
27 pub procedure_id: String,
28 pub standard_id: String,
29 pub procedure_type: String,
30 pub title: String,
31 pub description: String,
32 pub sampling_method: String,
33 pub sample_size: u32,
34 pub confidence_level: f64,
35 pub tolerable_misstatement: f64,
36 pub assertions_tested: Vec<String>,
37 pub jurisdiction: String,
38 pub reference_date: String,
39 pub steps: Vec<ProcedureStep>,
40}
41
42#[derive(Debug, Clone)]
44pub struct ProcedureGeneratorConfig {
45 pub procedures_per_standard: usize,
46 pub sampling_method: String,
47 pub confidence_level: f64,
48 pub tolerable_misstatement: f64,
49}
50
51impl Default for ProcedureGeneratorConfig {
52 fn default() -> Self {
53 Self {
54 procedures_per_standard: 3,
55 sampling_method: "statistical".to_string(),
56 confidence_level: 0.95,
57 tolerable_misstatement: 0.05,
58 }
59 }
60}
61
62const PROCEDURE_TEMPLATES: &[(&str, &str, &[&str])] = &[
64 (
65 "substantive_detail",
66 "Test of Details",
67 &["Occurrence", "Completeness", "Accuracy"],
68 ),
69 (
70 "analytical",
71 "Analytical Procedure",
72 &["Accuracy", "ValuationAndAllocation", "Completeness"],
73 ),
74 (
75 "controls_test",
76 "Test of Operating Effectiveness",
77 &["Occurrence", "Cutoff", "Classification"],
78 ),
79 (
80 "inspection",
81 "Inspection of Records/Documents",
82 &[
83 "Existence",
84 "RightsAndObligations",
85 "ValuationAndAllocation",
86 ],
87 ),
88 (
89 "confirmation",
90 "External Confirmation",
91 &["Existence", "CompletenessBalance", "RightsAndObligations"],
92 ),
93 (
94 "recalculation",
95 "Recalculation",
96 &["Accuracy", "ValuationAndAllocation"],
97 ),
98 (
99 "observation",
100 "Observation of Process",
101 &["Occurrence", "Completeness"],
102 ),
103 (
104 "inquiry",
105 "Inquiry of Management",
106 &["CompletenessDisclosure", "AccuracyAndValuation"],
107 ),
108 (
109 "cutoff_test",
110 "Cutoff Testing",
111 &["Cutoff", "Occurrence", "Completeness"],
112 ),
113];
114
115pub struct ProcedureGenerator {
117 rng: ChaCha8Rng,
118 config: ProcedureGeneratorConfig,
119 counter: u32,
120}
121
122impl ProcedureGenerator {
123 pub fn new(seed: u64) -> Self {
125 Self {
126 rng: seeded_rng(seed, 0),
127 config: ProcedureGeneratorConfig::default(),
128 counter: 0,
129 }
130 }
131
132 pub fn with_config(seed: u64, config: ProcedureGeneratorConfig) -> Self {
134 Self {
135 rng: seeded_rng(seed, 0),
136 config,
137 counter: 0,
138 }
139 }
140
141 pub fn generate_procedures(
143 &mut self,
144 registry: &StandardRegistry,
145 jurisdiction: &str,
146 reference_date: NaiveDate,
147 ) -> Vec<AuditProcedureRecord> {
148 let standards = registry.standards_for_jurisdiction(jurisdiction, reference_date);
149 let mut procedures = Vec::new();
150
151 for std in &standards {
152 let is_audit = matches!(
154 std.category,
155 StandardCategory::AuditingStandard | StandardCategory::RegulatoryRequirement
156 );
157 if !is_audit {
158 continue;
159 }
160
161 let count = self
162 .config
163 .procedures_per_standard
164 .min(PROCEDURE_TEMPLATES.len());
165 for i in 0..count {
166 let template_idx = (self.counter as usize + i) % PROCEDURE_TEMPLATES.len();
167 let (proc_type, title, assertions) = PROCEDURE_TEMPLATES[template_idx];
168
169 self.counter += 1;
170 let procedure_id = format!("PROC-{:05}", self.counter);
171
172 let sample_size = self.compute_sample_size();
173
174 let steps = self.generate_steps(proc_type, assertions);
175
176 procedures.push(AuditProcedureRecord {
177 procedure_id,
178 standard_id: std.id.as_str().to_string(),
179 procedure_type: proc_type.to_string(),
180 title: format!("{} — {}", title, std.title),
181 description: format!(
182 "{} procedure for {} compliance in jurisdiction {}",
183 title, std.id, jurisdiction
184 ),
185 sampling_method: self.config.sampling_method.clone(),
186 sample_size,
187 confidence_level: self.config.confidence_level,
188 tolerable_misstatement: self.config.tolerable_misstatement,
189 assertions_tested: assertions.iter().map(|a| a.to_string()).collect(),
190 jurisdiction: jurisdiction.to_string(),
191 reference_date: reference_date.to_string(),
192 steps,
193 });
194 }
195 }
196
197 procedures
198 }
199
200 fn compute_sample_size(&mut self) -> u32 {
201 let base = if self.config.confidence_level >= 0.95 {
203 58 } else if self.config.confidence_level >= 0.90 {
205 38
206 } else {
207 25
208 };
209
210 let variation = self.rng.random_range(0.8f64..1.2f64);
212 (base as f64 * variation) as u32
213 }
214
215 fn generate_steps(&self, proc_type: &str, assertions: &[&str]) -> Vec<ProcedureStep> {
216 let base_steps: &[(&str, &str)] = match proc_type {
217 "substantive_detail" => &[
218 (
219 "selection",
220 "Select sample from population using statistical sampling",
221 ),
222 (
223 "inspection",
224 "Inspect supporting documentation for each item",
225 ),
226 ("verification", "Verify amounts agree to source documents"),
227 (
228 "evaluation",
229 "Evaluate exceptions and project to population",
230 ),
231 ],
232 "analytical" => &[
233 (
234 "expectation",
235 "Develop independent expectation using prior-year data and trends",
236 ),
237 ("comparison", "Compare recorded amounts to expectation"),
238 (
239 "investigation",
240 "Investigate significant variances exceeding threshold",
241 ),
242 (
243 "conclusion",
244 "Form conclusion on reasonableness of recorded amounts",
245 ),
246 ],
247 "controls_test" => &[
248 (
249 "selection",
250 "Select sample of transactions processed during the period",
251 ),
252 ("inspection", "Inspect evidence of control operation"),
253 ("reperformance", "Reperform the control procedure"),
254 (
255 "evaluation",
256 "Evaluate control exceptions and determine impact",
257 ),
258 ],
259 "confirmation" => &[
260 ("selection", "Select accounts for external confirmation"),
261 ("dispatch", "Send confirmation requests to third parties"),
262 ("receipt", "Receive and evaluate confirmation responses"),
263 (
264 "alternative",
265 "Perform alternative procedures for non-responses",
266 ),
267 ],
268 "cutoff_test" => &[
269 ("selection", "Select transactions around period end"),
270 ("inspection", "Inspect dates on source documents"),
271 ("verification", "Verify recording in correct period"),
272 ("evaluation", "Evaluate cutoff exceptions"),
273 ],
274 _ => &[
275 ("planning", "Plan the procedure scope and approach"),
276 ("execution", "Execute the procedure steps"),
277 ("evaluation", "Evaluate results and form conclusion"),
278 ],
279 };
280
281 base_steps
282 .iter()
283 .enumerate()
284 .map(|(i, (step_type, desc))| {
285 let assertion = if i < assertions.len() {
286 assertions[i].to_string()
287 } else {
288 assertions[0].to_string()
289 };
290
291 ProcedureStep {
292 step_number: (i + 1) as u32,
293 step_type: step_type.to_string(),
294 description: desc.to_string(),
295 assertion,
296 }
297 })
298 .collect()
299 }
300}
301
302#[cfg(test)]
303#[allow(clippy::unwrap_used)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_generate_procedures() {
309 let registry = StandardRegistry::with_built_in();
310 let mut gen = ProcedureGenerator::new(42);
311 let date = NaiveDate::from_ymd_opt(2025, 6, 30).unwrap();
312 let procedures = gen.generate_procedures(®istry, "US", date);
313 assert!(
314 !procedures.is_empty(),
315 "Should generate procedures for US standards"
316 );
317
318 for proc in &procedures {
320 assert!(!proc.steps.is_empty());
321 assert!(!proc.assertions_tested.is_empty());
322 }
323 }
324}