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