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