1use chrono::Duration;
7use rand::{Rng, SeedableRng};
8use rand_chacha::ChaCha8Rng;
9use rust_decimal::Decimal;
10
11#[cfg(test)]
12use chrono::NaiveDate;
13
14use datasynth_core::models::audit::{
15 Assertion, AuditEngagement, AuditFinding, FindingSeverity, FindingStatus, FindingType,
16 MilestoneStatus, RemediationPlan, RemediationStatus, Workpaper,
17};
18
19#[derive(Debug, Clone)]
21pub struct FindingGeneratorConfig {
22 pub findings_per_engagement: (u32, u32),
24 pub material_weakness_probability: f64,
26 pub significant_deficiency_probability: f64,
28 pub misstatement_probability: f64,
30 pub remediation_plan_probability: f64,
32 pub management_agrees_probability: f64,
34 pub misstatement_range: (i64, i64),
36}
37
38impl Default for FindingGeneratorConfig {
39 fn default() -> Self {
40 Self {
41 findings_per_engagement: (3, 12),
42 material_weakness_probability: 0.05,
43 significant_deficiency_probability: 0.15,
44 misstatement_probability: 0.30,
45 remediation_plan_probability: 0.70,
46 management_agrees_probability: 0.85,
47 misstatement_range: (1_000, 500_000),
48 }
49 }
50}
51
52pub struct FindingGenerator {
54 rng: ChaCha8Rng,
55 config: FindingGeneratorConfig,
56 finding_counter: u32,
57 fiscal_year: u16,
58}
59
60impl FindingGenerator {
61 pub fn new(seed: u64) -> Self {
63 Self {
64 rng: ChaCha8Rng::seed_from_u64(seed),
65 config: FindingGeneratorConfig::default(),
66 finding_counter: 0,
67 fiscal_year: 2025,
68 }
69 }
70
71 pub fn with_config(seed: u64, config: FindingGeneratorConfig) -> Self {
73 Self {
74 rng: ChaCha8Rng::seed_from_u64(seed),
75 config,
76 finding_counter: 0,
77 fiscal_year: 2025,
78 }
79 }
80
81 pub fn generate_findings_for_engagement(
83 &mut self,
84 engagement: &AuditEngagement,
85 workpapers: &[Workpaper],
86 team_members: &[String],
87 ) -> Vec<AuditFinding> {
88 self.fiscal_year = engagement.fiscal_year;
89
90 let count = self.rng.gen_range(
91 self.config.findings_per_engagement.0..=self.config.findings_per_engagement.1,
92 );
93
94 let mut findings = Vec::with_capacity(count as usize);
95
96 for _ in 0..count {
97 let finding = self.generate_finding(engagement, workpapers, team_members);
98 findings.push(finding);
99 }
100
101 findings
102 }
103
104 pub fn generate_finding(
106 &mut self,
107 engagement: &AuditEngagement,
108 workpapers: &[Workpaper],
109 team_members: &[String],
110 ) -> AuditFinding {
111 self.finding_counter += 1;
112
113 let finding_type = self.select_finding_type();
114 let (title, account) = self.generate_finding_title(finding_type);
115
116 let mut finding = AuditFinding::new(engagement.engagement_id, finding_type, &title);
117
118 finding.finding_ref = format!("FIND-{}-{:03}", self.fiscal_year, self.finding_counter);
119
120 let (condition, criteria, cause, effect) = self.generate_ccce(finding_type, &account);
122 finding = finding.with_details(&condition, &criteria, &cause, &effect);
123
124 let recommendation = self.generate_recommendation(finding_type, &account);
126 finding = finding.with_recommendation(&recommendation);
127
128 finding.severity = self.determine_severity(finding_type, &finding);
130
131 if self.is_misstatement_type(finding_type) {
133 let (factual, projected, judgmental) = self.generate_misstatement_amounts();
134 finding = finding.with_misstatement(factual, projected, judgmental);
135
136 if let Some(f) = factual {
137 finding = finding.with_monetary_impact(f);
138 }
139 }
140
141 finding.assertions_affected = self.select_assertions(finding_type);
143 finding.accounts_affected = vec![account.clone()];
144 finding.process_areas = self.select_process_areas(&account);
145
146 if !workpapers.is_empty() {
148 let wp_count = self.rng.gen_range(1..=3.min(workpapers.len()));
149 for _ in 0..wp_count {
150 let idx = self.rng.gen_range(0..workpapers.len());
151 finding.workpaper_refs.push(workpapers[idx].workpaper_id);
152 }
153 }
154
155 let identifier = self.select_team_member(team_members, "senior");
157 finding.identified_by = identifier;
158 finding.identified_date =
159 engagement.fieldwork_start + Duration::days(self.rng.gen_range(7..30));
160
161 if self.rng.gen::<f64>() < 0.8 {
163 finding.reviewed_by = Some(self.select_team_member(team_members, "manager"));
164 finding.review_date =
165 Some(finding.identified_date + Duration::days(self.rng.gen_range(3..10)));
166 finding.status = FindingStatus::PendingReview;
167 }
168
169 finding.mark_for_reporting(
171 finding.finding_type.requires_sox_reporting() || finding.severity.score() >= 3,
172 finding.requires_governance_communication(),
173 );
174
175 if self.rng.gen::<f64>() < 0.7 {
177 let response_date = finding.identified_date + Duration::days(self.rng.gen_range(7..21));
178 let agrees = self.rng.gen::<f64>() < self.config.management_agrees_probability;
179 let response = self.generate_management_response(finding_type, agrees);
180 finding.add_management_response(&response, agrees, response_date);
181
182 if agrees && self.rng.gen::<f64>() < self.config.remediation_plan_probability {
184 let plan = self.generate_remediation_plan(&finding, &account);
185 finding.with_remediation_plan(plan);
186 }
187 }
188
189 finding
190 }
191
192 fn select_finding_type(&mut self) -> FindingType {
194 let r: f64 = self.rng.gen();
195
196 if r < self.config.material_weakness_probability {
197 FindingType::MaterialWeakness
198 } else if r < self.config.material_weakness_probability
199 + self.config.significant_deficiency_probability
200 {
201 FindingType::SignificantDeficiency
202 } else if r < self.config.material_weakness_probability
203 + self.config.significant_deficiency_probability
204 + self.config.misstatement_probability
205 {
206 if self.rng.gen::<f64>() < 0.3 {
207 FindingType::MaterialMisstatement
208 } else {
209 FindingType::ImmaterialMisstatement
210 }
211 } else {
212 let other_types = [
213 FindingType::ControlDeficiency,
214 FindingType::ComplianceException,
215 FindingType::OtherMatter,
216 FindingType::ItDeficiency,
217 FindingType::ProcessImprovement,
218 ];
219 let idx = self.rng.gen_range(0..other_types.len());
220 other_types[idx]
221 }
222 }
223
224 fn generate_finding_title(&mut self, finding_type: FindingType) -> (String, String) {
226 match finding_type {
227 FindingType::MaterialWeakness => {
228 let titles = [
229 (
230 "Inadequate segregation of duties in revenue cycle",
231 "Revenue",
232 ),
233 (
234 "Lack of effective review of journal entries",
235 "General Ledger",
236 ),
237 (
238 "Insufficient IT general controls over financial applications",
239 "IT Controls",
240 ),
241 (
242 "Inadequate controls over financial close process",
243 "Financial Close",
244 ),
245 ];
246 let idx = self.rng.gen_range(0..titles.len());
247 (titles[idx].0.into(), titles[idx].1.into())
248 }
249 FindingType::SignificantDeficiency => {
250 let titles = [
251 (
252 "Inadequate documentation of account reconciliations",
253 "Accounts Receivable",
254 ),
255 (
256 "Untimely review of vendor master file changes",
257 "Accounts Payable",
258 ),
259 ("Incomplete fixed asset physical inventory", "Fixed Assets"),
260 (
261 "Lack of formal approval for manual journal entries",
262 "General Ledger",
263 ),
264 ];
265 let idx = self.rng.gen_range(0..titles.len());
266 (titles[idx].0.into(), titles[idx].1.into())
267 }
268 FindingType::ControlDeficiency => {
269 let titles = [
270 (
271 "Missing secondary approval on expense reports",
272 "Operating Expenses",
273 ),
274 ("Incomplete access review documentation", "IT Controls"),
275 ("Delayed bank reconciliation preparation", "Cash"),
276 ("Inconsistent inventory count procedures", "Inventory"),
277 ];
278 let idx = self.rng.gen_range(0..titles.len());
279 (titles[idx].0.into(), titles[idx].1.into())
280 }
281 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
282 let titles = [
283 ("Revenue cutoff error", "Revenue"),
284 ("Inventory valuation adjustment", "Inventory"),
285 (
286 "Accounts receivable allowance understatement",
287 "Accounts Receivable",
288 ),
289 ("Accrued liabilities understatement", "Accrued Liabilities"),
290 ("Fixed asset depreciation calculation error", "Fixed Assets"),
291 ];
292 let idx = self.rng.gen_range(0..titles.len());
293 (titles[idx].0.into(), titles[idx].1.into())
294 }
295 FindingType::ComplianceException => {
296 let titles = [
297 ("Late filing of sales tax returns", "Tax"),
298 ("Incomplete Form 1099 reporting", "Tax"),
299 ("Non-compliance with debt covenant reporting", "Debt"),
300 ];
301 let idx = self.rng.gen_range(0..titles.len());
302 (titles[idx].0.into(), titles[idx].1.into())
303 }
304 FindingType::ItDeficiency => {
305 let titles = [
306 ("Excessive user access privileges", "IT Controls"),
307 ("Inadequate password policy enforcement", "IT Controls"),
308 ("Missing change management documentation", "IT Controls"),
309 ("Incomplete disaster recovery testing", "IT Controls"),
310 ];
311 let idx = self.rng.gen_range(0..titles.len());
312 (titles[idx].0.into(), titles[idx].1.into())
313 }
314 FindingType::OtherMatter | FindingType::ProcessImprovement => {
315 let titles = [
316 (
317 "Opportunity to improve month-end close efficiency",
318 "Financial Close",
319 ),
320 (
321 "Enhancement to vendor onboarding process",
322 "Accounts Payable",
323 ),
324 (
325 "Automation opportunity in reconciliation process",
326 "General Ledger",
327 ),
328 ];
329 let idx = self.rng.gen_range(0..titles.len());
330 (titles[idx].0.into(), titles[idx].1.into())
331 }
332 }
333 }
334
335 fn generate_ccce(
337 &mut self,
338 finding_type: FindingType,
339 account: &str,
340 ) -> (String, String, String, String) {
341 match finding_type {
342 FindingType::MaterialWeakness
343 | FindingType::SignificantDeficiency
344 | FindingType::ControlDeficiency => {
345 let condition = format!(
346 "During our testing of {} controls, we noted that the control was not operating effectively. \
347 Specifically, {} of {} items tested did not have evidence of the required control activity.",
348 account,
349 self.rng.gen_range(2..8),
350 self.rng.gen_range(20..40)
351 );
352 let criteria = format!(
353 "Company policy and SOX requirements mandate that all {} transactions receive appropriate \
354 review and approval prior to processing.",
355 account
356 );
357 let cause = "Staffing constraints and competing priorities resulted in reduced focus on control execution.".into();
358 let effect = format!(
359 "Transactions may be processed without appropriate oversight, increasing the risk of errors \
360 or fraud in the {} balance.",
361 account
362 );
363 (condition, criteria, cause, effect)
364 }
365 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
366 let amount = self
367 .rng
368 .gen_range(self.config.misstatement_range.0..self.config.misstatement_range.1);
369 let condition = format!(
370 "Our testing identified a misstatement in {} of approximately ${}. \
371 The error resulted from incorrect application of accounting standards.",
372 account, amount
373 );
374 let criteria = "US GAAP and company accounting policy require accurate recording of all transactions.".into();
375 let cause =
376 "Manual calculation error combined with inadequate review procedures.".into();
377 let effect = format!(
378 "The {} balance was {} by ${}, which {}.",
379 account,
380 if self.rng.gen::<bool>() {
381 "overstated"
382 } else {
383 "understated"
384 },
385 amount,
386 if finding_type == FindingType::MaterialMisstatement {
387 "represents a material misstatement"
388 } else {
389 "is below materiality but has been communicated to management"
390 }
391 );
392 (condition, criteria, cause, effect)
393 }
394 FindingType::ComplianceException => {
395 let condition = format!(
396 "The Company did not comply with {} regulatory requirements during the period under audit.",
397 account
398 );
399 let criteria =
400 "Applicable laws and regulations require timely and accurate compliance."
401 .into();
402 let cause = "Lack of monitoring procedures to track compliance deadlines.".into();
403 let effect =
404 "The Company may be subject to penalties or regulatory scrutiny.".into();
405 (condition, criteria, cause, effect)
406 }
407 _ => {
408 let condition = format!(
409 "We identified an opportunity to enhance the {} process.",
410 account
411 );
412 let criteria =
413 "Industry best practices suggest continuous improvement in control processes."
414 .into();
415 let cause =
416 "Current processes have not been updated to reflect operational changes."
417 .into();
418 let effect =
419 "Operational efficiency could be improved with process enhancements.".into();
420 (condition, criteria, cause, effect)
421 }
422 }
423 }
424
425 fn generate_recommendation(&mut self, finding_type: FindingType, account: &str) -> String {
427 match finding_type {
428 FindingType::MaterialWeakness | FindingType::SignificantDeficiency => {
429 format!(
430 "We recommend that management: (1) Implement additional review procedures for {} transactions, \
431 (2) Document all control activities contemporaneously, and \
432 (3) Provide additional training to personnel responsible for control execution.",
433 account
434 )
435 }
436 FindingType::ControlDeficiency => {
437 format!(
438 "We recommend that management strengthen the {} control by ensuring timely execution \
439 and documentation of all required review activities.",
440 account
441 )
442 }
443 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
444 "We recommend that management record the proposed adjusting entry and implement \
445 additional review procedures to prevent similar errors in future periods.".into()
446 }
447 FindingType::ComplianceException => {
448 "We recommend that management implement a compliance calendar with automated reminders \
449 and establish monitoring procedures to ensure timely compliance.".into()
450 }
451 FindingType::ItDeficiency => {
452 "We recommend that IT management review and remediate the identified access control \
453 weaknesses and implement periodic access certification procedures.".into()
454 }
455 _ => {
456 format!(
457 "We recommend that management evaluate the {} process for potential \
458 efficiency improvements and implement changes as appropriate.",
459 account
460 )
461 }
462 }
463 }
464
465 fn determine_severity(
467 &mut self,
468 finding_type: FindingType,
469 _finding: &AuditFinding,
470 ) -> FindingSeverity {
471 let base_severity = finding_type.default_severity();
472
473 if self.rng.gen::<f64>() < 0.2 {
475 match base_severity {
476 FindingSeverity::Critical => FindingSeverity::High,
477 FindingSeverity::High => {
478 if self.rng.gen::<bool>() {
479 FindingSeverity::Critical
480 } else {
481 FindingSeverity::Medium
482 }
483 }
484 FindingSeverity::Medium => {
485 if self.rng.gen::<bool>() {
486 FindingSeverity::High
487 } else {
488 FindingSeverity::Low
489 }
490 }
491 FindingSeverity::Low => FindingSeverity::Medium,
492 FindingSeverity::Informational => FindingSeverity::Low,
493 }
494 } else {
495 base_severity
496 }
497 }
498
499 fn is_misstatement_type(&self, finding_type: FindingType) -> bool {
501 matches!(
502 finding_type,
503 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement
504 )
505 }
506
507 fn generate_misstatement_amounts(
509 &mut self,
510 ) -> (Option<Decimal>, Option<Decimal>, Option<Decimal>) {
511 let factual = Decimal::new(
512 self.rng
513 .gen_range(self.config.misstatement_range.0..self.config.misstatement_range.1),
514 0,
515 );
516
517 let projected = if self.rng.gen::<f64>() < 0.5 {
518 Some(Decimal::new(
519 self.rng.gen_range(0..self.config.misstatement_range.1 / 2),
520 0,
521 ))
522 } else {
523 None
524 };
525
526 let judgmental = if self.rng.gen::<f64>() < 0.3 {
527 Some(Decimal::new(
528 self.rng.gen_range(0..self.config.misstatement_range.1 / 4),
529 0,
530 ))
531 } else {
532 None
533 };
534
535 (Some(factual), projected, judgmental)
536 }
537
538 fn select_assertions(&mut self, finding_type: FindingType) -> Vec<Assertion> {
540 let mut assertions = Vec::new();
541
542 match finding_type {
543 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
544 assertions.push(Assertion::Accuracy);
545 if self.rng.gen::<bool>() {
546 assertions.push(Assertion::ValuationAndAllocation);
547 }
548 }
549 FindingType::MaterialWeakness
550 | FindingType::SignificantDeficiency
551 | FindingType::ControlDeficiency => {
552 let possible = [
553 Assertion::Occurrence,
554 Assertion::Completeness,
555 Assertion::Accuracy,
556 Assertion::Classification,
557 ];
558 let count = self.rng.gen_range(1..=3);
559 for _ in 0..count {
560 let idx = self.rng.gen_range(0..possible.len());
561 if !assertions.contains(&possible[idx]) {
562 assertions.push(possible[idx]);
563 }
564 }
565 }
566 _ => {
567 assertions.push(Assertion::PresentationAndDisclosure);
568 }
569 }
570
571 assertions
572 }
573
574 fn select_process_areas(&mut self, account: &str) -> Vec<String> {
576 let account_lower = account.to_lowercase();
577
578 if account_lower.contains("revenue") || account_lower.contains("receivable") {
579 vec!["Order to Cash".into(), "Revenue Recognition".into()]
580 } else if account_lower.contains("payable") || account_lower.contains("expense") {
581 vec!["Procure to Pay".into(), "Expense Management".into()]
582 } else if account_lower.contains("inventory") {
583 vec!["Inventory Management".into(), "Cost of Goods Sold".into()]
584 } else if account_lower.contains("fixed asset") {
585 vec!["Capital Asset Management".into()]
586 } else if account_lower.contains("it") {
587 vec![
588 "IT General Controls".into(),
589 "IT Application Controls".into(),
590 ]
591 } else if account_lower.contains("payroll") {
592 vec!["Hire to Retire".into(), "Payroll Processing".into()]
593 } else {
594 vec!["Financial Close".into()]
595 }
596 }
597
598 fn generate_management_response(&mut self, finding_type: FindingType, agrees: bool) -> String {
600 if agrees {
601 match finding_type {
602 FindingType::MaterialWeakness | FindingType::SignificantDeficiency => {
603 "Management agrees with the finding and has initiated a remediation plan to \
604 address the identified control deficiency. We expect to complete remediation \
605 prior to the next audit cycle."
606 .into()
607 }
608 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
609 "Management agrees with the proposed adjustment and will record the entry. \
610 We have implemented additional review procedures to prevent similar errors."
611 .into()
612 }
613 _ => "Management agrees with the observation and will implement the recommended \
614 improvements as resources permit."
615 .into(),
616 }
617 } else {
618 "Management respectfully disagrees with the finding. We believe that existing \
619 controls are adequate and operating effectively. We will provide additional \
620 documentation to support our position."
621 .into()
622 }
623 }
624
625 fn generate_remediation_plan(
627 &mut self,
628 finding: &AuditFinding,
629 account: &str,
630 ) -> RemediationPlan {
631 let target_date = finding.identified_date + Duration::days(self.rng.gen_range(60..180));
632
633 let description = format!(
634 "Implement enhanced controls and monitoring procedures for {} to address \
635 the identified deficiency. This includes updated policies, additional training, \
636 and implementation of automated controls where feasible.",
637 account
638 );
639
640 let responsible_party = format!(
641 "{} Manager",
642 if account.to_lowercase().contains("it") {
643 "IT"
644 } else {
645 "Controller"
646 }
647 );
648
649 let mut plan = RemediationPlan::new(
650 finding.finding_id,
651 &description,
652 &responsible_party,
653 target_date,
654 );
655
656 plan.validation_approach =
657 "Auditor will test remediated controls during the next audit cycle.".into();
658
659 let milestone_dates = [
661 (
662 finding.identified_date + Duration::days(30),
663 "Complete root cause analysis",
664 ),
665 (
666 finding.identified_date + Duration::days(60),
667 "Document updated control procedures",
668 ),
669 (
670 finding.identified_date + Duration::days(90),
671 "Implement control changes",
672 ),
673 (target_date, "Complete testing and validation"),
674 ];
675
676 for (date, desc) in milestone_dates {
677 plan.add_milestone(desc, date);
678 }
679
680 if self.rng.gen::<f64>() < 0.3 {
682 plan.status = RemediationStatus::InProgress;
683 if !plan.milestones.is_empty() {
684 plan.milestones[0].status = MilestoneStatus::Complete;
685 plan.milestones[0].completion_date = Some(plan.milestones[0].target_date);
686 }
687 }
688
689 plan
690 }
691
692 fn select_team_member(&mut self, team_members: &[String], role_hint: &str) -> String {
694 let matching: Vec<&String> = team_members
695 .iter()
696 .filter(|m| m.to_lowercase().contains(role_hint))
697 .collect();
698
699 if let Some(&member) = matching.first() {
700 member.clone()
701 } else if !team_members.is_empty() {
702 let idx = self.rng.gen_range(0..team_members.len());
703 team_members[idx].clone()
704 } else {
705 format!("{}001", role_hint.to_uppercase())
706 }
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use datasynth_core::models::audit::EngagementType;
714
715 fn create_test_engagement() -> AuditEngagement {
716 AuditEngagement::new(
717 "ENTITY001",
718 "Test Company Inc.",
719 EngagementType::AnnualAudit,
720 2025,
721 NaiveDate::from_ymd_opt(2025, 12, 31).unwrap(),
722 )
723 .with_materiality(
724 Decimal::new(1_000_000, 0),
725 0.75,
726 0.05,
727 "Total Revenue",
728 0.005,
729 )
730 .with_timeline(
731 NaiveDate::from_ymd_opt(2025, 10, 1).unwrap(),
732 NaiveDate::from_ymd_opt(2025, 10, 31).unwrap(),
733 NaiveDate::from_ymd_opt(2026, 1, 5).unwrap(),
734 NaiveDate::from_ymd_opt(2026, 2, 15).unwrap(),
735 NaiveDate::from_ymd_opt(2026, 2, 16).unwrap(),
736 NaiveDate::from_ymd_opt(2026, 3, 15).unwrap(),
737 )
738 }
739
740 #[test]
741 fn test_finding_generation() {
742 let mut generator = FindingGenerator::new(42);
743 let engagement = create_test_engagement();
744 let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
745
746 let findings = generator.generate_findings_for_engagement(&engagement, &[], &team);
747
748 assert!(!findings.is_empty());
749 for finding in &findings {
750 assert!(!finding.condition.is_empty());
751 assert!(!finding.criteria.is_empty());
752 assert!(!finding.recommendation.is_empty());
753 }
754 }
755
756 #[test]
757 fn test_finding_types_distribution() {
758 let mut generator = FindingGenerator::new(42);
759 let engagement = create_test_engagement();
760 let team = vec!["STAFF001".into()];
761
762 let config = FindingGeneratorConfig {
764 findings_per_engagement: (50, 50),
765 ..Default::default()
766 };
767 generator.config = config;
768
769 let findings = generator.generate_findings_for_engagement(&engagement, &[], &team);
770
771 let material_weaknesses = findings
772 .iter()
773 .filter(|f| f.finding_type == FindingType::MaterialWeakness)
774 .count();
775 let significant_deficiencies = findings
776 .iter()
777 .filter(|f| f.finding_type == FindingType::SignificantDeficiency)
778 .count();
779
780 assert!(material_weaknesses < 10);
782 assert!(significant_deficiencies > material_weaknesses);
784 }
785
786 #[test]
787 fn test_misstatement_finding() {
788 let config = FindingGeneratorConfig {
789 misstatement_probability: 1.0,
790 material_weakness_probability: 0.0,
791 significant_deficiency_probability: 0.0,
792 ..Default::default()
793 };
794 let mut generator = FindingGenerator::with_config(42, config);
795 let engagement = create_test_engagement();
796
797 let finding = generator.generate_finding(&engagement, &[], &["STAFF001".into()]);
798
799 assert!(finding.is_misstatement);
800 assert!(finding.factual_misstatement.is_some() || finding.projected_misstatement.is_some());
801 }
802
803 #[test]
804 fn test_remediation_plan() {
805 let config = FindingGeneratorConfig {
806 remediation_plan_probability: 1.0,
807 management_agrees_probability: 1.0,
808 ..Default::default()
809 };
810 let mut generator = FindingGenerator::with_config(42, config);
811 let engagement = create_test_engagement();
812
813 let findings =
814 generator.generate_findings_for_engagement(&engagement, &[], &["STAFF001".into()]);
815
816 let with_plans = findings
818 .iter()
819 .filter(|f| f.remediation_plan.is_some())
820 .count();
821 assert!(with_plans > 0);
822
823 for finding in findings.iter().filter(|f| f.remediation_plan.is_some()) {
824 let plan = finding.remediation_plan.as_ref().unwrap();
825 assert!(!plan.description.is_empty());
826 assert!(!plan.milestones.is_empty());
827 }
828 }
829
830 #[test]
831 fn test_governance_communication() {
832 let config = FindingGeneratorConfig {
833 material_weakness_probability: 1.0,
834 ..Default::default()
835 };
836 let mut generator = FindingGenerator::with_config(42, config);
837 let engagement = create_test_engagement();
838
839 let finding = generator.generate_finding(&engagement, &[], &["STAFF001".into()]);
840
841 assert!(finding.report_to_governance);
842 assert!(finding.include_in_management_letter);
843 }
844}