Skip to main content

datasynth_generators/audit/
finding_generator.rs

1//! Finding generator for audit engagements.
2//!
3//! Generates audit findings with condition/criteria/cause/effect structure,
4//! remediation plans, and cross-references per ISA 265.
5
6use 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/// Configuration for finding generation.
17#[derive(Debug, Clone)]
18pub struct FindingGeneratorConfig {
19    /// Number of findings per engagement (min, max)
20    pub findings_per_engagement: (u32, u32),
21    /// Probability of material weakness
22    pub material_weakness_probability: f64,
23    /// Probability of significant deficiency
24    pub significant_deficiency_probability: f64,
25    /// Probability of misstatement finding
26    pub misstatement_probability: f64,
27    /// Probability of remediation plan
28    pub remediation_plan_probability: f64,
29    /// Probability of management agreement
30    pub management_agrees_probability: f64,
31    /// Misstatement amount range (min, max)
32    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
49/// Generator for audit findings.
50pub struct FindingGenerator {
51    rng: ChaCha8Rng,
52    config: FindingGeneratorConfig,
53    finding_counter: u32,
54    fiscal_year: u16,
55}
56
57impl FindingGenerator {
58    /// Create a new generator with the given seed.
59    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    /// Create a new generator with custom configuration.
69    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    /// Generate findings for an engagement.
79    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    /// Generate a single finding.
102    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        // Generate condition, criteria, cause, effect
118        let (condition, criteria, cause, effect) = self.generate_ccce(finding_type, &account);
119        finding = finding.with_details(&condition, &criteria, &cause, &effect);
120
121        // Generate recommendation
122        let recommendation = self.generate_recommendation(finding_type, &account);
123        finding = finding.with_recommendation(&recommendation);
124
125        // Set severity based on type
126        finding.severity = self.determine_severity(finding_type, &finding);
127
128        // Add monetary impact for misstatements
129        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        // Add assertions and accounts
139        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        // Link to workpapers
144        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        // Set identified by
153        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        // Maybe add review
159        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        // Determine reporting requirements
167        finding.mark_for_reporting(
168            finding.finding_type.requires_sox_reporting() || finding.severity.score() >= 3,
169            finding.requires_governance_communication(),
170        );
171
172        // Maybe add management response
173        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            // Maybe add remediation plan
180            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    /// Select finding type based on probabilities.
190    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    /// Generate finding title and related account.
222    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    /// Generate condition, criteria, cause, effect.
333    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    /// Generate recommendation.
423    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    /// Determine severity based on finding type and other factors.
463    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        // Maybe adjust severity
471        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    /// Check if finding type is a misstatement.
497    fn is_misstatement_type(&self, finding_type: FindingType) -> bool {
498        matches!(
499            finding_type,
500            FindingType::MaterialMisstatement | FindingType::ImmaterialMisstatement
501        )
502    }
503
504    /// Generate misstatement amounts.
505    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    /// Select assertions affected.
536    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    /// Select process areas.
572    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    /// Generate management response.
596    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    /// Generate remediation plan.
623    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        // Add milestones
657        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        // Maybe mark some progress
678        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    /// Select a team member.
690    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        // Generate many findings to check distribution
735        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        // Material weaknesses should be rare
753        assert!(material_weaknesses < 10);
754        // Significant deficiencies should be more common than material weaknesses
755        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        // At least some findings should have remediation plans
789        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}