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