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 {} transactions receive appropriate \
353 review and approval prior to processing.",
354 account
355 );
356 let cause = "Staffing constraints and competing priorities resulted in reduced focus on control execution.".into();
357 let effect = format!(
358 "Transactions may be processed without appropriate oversight, increasing the risk of errors \
359 or fraud in the {} balance.",
360 account
361 );
362 (condition, criteria, cause, effect)
363 }
364 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
365 let amount = self.rng.random_range(
366 self.config.misstatement_range.0..self.config.misstatement_range.1,
367 );
368 let condition = format!(
369 "Our testing identified a misstatement in {} of approximately ${}. \
370 The error resulted from incorrect application of accounting standards.",
371 account, amount
372 );
373 let criteria = "US GAAP and company accounting policy require accurate recording of all transactions.".into();
374 let cause =
375 "Manual calculation error combined with inadequate review procedures.".into();
376 let effect = format!(
377 "The {} balance was {} by ${}, which {}.",
378 account,
379 if self.rng.random::<bool>() {
380 "overstated"
381 } else {
382 "understated"
383 },
384 amount,
385 if finding_type == FindingType::MaterialMisstatement {
386 "represents a material misstatement"
387 } else {
388 "is below materiality but has been communicated to management"
389 }
390 );
391 (condition, criteria, cause, effect)
392 }
393 FindingType::ComplianceException => {
394 let condition = format!(
395 "The Company did not comply with {} regulatory requirements during the period under audit.",
396 account
397 );
398 let criteria =
399 "Applicable laws and regulations require timely and accurate compliance."
400 .into();
401 let cause = "Lack of monitoring procedures to track compliance deadlines.".into();
402 let effect =
403 "The Company may be subject to penalties or regulatory scrutiny.".into();
404 (condition, criteria, cause, effect)
405 }
406 _ => {
407 let condition = format!(
408 "We identified an opportunity to enhance the {} process.",
409 account
410 );
411 let criteria =
412 "Industry best practices suggest continuous improvement in control processes."
413 .into();
414 let cause =
415 "Current processes have not been updated to reflect operational changes."
416 .into();
417 let effect =
418 "Operational efficiency could be improved with process enhancements.".into();
419 (condition, criteria, cause, effect)
420 }
421 }
422 }
423
424 fn generate_recommendation(&mut self, finding_type: FindingType, account: &str) -> String {
426 match finding_type {
427 FindingType::MaterialWeakness | FindingType::SignificantDeficiency => {
428 format!(
429 "We recommend that management: (1) Implement additional review procedures for {} transactions, \
430 (2) Document all control activities contemporaneously, and \
431 (3) Provide additional training to personnel responsible for control execution.",
432 account
433 )
434 }
435 FindingType::ControlDeficiency => {
436 format!(
437 "We recommend that management strengthen the {} control by ensuring timely execution \
438 and documentation of all required review activities.",
439 account
440 )
441 }
442 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
443 "We recommend that management record the proposed adjusting entry and implement \
444 additional review procedures to prevent similar errors in future periods.".into()
445 }
446 FindingType::ComplianceException => {
447 "We recommend that management implement a compliance calendar with automated reminders \
448 and establish monitoring procedures to ensure timely compliance.".into()
449 }
450 FindingType::ItDeficiency => {
451 "We recommend that IT management review and remediate the identified access control \
452 weaknesses and implement periodic access certification procedures.".into()
453 }
454 _ => {
455 format!(
456 "We recommend that management evaluate the {} process for potential \
457 efficiency improvements and implement changes as appropriate.",
458 account
459 )
460 }
461 }
462 }
463
464 fn determine_severity(
466 &mut self,
467 finding_type: FindingType,
468 _finding: &AuditFinding,
469 ) -> FindingSeverity {
470 let base_severity = finding_type.default_severity();
471
472 if self.rng.random::<f64>() < 0.2 {
474 match base_severity {
475 FindingSeverity::Critical => FindingSeverity::High,
476 FindingSeverity::High => {
477 if self.rng.random::<bool>() {
478 FindingSeverity::Critical
479 } else {
480 FindingSeverity::Medium
481 }
482 }
483 FindingSeverity::Medium => {
484 if self.rng.random::<bool>() {
485 FindingSeverity::High
486 } else {
487 FindingSeverity::Low
488 }
489 }
490 FindingSeverity::Low => FindingSeverity::Medium,
491 FindingSeverity::Informational => FindingSeverity::Low,
492 }
493 } else {
494 base_severity
495 }
496 }
497
498 fn is_misstatement_type(&self, finding_type: FindingType) -> bool {
500 matches!(
501 finding_type,
502 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement
503 )
504 }
505
506 fn generate_misstatement_amounts(
508 &mut self,
509 ) -> (Option<Decimal>, Option<Decimal>, Option<Decimal>) {
510 let factual = Decimal::new(
511 self.rng
512 .random_range(self.config.misstatement_range.0..self.config.misstatement_range.1),
513 0,
514 );
515
516 let projected = if self.rng.random::<f64>() < 0.5 {
517 Some(Decimal::new(
518 self.rng
519 .random_range(0..self.config.misstatement_range.1 / 2),
520 0,
521 ))
522 } else {
523 None
524 };
525
526 let judgmental = if self.rng.random::<f64>() < 0.3 {
527 Some(Decimal::new(
528 self.rng
529 .random_range(0..self.config.misstatement_range.1 / 4),
530 0,
531 ))
532 } else {
533 None
534 };
535
536 (Some(factual), projected, judgmental)
537 }
538
539 fn select_assertions(&mut self, finding_type: FindingType) -> Vec<Assertion> {
541 let mut assertions = Vec::new();
542
543 match finding_type {
544 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
545 assertions.push(Assertion::Accuracy);
546 if self.rng.random::<bool>() {
547 assertions.push(Assertion::ValuationAndAllocation);
548 }
549 }
550 FindingType::MaterialWeakness
551 | FindingType::SignificantDeficiency
552 | FindingType::ControlDeficiency => {
553 let possible = [
554 Assertion::Occurrence,
555 Assertion::Completeness,
556 Assertion::Accuracy,
557 Assertion::Classification,
558 ];
559 let count = self.rng.random_range(1..=3);
560 for _ in 0..count {
561 let idx = self.rng.random_range(0..possible.len());
562 if !assertions.contains(&possible[idx]) {
563 assertions.push(possible[idx]);
564 }
565 }
566 }
567 _ => {
568 assertions.push(Assertion::PresentationAndDisclosure);
569 }
570 }
571
572 assertions
573 }
574
575 fn select_process_areas(&mut self, account: &str) -> Vec<String> {
577 let account_lower = account.to_lowercase();
578
579 if account_lower.contains("revenue") || account_lower.contains("receivable") {
580 vec!["Order to Cash".into(), "Revenue Recognition".into()]
581 } else if account_lower.contains("payable") || account_lower.contains("expense") {
582 vec!["Procure to Pay".into(), "Expense Management".into()]
583 } else if account_lower.contains("inventory") {
584 vec!["Inventory Management".into(), "Cost of Goods Sold".into()]
585 } else if account_lower.contains("fixed asset") {
586 vec!["Capital Asset Management".into()]
587 } else if account_lower.contains("it") {
588 vec![
589 "IT General Controls".into(),
590 "IT Application Controls".into(),
591 ]
592 } else if account_lower.contains("payroll") {
593 vec!["Hire to Retire".into(), "Payroll Processing".into()]
594 } else {
595 vec!["Financial Close".into()]
596 }
597 }
598
599 fn generate_management_response(&mut self, finding_type: FindingType, agrees: bool) -> String {
601 if agrees {
602 match finding_type {
603 FindingType::MaterialWeakness | FindingType::SignificantDeficiency => {
604 "Management agrees with the finding and has initiated a remediation plan to \
605 address the identified control deficiency. We expect to complete remediation \
606 prior to the next audit cycle."
607 .into()
608 }
609 FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement => {
610 "Management agrees with the proposed adjustment and will record the entry. \
611 We have implemented additional review procedures to prevent similar errors."
612 .into()
613 }
614 _ => "Management agrees with the observation and will implement the recommended \
615 improvements as resources permit."
616 .into(),
617 }
618 } else {
619 "Management respectfully disagrees with the finding. We believe that existing \
620 controls are adequate and operating effectively. We will provide additional \
621 documentation to support our position."
622 .into()
623 }
624 }
625
626 fn generate_remediation_plan(
628 &mut self,
629 finding: &AuditFinding,
630 account: &str,
631 ) -> RemediationPlan {
632 let target_date = finding.identified_date + Duration::days(self.rng.random_range(60..180));
633
634 let description = format!(
635 "Implement enhanced controls and monitoring procedures for {} to address \
636 the identified deficiency. This includes updated policies, additional training, \
637 and implementation of automated controls where feasible.",
638 account
639 );
640
641 let responsible_party = format!(
642 "{} Manager",
643 if account.to_lowercase().contains("it") {
644 "IT"
645 } else {
646 "Controller"
647 }
648 );
649
650 let mut plan = RemediationPlan::new(
651 finding.finding_id,
652 &description,
653 &responsible_party,
654 target_date,
655 );
656
657 plan.validation_approach =
658 "Auditor will test remediated controls during the next audit cycle.".into();
659
660 let milestone_dates = [
662 (
663 finding.identified_date + Duration::days(30),
664 "Complete root cause analysis",
665 ),
666 (
667 finding.identified_date + Duration::days(60),
668 "Document updated control procedures",
669 ),
670 (
671 finding.identified_date + Duration::days(90),
672 "Implement control changes",
673 ),
674 (target_date, "Complete testing and validation"),
675 ];
676
677 for (date, desc) in milestone_dates {
678 plan.add_milestone(desc, date);
679 }
680
681 if self.rng.random::<f64>() < 0.3 {
683 plan.status = RemediationStatus::InProgress;
684 if !plan.milestones.is_empty() {
685 plan.milestones[0].status = MilestoneStatus::Complete;
686 plan.milestones[0].completion_date = Some(plan.milestones[0].target_date);
687 }
688 }
689
690 plan
691 }
692
693 fn select_team_member(&mut self, team_members: &[String], role_hint: &str) -> String {
695 let matching: Vec<&String> = team_members
696 .iter()
697 .filter(|m| m.to_lowercase().contains(role_hint))
698 .collect();
699
700 if let Some(&member) = matching.first() {
701 member.clone()
702 } else if !team_members.is_empty() {
703 let idx = self.rng.random_range(0..team_members.len());
704 team_members[idx].clone()
705 } else {
706 format!("{}001", role_hint.to_uppercase())
707 }
708 }
709}
710
711#[cfg(test)]
712#[allow(clippy::unwrap_used)]
713mod tests {
714 use super::*;
715 use crate::audit::test_helpers::create_test_engagement;
716
717 #[test]
718 fn test_finding_generation() {
719 let mut generator = FindingGenerator::new(42);
720 let engagement = create_test_engagement();
721 let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
722
723 let findings = generator.generate_findings_for_engagement(&engagement, &[], &team);
724
725 assert!(!findings.is_empty());
726 for finding in &findings {
727 assert!(!finding.condition.is_empty());
728 assert!(!finding.criteria.is_empty());
729 assert!(!finding.recommendation.is_empty());
730 }
731 }
732
733 #[test]
734 fn test_finding_types_distribution() {
735 let mut generator = FindingGenerator::new(42);
736 let engagement = create_test_engagement();
737 let team = vec!["STAFF001".into()];
738
739 let config = FindingGeneratorConfig {
741 findings_per_engagement: (50, 50),
742 ..Default::default()
743 };
744 generator.config = config;
745
746 let findings = generator.generate_findings_for_engagement(&engagement, &[], &team);
747
748 let material_weaknesses = findings
749 .iter()
750 .filter(|f| f.finding_type == FindingType::MaterialWeakness)
751 .count();
752 let significant_deficiencies = findings
753 .iter()
754 .filter(|f| f.finding_type == FindingType::SignificantDeficiency)
755 .count();
756
757 assert!(material_weaknesses < 10);
759 assert!(significant_deficiencies > material_weaknesses);
761 }
762
763 #[test]
764 fn test_misstatement_finding() {
765 let config = FindingGeneratorConfig {
766 misstatement_probability: 1.0,
767 material_weakness_probability: 0.0,
768 significant_deficiency_probability: 0.0,
769 ..Default::default()
770 };
771 let mut generator = FindingGenerator::with_config(42, config);
772 let engagement = create_test_engagement();
773
774 let finding = generator.generate_finding(&engagement, &[], &["STAFF001".into()]);
775
776 assert!(finding.is_misstatement);
777 assert!(finding.factual_misstatement.is_some() || finding.projected_misstatement.is_some());
778 }
779
780 #[test]
781 fn test_remediation_plan() {
782 let config = FindingGeneratorConfig {
783 remediation_plan_probability: 1.0,
784 management_agrees_probability: 1.0,
785 ..Default::default()
786 };
787 let mut generator = FindingGenerator::with_config(42, config);
788 let engagement = create_test_engagement();
789
790 let findings =
791 generator.generate_findings_for_engagement(&engagement, &[], &["STAFF001".into()]);
792
793 let with_plans = findings
795 .iter()
796 .filter(|f| f.remediation_plan.is_some())
797 .count();
798 assert!(with_plans > 0);
799
800 for finding in findings.iter().filter(|f| f.remediation_plan.is_some()) {
801 let plan = finding.remediation_plan.as_ref().unwrap();
802 assert!(!plan.description.is_empty());
803 assert!(!plan.milestones.is_empty());
804 }
805 }
806
807 #[test]
808 fn test_governance_communication() {
809 let config = FindingGeneratorConfig {
810 material_weakness_probability: 1.0,
811 ..Default::default()
812 };
813 let mut generator = FindingGenerator::with_config(42, config);
814 let engagement = create_test_engagement();
815
816 let finding = generator.generate_finding(&engagement, &[], &["STAFF001".into()]);
817
818 assert!(finding.report_to_governance);
819 assert!(finding.include_in_management_letter);
820 }
821}