1use chrono::{Duration, NaiveDate};
7use datasynth_core::utils::seeded_rng;
8use rand::RngExt;
9use rand_chacha::ChaCha8Rng;
10
11use rust_decimal::Decimal;
12
13use datasynth_core::models::audit::{
14 Assertion, AuditEngagement, EngagementPhase, ProcedureType, RiskLevel, SamplingMethod,
15 Workpaper, WorkpaperConclusion, WorkpaperScope, WorkpaperSection, WorkpaperStatus,
16};
17
18#[derive(Debug, Clone)]
20pub struct WorkpaperGeneratorConfig {
21 pub workpapers_per_section: (u32, u32),
23 pub population_size_range: (u64, u64),
25 pub sample_percentage_range: (f64, f64),
27 pub exception_rate_range: (f64, f64),
29 pub unsatisfactory_probability: f64,
31 pub first_review_delay_range: (u32, u32),
33 pub second_review_delay_range: (u32, u32),
35}
36
37impl Default for WorkpaperGeneratorConfig {
38 fn default() -> Self {
39 Self {
40 workpapers_per_section: (3, 10),
41 population_size_range: (100, 10000),
42 sample_percentage_range: (0.01, 0.10),
43 exception_rate_range: (0.0, 0.08),
44 unsatisfactory_probability: 0.05,
45 first_review_delay_range: (1, 5),
46 second_review_delay_range: (1, 3),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Default)]
57pub struct WorkpaperEnrichment {
58 pub account_area: Option<String>,
60 pub account_balance: Option<Decimal>,
62 pub risk_level: Option<String>, pub materiality: Option<Decimal>,
66 pub sampling_info: Option<String>,
68}
69
70pub struct WorkpaperGenerator {
72 rng: ChaCha8Rng,
74 config: WorkpaperGeneratorConfig,
76 section_counters: std::collections::HashMap<WorkpaperSection, u32>,
78}
79
80impl WorkpaperGenerator {
81 pub fn new(seed: u64) -> Self {
83 Self {
84 rng: seeded_rng(seed, 0),
85 config: WorkpaperGeneratorConfig::default(),
86 section_counters: std::collections::HashMap::new(),
87 }
88 }
89
90 pub fn with_config(seed: u64, config: WorkpaperGeneratorConfig) -> Self {
92 Self {
93 rng: seeded_rng(seed, 0),
94 config,
95 section_counters: std::collections::HashMap::new(),
96 }
97 }
98
99 pub fn generate_workpapers_for_phase(
101 &mut self,
102 engagement: &AuditEngagement,
103 phase: EngagementPhase,
104 phase_date: NaiveDate,
105 team_members: &[String],
106 ) -> Vec<Workpaper> {
107 let section = match phase {
108 EngagementPhase::Planning => WorkpaperSection::Planning,
109 EngagementPhase::RiskAssessment => WorkpaperSection::RiskAssessment,
110 EngagementPhase::ControlTesting => WorkpaperSection::ControlTesting,
111 EngagementPhase::SubstantiveTesting => WorkpaperSection::SubstantiveTesting,
112 EngagementPhase::Completion => WorkpaperSection::Completion,
113 EngagementPhase::Reporting => WorkpaperSection::Reporting,
114 };
115
116 let count = self.rng.random_range(
117 self.config.workpapers_per_section.0..=self.config.workpapers_per_section.1,
118 );
119
120 (0..count)
121 .map(|_| self.generate_workpaper(engagement, section, phase_date, team_members))
122 .collect()
123 }
124
125 pub fn generate_workpaper(
127 &mut self,
128 engagement: &AuditEngagement,
129 section: WorkpaperSection,
130 base_date: NaiveDate,
131 team_members: &[String],
132 ) -> Workpaper {
133 let counter = self.section_counters.entry(section).or_insert(0);
134 *counter += 1;
135
136 let workpaper_ref = format!("{}-{:03}", section.reference_prefix(), counter);
137 let title = self.generate_workpaper_title(section);
138
139 let mut wp = Workpaper::new(engagement.engagement_id, &workpaper_ref, &title, section);
140
141 let (objective, assertions) = self.generate_objective_and_assertions(section);
143 wp = wp.with_objective(&objective, assertions);
144
145 let (procedure, procedure_type) = self.generate_procedure(section);
147 wp = wp.with_procedure(&procedure, procedure_type);
148
149 let (scope, population, sample, method) = self.generate_scope_and_sampling(section);
151 wp = wp.with_scope(scope, population, sample, method);
152
153 let (summary, exceptions, conclusion) =
155 self.generate_results(sample, &engagement.overall_audit_risk);
156 wp = wp.with_results(&summary, exceptions, conclusion);
157
158 wp.risk_level_addressed = engagement.overall_audit_risk;
159
160 let preparer = self.select_team_member(team_members, "staff");
162 let preparer_name = self.generate_auditor_name();
163 wp = wp.with_preparer(&preparer, &preparer_name, base_date);
164
165 let first_review_delay = self.rng.random_range(
167 self.config.first_review_delay_range.0..=self.config.first_review_delay_range.1,
168 );
169 let first_review_date = base_date + Duration::days(first_review_delay as i64);
170 let reviewer = self.select_team_member(team_members, "senior");
171 let reviewer_name = self.generate_auditor_name();
172 wp.add_first_review(&reviewer, &reviewer_name, first_review_date);
173
174 if self.rng.random::<f64>() < 0.7 {
176 let second_review_delay = self.rng.random_range(
177 self.config.second_review_delay_range.0..=self.config.second_review_delay_range.1,
178 );
179 let second_review_date = first_review_date + Duration::days(second_review_delay as i64);
180 let second_reviewer = self.select_team_member(team_members, "manager");
181 let second_reviewer_name = self.generate_auditor_name();
182 wp.add_second_review(&second_reviewer, &second_reviewer_name, second_review_date);
183 } else {
184 wp.status = WorkpaperStatus::FirstReviewComplete;
185 }
186
187 if self.rng.random::<f64>() < 0.30 {
189 let note = self.generate_review_note();
190 wp.add_review_note(&reviewer, ¬e);
191 }
192
193 wp
194 }
195
196 pub fn generate_workpaper_with_context(
202 &mut self,
203 engagement: &AuditEngagement,
204 section: WorkpaperSection,
205 base_date: NaiveDate,
206 team_members: &[String],
207 enrichment: &WorkpaperEnrichment,
208 ) -> Workpaper {
209 let mut wp = self.generate_workpaper(engagement, section, base_date, team_members);
210
211 if let (Some(area), Some(risk)) = (&enrichment.account_area, &enrichment.risk_level) {
213 wp.title = format!("{} \u{2014} {} Risk", area, risk);
214 } else if let Some(area) = &enrichment.account_area {
215 wp.title = format!("{} {}", area, wp.title);
216 }
217
218 let mut addenda = Vec::new();
220 if let Some(balance) = enrichment.account_balance {
221 addenda.push(format!("GL Balance: ${}", balance));
222 }
223 if let Some(mat) = enrichment.materiality {
224 addenda.push(format!("Performance materiality: ${}", mat));
225 }
226 if !addenda.is_empty() {
227 wp.objective = format!("{} | {}", wp.objective, addenda.join(". "));
228 }
229
230 if let Some(sampling) = &enrichment.sampling_info {
232 wp.procedure_performed = format!("{} | Sample: {}", wp.procedure_performed, sampling);
233 }
234
235 if let Some(risk) = &enrichment.risk_level {
237 let (lo, hi) = match risk.as_str() {
238 "High" => (95.0, 100.0),
239 "Moderate" => (75.0, 90.0),
240 "Low" | "Minimal" => (50.0, 70.0),
241 _ => (70.0, 100.0),
242 };
243 wp.scope.coverage_percentage = self.rng.random_range(lo..hi);
244 }
245
246 wp
247 }
248
249 fn generate_workpaper_title(&mut self, section: WorkpaperSection) -> String {
251 let titles = match section {
252 WorkpaperSection::Planning => vec![
253 "Engagement Planning Memo",
254 "Understanding the Entity and Environment",
255 "Materiality Assessment",
256 "Preliminary Analytical Procedures",
257 "Risk Assessment Summary",
258 "Audit Strategy and Approach",
259 "Staffing and Resource Plan",
260 "Client Acceptance Procedures",
261 ],
262 WorkpaperSection::RiskAssessment => vec![
263 "Business Risk Assessment",
264 "Fraud Risk Evaluation",
265 "IT General Controls Assessment",
266 "Internal Control Evaluation",
267 "Significant Account Identification",
268 "Risk of Material Misstatement Assessment",
269 "Related Party Risk Assessment",
270 "Going Concern Assessment",
271 ],
272 WorkpaperSection::ControlTesting => vec![
273 "Revenue Recognition Controls Testing",
274 "Purchase and Payables Controls Testing",
275 "Treasury and Cash Controls Testing",
276 "Payroll Controls Testing",
277 "Fixed Asset Controls Testing",
278 "Inventory Controls Testing",
279 "IT Application Controls Testing",
280 "Entity Level Controls Testing",
281 ],
282 WorkpaperSection::SubstantiveTesting => vec![
283 "Revenue Cutoff Testing",
284 "Accounts Receivable Confirmation",
285 "Inventory Observation and Testing",
286 "Fixed Asset Verification",
287 "Accounts Payable Completeness",
288 "Expense Testing",
289 "Debt and Interest Testing",
290 "Bank Reconciliation Review",
291 "Journal Entry Testing",
292 "Analytical Procedures - Revenue",
293 "Analytical Procedures - Expenses",
294 ],
295 WorkpaperSection::Completion => vec![
296 "Subsequent Events Review",
297 "Management Representation Letter",
298 "Attorney Letter Summary",
299 "Going Concern Evaluation",
300 "Summary of Uncorrected Misstatements",
301 "Summary of Audit Differences",
302 "Completion Checklist",
303 ],
304 WorkpaperSection::Reporting => vec![
305 "Draft Financial Statements Review",
306 "Disclosure Checklist",
307 "Communication with Those Charged with Governance",
308 "Report Issuance Checklist",
309 ],
310 WorkpaperSection::PermanentFile => vec![
311 "Chart of Accounts",
312 "Organization Structure",
313 "Key Contracts Summary",
314 "Related Party Identification",
315 ],
316 };
317
318 let idx = self.rng.random_range(0..titles.len());
319 titles[idx].to_string()
320 }
321
322 fn generate_objective_and_assertions(
324 &mut self,
325 section: WorkpaperSection,
326 ) -> (String, Vec<Assertion>) {
327 match section {
328 WorkpaperSection::Planning | WorkpaperSection::RiskAssessment => (
329 "Understand the entity and assess risks of material misstatement".into(),
330 vec![],
331 ),
332 WorkpaperSection::ControlTesting => (
333 "Test the operating effectiveness of key controls".into(),
334 Assertion::transaction_assertions(),
335 ),
336 WorkpaperSection::SubstantiveTesting => {
337 let assertions = if self.rng.random::<f64>() < 0.5 {
338 Assertion::transaction_assertions()
339 } else {
340 Assertion::balance_assertions()
341 };
342 (
343 "Obtain sufficient appropriate audit evidence regarding account balances"
344 .into(),
345 assertions,
346 )
347 }
348 WorkpaperSection::Completion => (
349 "Complete all required completion procedures".into(),
350 vec![
351 Assertion::Completeness,
352 Assertion::PresentationAndDisclosure,
353 ],
354 ),
355 WorkpaperSection::Reporting => (
356 "Ensure compliance with reporting requirements".into(),
357 vec![Assertion::PresentationAndDisclosure],
358 ),
359 WorkpaperSection::PermanentFile => {
360 ("Maintain permanent file documentation".into(), vec![])
361 }
362 }
363 }
364
365 fn generate_procedure(&mut self, section: WorkpaperSection) -> (String, ProcedureType) {
367 match section {
368 WorkpaperSection::Planning | WorkpaperSection::RiskAssessment => (
369 "Performed inquiries and reviewed documentation".into(),
370 ProcedureType::InquiryObservation,
371 ),
372 WorkpaperSection::ControlTesting => {
373 let procedures = [
374 (
375 "Selected a sample of transactions and tested the control operation",
376 ProcedureType::TestOfControls,
377 ),
378 (
379 "Observed the control being performed by personnel",
380 ProcedureType::InquiryObservation,
381 ),
382 (
383 "Inspected documentation of control performance",
384 ProcedureType::Inspection,
385 ),
386 (
387 "Reperformed the control procedure",
388 ProcedureType::Reperformance,
389 ),
390 ];
391 let idx = self.rng.random_range(0..procedures.len());
392 (procedures[idx].0.into(), procedures[idx].1)
393 }
394 WorkpaperSection::SubstantiveTesting => {
395 let procedures = [
396 (
397 "Selected a sample and agreed details to supporting documentation",
398 ProcedureType::SubstantiveTest,
399 ),
400 (
401 "Sent confirmations and agreed responses to records",
402 ProcedureType::Confirmation,
403 ),
404 (
405 "Recalculated amounts and agreed to supporting schedules",
406 ProcedureType::Recalculation,
407 ),
408 (
409 "Performed analytical procedures and investigated variances",
410 ProcedureType::AnalyticalProcedures,
411 ),
412 (
413 "Inspected physical assets and documentation",
414 ProcedureType::Inspection,
415 ),
416 ];
417 let idx = self.rng.random_range(0..procedures.len());
418 (procedures[idx].0.into(), procedures[idx].1)
419 }
420 WorkpaperSection::Completion | WorkpaperSection::Reporting => (
421 "Reviewed documentation and performed inquiries".into(),
422 ProcedureType::InquiryObservation,
423 ),
424 WorkpaperSection::PermanentFile => (
425 "Compiled and organized permanent file documentation".into(),
426 ProcedureType::Inspection,
427 ),
428 }
429 }
430
431 fn generate_scope_and_sampling(
433 &mut self,
434 section: WorkpaperSection,
435 ) -> (WorkpaperScope, u64, u32, SamplingMethod) {
436 let scope = WorkpaperScope {
437 coverage_percentage: self.rng.random_range(70.0..100.0),
438 period_start: None,
439 period_end: None,
440 limitations: Vec::new(),
441 };
442
443 match section {
444 WorkpaperSection::ControlTesting | WorkpaperSection::SubstantiveTesting => {
445 let population = self.rng.random_range(
446 self.config.population_size_range.0..=self.config.population_size_range.1,
447 );
448 let sample_pct = self.rng.random_range(
449 self.config.sample_percentage_range.0..=self.config.sample_percentage_range.1,
450 );
451 let sample = ((population as f64 * sample_pct).max(25.0) as u32).min(200);
452
453 let method = if self.rng.random::<f64>() < 0.4 {
454 SamplingMethod::StatisticalRandom
455 } else if self.rng.random::<f64>() < 0.3 {
456 SamplingMethod::MonetaryUnit
457 } else {
458 SamplingMethod::Judgmental
459 };
460
461 (scope, population, sample, method)
462 }
463 _ => (scope, 0, 0, SamplingMethod::Judgmental),
464 }
465 }
466
467 fn generate_results(
469 &mut self,
470 sample_size: u32,
471 risk_level: &RiskLevel,
472 ) -> (String, u32, WorkpaperConclusion) {
473 if sample_size == 0 {
474 return (
475 "Procedures completed without exception".into(),
476 0,
477 WorkpaperConclusion::Satisfactory,
478 );
479 }
480
481 let exception_probability = match risk_level {
483 RiskLevel::Low => 0.10,
484 RiskLevel::Medium => 0.25,
485 RiskLevel::High | RiskLevel::Significant => 0.40,
486 };
487
488 let has_exceptions = self.rng.random::<f64>() < exception_probability;
489
490 let (exceptions, conclusion) = if has_exceptions {
491 let exception_rate = self.rng.random_range(
492 self.config.exception_rate_range.0..=self.config.exception_rate_range.1,
493 );
494 let exceptions =
495 ((sample_size as f64 * exception_rate).max(1.0) as u32).min(sample_size);
496
497 let conclusion = if self.rng.random::<f64>() < self.config.unsatisfactory_probability {
498 WorkpaperConclusion::Unsatisfactory
499 } else {
500 WorkpaperConclusion::SatisfactoryWithExceptions
501 };
502
503 (exceptions, conclusion)
504 } else {
505 (0, WorkpaperConclusion::Satisfactory)
506 };
507
508 let summary = match conclusion {
509 WorkpaperConclusion::Satisfactory => {
510 format!("Tested {sample_size} items with no exceptions noted")
511 }
512 WorkpaperConclusion::SatisfactoryWithExceptions => {
513 format!(
514 "Tested {sample_size} items with {exceptions} exceptions noted. Exceptions were immaterial and have been evaluated"
515 )
516 }
517 WorkpaperConclusion::Unsatisfactory => {
518 format!(
519 "Tested {sample_size} items with {exceptions} exceptions noted. Exceptions represent material misstatement requiring adjustment"
520 )
521 }
522 _ => format!("Tested {sample_size} items"),
523 };
524
525 (summary, exceptions, conclusion)
526 }
527
528 fn select_team_member(&mut self, team_members: &[String], role_hint: &str) -> String {
530 let matching: Vec<&String> = team_members
531 .iter()
532 .filter(|m| m.to_lowercase().contains(role_hint))
533 .collect();
534
535 if let Some(&member) = matching.first() {
536 member.clone()
537 } else if !team_members.is_empty() {
538 let idx = self.rng.random_range(0..team_members.len());
539 team_members[idx].clone()
540 } else {
541 format!("{}001", role_hint.to_uppercase())
542 }
543 }
544
545 fn generate_auditor_name(&mut self) -> String {
547 let first_names = [
548 "Michael", "Sarah", "David", "Jennifer", "Robert", "Emily", "James", "Amanda",
549 ];
550 let last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Davis"];
551
552 let first_idx = self.rng.random_range(0..first_names.len());
553 let last_idx = self.rng.random_range(0..last_names.len());
554
555 format!("{} {}", first_names[first_idx], last_names[last_idx])
556 }
557
558 fn generate_review_note(&mut self) -> String {
560 let notes = [
561 "Please expand on the rationale for sample selection",
562 "Cross-reference needed to risk assessment workpaper",
563 "Please document conclusion more clearly",
564 "Need to include population definition",
565 "Please add reference to prior year workpaper",
566 "Document discussion with management regarding exceptions",
567 "Clarify testing approach for this control",
568 "Add evidence reference for supporting documentation",
569 ];
570
571 let idx = self.rng.random_range(0..notes.len());
572 notes[idx].to_string()
573 }
574
575 pub fn generate_complete_workpaper_set(
577 &mut self,
578 engagement: &AuditEngagement,
579 team_members: &[String],
580 ) -> Vec<Workpaper> {
581 let mut all_workpapers = Vec::new();
582
583 all_workpapers.extend(self.generate_workpapers_for_phase(
585 engagement,
586 EngagementPhase::Planning,
587 engagement.planning_start,
588 team_members,
589 ));
590
591 all_workpapers.extend(self.generate_workpapers_for_phase(
593 engagement,
594 EngagementPhase::RiskAssessment,
595 engagement.planning_end,
596 team_members,
597 ));
598
599 all_workpapers.extend(self.generate_workpapers_for_phase(
601 engagement,
602 EngagementPhase::ControlTesting,
603 engagement.fieldwork_start,
604 team_members,
605 ));
606
607 all_workpapers.extend(self.generate_workpapers_for_phase(
609 engagement,
610 EngagementPhase::SubstantiveTesting,
611 engagement.fieldwork_start + Duration::days(14),
612 team_members,
613 ));
614
615 all_workpapers.extend(self.generate_workpapers_for_phase(
617 engagement,
618 EngagementPhase::Completion,
619 engagement.completion_start,
620 team_members,
621 ));
622
623 all_workpapers.extend(self.generate_workpapers_for_phase(
625 engagement,
626 EngagementPhase::Reporting,
627 engagement.report_date - Duration::days(7),
628 team_members,
629 ));
630
631 all_workpapers
632 }
633}
634
635#[cfg(test)]
636#[allow(clippy::unwrap_used)]
637mod tests {
638 use super::*;
639 use crate::audit::test_helpers::create_test_engagement;
640
641 #[test]
642 fn test_workpaper_generation() {
643 let mut generator = WorkpaperGenerator::new(42);
644 let engagement = create_test_engagement();
645 let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
646
647 let wp = generator.generate_workpaper(
648 &engagement,
649 WorkpaperSection::SubstantiveTesting,
650 NaiveDate::from_ymd_opt(2026, 1, 15).unwrap(),
651 &team,
652 );
653
654 assert!(!wp.workpaper_ref.is_empty());
655 assert!(!wp.title.is_empty());
656 assert!(!wp.preparer_id.is_empty());
657 }
658
659 #[test]
660 fn test_phase_workpapers() {
661 let mut generator = WorkpaperGenerator::new(42);
662 let engagement = create_test_engagement();
663 let team = vec!["STAFF001".into(), "SENIOR001".into()];
664
665 let workpapers = generator.generate_workpapers_for_phase(
666 &engagement,
667 EngagementPhase::ControlTesting,
668 NaiveDate::from_ymd_opt(2026, 1, 15).unwrap(),
669 &team,
670 );
671
672 assert!(!workpapers.is_empty());
673 for wp in &workpapers {
674 assert_eq!(wp.section, WorkpaperSection::ControlTesting);
675 }
676 }
677
678 #[test]
679 fn test_complete_workpaper_set() {
680 let mut generator = WorkpaperGenerator::new(42);
681 let engagement = create_test_engagement();
682 let team = vec![
683 "STAFF001".into(),
684 "STAFF002".into(),
685 "SENIOR001".into(),
686 "MANAGER001".into(),
687 ];
688
689 let workpapers = generator.generate_complete_workpaper_set(&engagement, &team);
690
691 assert!(workpapers.len() >= 18); let sections: std::collections::HashSet<_> = workpapers.iter().map(|w| w.section).collect();
696 assert!(sections.len() >= 5);
697 }
698
699 #[test]
700 fn test_workpaper_with_context_enriches_title_and_objective() {
701 let mut generator = WorkpaperGenerator::new(42);
702 let engagement = create_test_engagement();
703 let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
704
705 let enrichment = WorkpaperEnrichment {
706 account_area: Some("Revenue".into()),
707 account_balance: Some(Decimal::new(5_000_000, 0)),
708 risk_level: Some("High".into()),
709 materiality: Some(Decimal::new(325_000, 0)),
710 sampling_info: Some("MUS \u{2014} Population: 1200, Sample: 45".into()),
711 };
712
713 let wp = generator.generate_workpaper_with_context(
714 &engagement,
715 WorkpaperSection::SubstantiveTesting,
716 NaiveDate::from_ymd_opt(2026, 1, 15).unwrap(),
717 &team,
718 &enrichment,
719 );
720
721 assert!(
722 wp.title.contains("Revenue") && wp.title.contains("High"),
723 "Title should contain account area and risk: {}",
724 wp.title
725 );
726 assert!(
727 wp.objective.contains("GL Balance"),
728 "Objective should contain GL balance: {}",
729 wp.objective
730 );
731 assert!(
732 wp.objective.contains("materiality"),
733 "Objective should contain materiality: {}",
734 wp.objective
735 );
736 assert!(
737 wp.procedure_performed.contains("Sample: MUS"),
738 "Procedure should contain sampling info: {}",
739 wp.procedure_performed
740 );
741 assert!(
743 wp.scope.coverage_percentage >= 95.0,
744 "High risk scope should be >=95%: {}",
745 wp.scope.coverage_percentage
746 );
747 }
748
749 #[test]
750 fn test_workpaper_with_empty_enrichment_same_as_base() {
751 let mut gen1 = WorkpaperGenerator::new(42);
752 let mut gen2 = WorkpaperGenerator::new(42);
753 let engagement = create_test_engagement();
754 let team = vec!["STAFF001".into(), "SENIOR001".into()];
755
756 let base = gen1.generate_workpaper(
757 &engagement,
758 WorkpaperSection::Planning,
759 NaiveDate::from_ymd_opt(2026, 1, 15).unwrap(),
760 &team,
761 );
762
763 let enriched = gen2.generate_workpaper_with_context(
764 &engagement,
765 WorkpaperSection::Planning,
766 NaiveDate::from_ymd_opt(2026, 1, 15).unwrap(),
767 &team,
768 &WorkpaperEnrichment::default(),
769 );
770
771 assert_eq!(base.objective, enriched.objective);
773 }
774
775 #[test]
776 fn test_workpaper_scope_adjustment_by_risk() {
777 let engagement = create_test_engagement();
778 let team = vec!["STAFF001".into()];
779
780 for (risk, min_cov) in [("High", 95.0), ("Moderate", 75.0), ("Low", 50.0)] {
781 let mut generator = WorkpaperGenerator::new(99);
782 let enrichment = WorkpaperEnrichment {
783 risk_level: Some(risk.into()),
784 ..Default::default()
785 };
786 let wp = generator.generate_workpaper_with_context(
787 &engagement,
788 WorkpaperSection::SubstantiveTesting,
789 NaiveDate::from_ymd_opt(2026, 1, 15).unwrap(),
790 &team,
791 &enrichment,
792 );
793 assert!(
794 wp.scope.coverage_percentage >= min_cov,
795 "Risk={risk}: expected coverage >= {min_cov}, got {}",
796 wp.scope.coverage_percentage
797 );
798 }
799 }
800
801 #[test]
802 fn test_workpaper_review_chain() {
803 let mut generator = WorkpaperGenerator::new(42);
804 let engagement = create_test_engagement();
805 let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
806
807 let wp = generator.generate_workpaper(
808 &engagement,
809 WorkpaperSection::SubstantiveTesting,
810 NaiveDate::from_ymd_opt(2026, 1, 15).unwrap(),
811 &team,
812 );
813
814 assert!(!wp.preparer_id.is_empty());
816
817 assert!(wp.reviewer_id.is_some());
819
820 assert!(wp.reviewer_date.unwrap() >= wp.preparer_date);
822 }
823}