Skip to main content

datasynth_generators/audit/
risk_generator.rs

1//! Risk assessment generator for audit engagements.
2//!
3//! Generates risk assessments with fraud risk factors, planned responses,
4//! and cross-references per ISA 315 and ISA 330.
5
6use chrono::Duration;
7use datasynth_core::utils::seeded_rng;
8use rand::Rng;
9use rand_chacha::ChaCha8Rng;
10
11use datasynth_core::models::audit::{
12    Assertion, AuditEngagement, DetectionRisk, EngagementPhase, FraudRiskFactor,
13    FraudTriangleElement, PlannedResponse, ResponseNature, ResponseProcedureType, ResponseStatus,
14    ResponseTiming, RiskAssessment, RiskCategory, RiskLevel, RiskReviewStatus, Trend,
15};
16
17/// Configuration for risk assessment generation.
18#[derive(Debug, Clone)]
19pub struct RiskAssessmentGeneratorConfig {
20    /// Number of risks per engagement (min, max)
21    pub risks_per_engagement: (u32, u32),
22    /// Probability of significant risk designation
23    pub significant_risk_probability: f64,
24    /// Probability of high inherent risk
25    pub high_inherent_risk_probability: f64,
26    /// Probability of fraud risk factors
27    pub fraud_factor_probability: f64,
28    /// Number of planned responses per risk (min, max)
29    pub responses_per_risk: (u32, u32),
30}
31
32impl Default for RiskAssessmentGeneratorConfig {
33    fn default() -> Self {
34        Self {
35            risks_per_engagement: (8, 20),
36            significant_risk_probability: 0.20,
37            high_inherent_risk_probability: 0.25,
38            fraud_factor_probability: 0.30,
39            responses_per_risk: (1, 4),
40        }
41    }
42}
43
44/// Generator for risk assessments.
45pub struct RiskAssessmentGenerator {
46    rng: ChaCha8Rng,
47    config: RiskAssessmentGeneratorConfig,
48    risk_counter: u32,
49}
50
51impl RiskAssessmentGenerator {
52    /// Create a new generator with the given seed.
53    pub fn new(seed: u64) -> Self {
54        Self {
55            rng: seeded_rng(seed, 0),
56            config: RiskAssessmentGeneratorConfig::default(),
57            risk_counter: 0,
58        }
59    }
60
61    /// Create a new generator with custom configuration.
62    pub fn with_config(seed: u64, config: RiskAssessmentGeneratorConfig) -> Self {
63        Self {
64            rng: seeded_rng(seed, 0),
65            config,
66            risk_counter: 0,
67        }
68    }
69
70    /// Generate risk assessments for an engagement.
71    pub fn generate_risks_for_engagement(
72        &mut self,
73        engagement: &AuditEngagement,
74        team_members: &[String],
75        accounts: &[String],
76    ) -> Vec<RiskAssessment> {
77        let count = self
78            .rng
79            .random_range(self.config.risks_per_engagement.0..=self.config.risks_per_engagement.1);
80
81        let mut risks = Vec::with_capacity(count as usize);
82
83        // Always include presumed risks per ISA 240
84        risks.push(self.generate_revenue_fraud_risk(engagement, team_members));
85        risks.push(self.generate_management_override_risk(engagement, team_members));
86
87        // Generate additional risks for various accounts/processes
88        let risk_areas = self.get_risk_areas(accounts);
89        for area in risk_areas.iter().take((count - 2) as usize) {
90            let risk = self.generate_risk_assessment(engagement, area, team_members);
91            risks.push(risk);
92        }
93
94        risks
95    }
96
97    /// Generate a single risk assessment.
98    pub fn generate_risk_assessment(
99        &mut self,
100        engagement: &AuditEngagement,
101        account_or_process: &str,
102        team_members: &[String],
103    ) -> RiskAssessment {
104        self.risk_counter += 1;
105
106        let risk_category = self.select_risk_category();
107        let (description, assertion) =
108            self.generate_risk_description(account_or_process, risk_category);
109
110        let mut risk = RiskAssessment::new(
111            engagement.engagement_id,
112            risk_category,
113            account_or_process,
114            &description,
115        );
116
117        risk.risk_ref = format!("RISK-{:04}", self.risk_counter);
118
119        // Set assertion if applicable
120        if let Some(assertion) = assertion {
121            risk = risk.with_assertion(assertion);
122        }
123
124        // Set risk levels
125        let inherent_risk = self.generate_inherent_risk();
126        let control_risk = self.generate_control_risk(&inherent_risk);
127        risk = risk.with_risk_levels(inherent_risk, control_risk);
128
129        // Maybe mark as significant
130        if self.rng.random::<f64>() < self.config.significant_risk_probability
131            || matches!(
132                risk.risk_of_material_misstatement,
133                RiskLevel::High | RiskLevel::Significant
134            )
135        {
136            let rationale = self.generate_significant_risk_rationale(risk_category);
137            risk = risk.mark_significant(&rationale);
138        }
139
140        // Add fraud risk factors if applicable
141        if self.rng.random::<f64>() < self.config.fraud_factor_probability {
142            let factors = self.generate_fraud_factors();
143            for factor in factors {
144                risk.add_fraud_factor(factor);
145            }
146        }
147
148        // Set response nature and timing
149        risk.response_nature = self.select_response_nature(&risk);
150        risk.response_timing = self.select_response_timing(engagement);
151        risk.response_extent = self.generate_response_extent(&risk);
152
153        // Add planned responses
154        let response_count = self
155            .rng
156            .random_range(self.config.responses_per_risk.0..=self.config.responses_per_risk.1);
157        for _ in 0..response_count {
158            let response = self.generate_planned_response(&risk, team_members, engagement);
159            risk.add_response(response);
160        }
161
162        // Set assessor
163        let assessor = self.select_team_member(team_members, "senior");
164        risk = risk.with_assessed_by(&assessor, engagement.planning_start + Duration::days(7));
165
166        // Maybe add review
167        if self.rng.random::<f64>() < 0.8 {
168            risk.review_status = RiskReviewStatus::Approved;
169            risk.reviewer_id = Some(self.select_team_member(team_members, "manager"));
170            risk.review_date = Some(engagement.planning_start + Duration::days(14));
171        }
172
173        risk
174    }
175
176    /// Generate presumed revenue fraud risk per ISA 240.
177    fn generate_revenue_fraud_risk(
178        &mut self,
179        engagement: &AuditEngagement,
180        team_members: &[String],
181    ) -> RiskAssessment {
182        self.risk_counter += 1;
183
184        let mut risk = RiskAssessment::new(
185            engagement.engagement_id,
186            RiskCategory::FraudRisk,
187            "Revenue Recognition",
188            "Presumed fraud risk in revenue recognition per ISA 240.26",
189        );
190
191        risk.risk_ref = format!("RISK-{:04}", self.risk_counter);
192        risk = risk.with_assertion(Assertion::Occurrence);
193        risk = risk.with_risk_levels(RiskLevel::High, RiskLevel::Medium);
194        risk = risk.mark_significant("Presumed fraud risk per ISA 240 - revenue recognition");
195        risk.presumed_revenue_fraud_risk = true;
196
197        // Add fraud triangle factors
198        risk.add_fraud_factor(FraudRiskFactor::new(
199            FraudTriangleElement::Pressure,
200            "Management compensation tied to revenue targets",
201            75,
202            "Compensation plan review",
203        ));
204        risk.add_fraud_factor(FraudRiskFactor::new(
205            FraudTriangleElement::Opportunity,
206            "Complex revenue arrangements with multiple performance obligations",
207            60,
208            "Contract review",
209        ));
210
211        risk.response_nature = ResponseNature::SubstantiveOnly;
212        risk.response_timing = ResponseTiming::YearEnd;
213        risk.response_extent = "Extended substantive testing with increased sample sizes".into();
214
215        // Add responses
216        risk.add_response(PlannedResponse::new(
217            "Test revenue cutoff at year-end",
218            ResponseProcedureType::TestOfDetails,
219            Assertion::Cutoff,
220            &self.select_team_member(team_members, "senior"),
221            engagement.fieldwork_start + Duration::days(14),
222        ));
223        risk.add_response(PlannedResponse::new(
224            "Confirm significant revenue transactions with customers",
225            ResponseProcedureType::Confirmation,
226            Assertion::Occurrence,
227            &self.select_team_member(team_members, "staff"),
228            engagement.fieldwork_start + Duration::days(21),
229        ));
230        risk.add_response(PlannedResponse::new(
231            "Perform analytical procedures on revenue trends",
232            ResponseProcedureType::AnalyticalProcedure,
233            Assertion::Completeness,
234            &self.select_team_member(team_members, "senior"),
235            engagement.fieldwork_start + Duration::days(7),
236        ));
237
238        let assessor = self.select_team_member(team_members, "manager");
239        risk = risk.with_assessed_by(&assessor, engagement.planning_start);
240        risk.review_status = RiskReviewStatus::Approved;
241        risk.reviewer_id = Some(engagement.engagement_partner_id.clone());
242        risk.review_date = Some(engagement.planning_start + Duration::days(7));
243
244        risk
245    }
246
247    /// Generate presumed management override risk per ISA 240.
248    fn generate_management_override_risk(
249        &mut self,
250        engagement: &AuditEngagement,
251        team_members: &[String],
252    ) -> RiskAssessment {
253        self.risk_counter += 1;
254
255        let mut risk = RiskAssessment::new(
256            engagement.engagement_id,
257            RiskCategory::FraudRisk,
258            "Management Override of Controls",
259            "Presumed risk of management override of controls per ISA 240.31",
260        );
261
262        risk.risk_ref = format!("RISK-{:04}", self.risk_counter);
263        risk = risk.with_risk_levels(RiskLevel::High, RiskLevel::High);
264        risk = risk.mark_significant("Presumed fraud risk per ISA 240 - management override");
265        risk.presumed_management_override = true;
266
267        risk.add_fraud_factor(FraudRiskFactor::new(
268            FraudTriangleElement::Opportunity,
269            "Management has ability to override controls",
270            80,
271            "Control environment assessment",
272        ));
273        risk.add_fraud_factor(FraudRiskFactor::new(
274            FraudTriangleElement::Rationalization,
275            "Tone at the top may not emphasize ethical behavior",
276            50,
277            "Governance inquiries",
278        ));
279
280        risk.response_nature = ResponseNature::SubstantiveOnly;
281        risk.response_timing = ResponseTiming::YearEnd;
282        risk.response_extent = "Mandatory procedures per ISA 240.32-34".into();
283
284        // ISA 240 required responses
285        risk.add_response(PlannedResponse::new(
286            "Test appropriateness of journal entries and adjustments",
287            ResponseProcedureType::TestOfDetails,
288            Assertion::Accuracy,
289            &self.select_team_member(team_members, "senior"),
290            engagement.fieldwork_start + Duration::days(28),
291        ));
292        risk.add_response(PlannedResponse::new(
293            "Review accounting estimates for bias",
294            ResponseProcedureType::AnalyticalProcedure,
295            Assertion::ValuationAndAllocation,
296            &self.select_team_member(team_members, "manager"),
297            engagement.fieldwork_start + Duration::days(35),
298        ));
299        risk.add_response(PlannedResponse::new(
300            "Evaluate business rationale for significant unusual transactions",
301            ResponseProcedureType::Inquiry,
302            Assertion::Occurrence,
303            &self.select_team_member(team_members, "manager"),
304            engagement.fieldwork_start + Duration::days(42),
305        ));
306
307        let assessor = self.select_team_member(team_members, "manager");
308        risk = risk.with_assessed_by(&assessor, engagement.planning_start);
309        risk.review_status = RiskReviewStatus::Approved;
310        risk.reviewer_id = Some(engagement.engagement_partner_id.clone());
311        risk.review_date = Some(engagement.planning_start + Duration::days(7));
312
313        risk
314    }
315
316    /// Get risk areas based on accounts.
317    fn get_risk_areas(&mut self, accounts: &[String]) -> Vec<String> {
318        let mut areas: Vec<String> = if accounts.is_empty() {
319            vec![
320                "Cash and Cash Equivalents".into(),
321                "Accounts Receivable".into(),
322                "Inventory".into(),
323                "Property, Plant and Equipment".into(),
324                "Accounts Payable".into(),
325                "Accrued Liabilities".into(),
326                "Long-term Debt".into(),
327                "Revenue".into(),
328                "Cost of Sales".into(),
329                "Operating Expenses".into(),
330                "Payroll and Benefits".into(),
331                "Income Taxes".into(),
332                "Related Party Transactions".into(),
333                "Financial Statement Disclosures".into(),
334                "IT General Controls".into(),
335            ]
336        } else {
337            accounts.to_vec()
338        };
339
340        // Shuffle and return
341        for i in (1..areas.len()).rev() {
342            let j = self.rng.random_range(0..=i);
343            areas.swap(i, j);
344        }
345        areas
346    }
347
348    /// Select risk category.
349    fn select_risk_category(&mut self) -> RiskCategory {
350        let categories = [
351            (RiskCategory::AssertionLevel, 0.50),
352            (RiskCategory::FinancialStatementLevel, 0.15),
353            (RiskCategory::EstimateRisk, 0.10),
354            (RiskCategory::ItGeneralControl, 0.10),
355            (RiskCategory::RelatedParty, 0.05),
356            (RiskCategory::GoingConcern, 0.05),
357            (RiskCategory::RegulatoryCompliance, 0.05),
358        ];
359
360        let r: f64 = self.rng.random();
361        let mut cumulative = 0.0;
362        for (category, probability) in categories {
363            cumulative += probability;
364            if r < cumulative {
365                return category;
366            }
367        }
368        RiskCategory::AssertionLevel
369    }
370
371    /// Generate risk description and assertion.
372    fn generate_risk_description(
373        &mut self,
374        account_or_process: &str,
375        category: RiskCategory,
376    ) -> (String, Option<Assertion>) {
377        let assertions = [
378            (Assertion::Existence, "existence"),
379            (Assertion::Completeness, "completeness"),
380            (Assertion::Accuracy, "accuracy"),
381            (Assertion::ValuationAndAllocation, "valuation"),
382            (Assertion::Cutoff, "cutoff"),
383            (Assertion::RightsAndObligations, "rights and obligations"),
384            (
385                Assertion::PresentationAndDisclosure,
386                "presentation and disclosure",
387            ),
388        ];
389
390        let idx = self.rng.random_range(0..assertions.len());
391        let (assertion, assertion_name) = assertions[idx];
392
393        let description = match category {
394            RiskCategory::AssertionLevel => {
395                format!(
396                    "Risk that {} is materially misstated due to {}",
397                    account_or_process, assertion_name
398                )
399            }
400            RiskCategory::FinancialStatementLevel => {
401                format!(
402                    "Pervasive risk affecting {} due to control environment weaknesses",
403                    account_or_process
404                )
405            }
406            RiskCategory::EstimateRisk => {
407                format!(
408                    "Risk of material misstatement in {} estimates due to estimation uncertainty",
409                    account_or_process
410                )
411            }
412            RiskCategory::ItGeneralControl => {
413                format!(
414                    "IT general control risk affecting {} data integrity and processing",
415                    account_or_process
416                )
417            }
418            RiskCategory::RelatedParty => {
419                format!(
420                    "Risk of undisclosed related party transactions affecting {}",
421                    account_or_process
422                )
423            }
424            RiskCategory::GoingConcern => {
425                "Risk that the entity may not continue as a going concern".into()
426            }
427            RiskCategory::RegulatoryCompliance => {
428                format!(
429                    "Risk of non-compliance with laws and regulations affecting {}",
430                    account_or_process
431                )
432            }
433            RiskCategory::FraudRisk => {
434                format!("Fraud risk in {}", account_or_process)
435            }
436        };
437
438        let assertion_opt = match category {
439            RiskCategory::AssertionLevel | RiskCategory::EstimateRisk => Some(assertion),
440            _ => None,
441        };
442
443        (description, assertion_opt)
444    }
445
446    /// Generate inherent risk level.
447    fn generate_inherent_risk(&mut self) -> RiskLevel {
448        if self.rng.random::<f64>() < self.config.high_inherent_risk_probability {
449            RiskLevel::High
450        } else if self.rng.random::<f64>() < 0.5 {
451            RiskLevel::Medium
452        } else {
453            RiskLevel::Low
454        }
455    }
456
457    /// Generate control risk based on inherent risk.
458    fn generate_control_risk(&mut self, inherent_risk: &RiskLevel) -> RiskLevel {
459        // Control risk tends to be correlated with inherent risk
460        match inherent_risk {
461            RiskLevel::High | RiskLevel::Significant => {
462                if self.rng.random::<f64>() < 0.6 {
463                    RiskLevel::High
464                } else {
465                    RiskLevel::Medium
466                }
467            }
468            RiskLevel::Medium => {
469                if self.rng.random::<f64>() < 0.4 {
470                    RiskLevel::Medium
471                } else if self.rng.random::<f64>() < 0.7 {
472                    RiskLevel::Low
473                } else {
474                    RiskLevel::High
475                }
476            }
477            RiskLevel::Low => {
478                if self.rng.random::<f64>() < 0.7 {
479                    RiskLevel::Low
480                } else {
481                    RiskLevel::Medium
482                }
483            }
484        }
485    }
486
487    /// Generate significant risk rationale.
488    fn generate_significant_risk_rationale(&mut self, category: RiskCategory) -> String {
489        match category {
490            RiskCategory::FraudRisk => {
491                "Fraud risk requiring special audit consideration per ISA 240".into()
492            }
493            RiskCategory::EstimateRisk => {
494                "High estimation uncertainty requiring special audit consideration per ISA 540"
495                    .into()
496            }
497            RiskCategory::RelatedParty => {
498                "Related party transactions outside normal course of business per ISA 550".into()
499            }
500            RiskCategory::GoingConcern => {
501                "Significant doubt about going concern per ISA 570".into()
502            }
503            _ => {
504                let rationales = [
505                    "High inherent risk combined with weak control environment",
506                    "Significant management judgment involved",
507                    "Complex transactions requiring specialized knowledge",
508                    "History of misstatements in this area",
509                    "New accounting standard implementation",
510                ];
511                let idx = self.rng.random_range(0..rationales.len());
512                rationales[idx].into()
513            }
514        }
515    }
516
517    /// Generate fraud risk factors.
518    fn generate_fraud_factors(&mut self) -> Vec<FraudRiskFactor> {
519        let mut factors = Vec::new();
520        let count = self.rng.random_range(1..=3);
521
522        let pressure_indicators = [
523            "Financial pressure from debt covenants",
524            "Compensation tied to financial targets",
525            "Industry decline affecting profitability",
526            "Unrealistic budget expectations",
527        ];
528
529        let opportunity_indicators = [
530            "Weak segregation of duties",
531            "Lack of independent oversight",
532            "Complex organizational structure",
533            "Inadequate monitoring of controls",
534        ];
535
536        let rationalization_indicators = [
537            "History of management explanations for variances",
538            "Aggressive accounting policies",
539            "Frequent disputes with auditors",
540            "Strained relationship with regulators",
541        ];
542
543        for _ in 0..count {
544            let element = match self.rng.random_range(0..3) {
545                0 => {
546                    let idx = self.rng.random_range(0..pressure_indicators.len());
547                    FraudRiskFactor::new(
548                        FraudTriangleElement::Pressure,
549                        pressure_indicators[idx],
550                        self.rng.random_range(40..90),
551                        "Risk assessment procedures",
552                    )
553                }
554                1 => {
555                    let idx = self.rng.random_range(0..opportunity_indicators.len());
556                    FraudRiskFactor::new(
557                        FraudTriangleElement::Opportunity,
558                        opportunity_indicators[idx],
559                        self.rng.random_range(40..90),
560                        "Control environment assessment",
561                    )
562                }
563                _ => {
564                    let idx = self.rng.random_range(0..rationalization_indicators.len());
565                    FraudRiskFactor::new(
566                        FraudTriangleElement::Rationalization,
567                        rationalization_indicators[idx],
568                        self.rng.random_range(30..70),
569                        "Management inquiries",
570                    )
571                }
572            };
573
574            let trend = match self.rng.random_range(0..3) {
575                0 => Trend::Increasing,
576                1 => Trend::Stable,
577                _ => Trend::Decreasing,
578            };
579
580            factors.push(element.with_trend(trend));
581        }
582
583        factors
584    }
585
586    /// Select response nature based on risk.
587    fn select_response_nature(&mut self, risk: &RiskAssessment) -> ResponseNature {
588        match risk.risk_of_material_misstatement {
589            RiskLevel::High | RiskLevel::Significant => ResponseNature::SubstantiveOnly,
590            RiskLevel::Medium => {
591                if self.rng.random::<f64>() < 0.6 {
592                    ResponseNature::Combined
593                } else {
594                    ResponseNature::SubstantiveOnly
595                }
596            }
597            RiskLevel::Low => {
598                if self.rng.random::<f64>() < 0.4 {
599                    ResponseNature::ControlsReliance
600                } else {
601                    ResponseNature::Combined
602                }
603            }
604        }
605    }
606
607    /// Select response timing.
608    fn select_response_timing(&mut self, engagement: &AuditEngagement) -> ResponseTiming {
609        match engagement.current_phase {
610            EngagementPhase::Planning | EngagementPhase::RiskAssessment => {
611                if self.rng.random::<f64>() < 0.3 {
612                    ResponseTiming::Interim
613                } else {
614                    ResponseTiming::YearEnd
615                }
616            }
617            _ => ResponseTiming::YearEnd,
618        }
619    }
620
621    /// Generate response extent description.
622    fn generate_response_extent(&mut self, risk: &RiskAssessment) -> String {
623        match risk.required_detection_risk() {
624            DetectionRisk::Low => {
625                "Extended testing with larger sample sizes and unpredictable procedures".into()
626            }
627            DetectionRisk::Medium => {
628                "Moderate sample sizes with standard testing procedures".into()
629            }
630            DetectionRisk::High => {
631                "Reduced testing extent with reliance on analytical procedures".into()
632            }
633        }
634    }
635
636    /// Generate a planned response.
637    fn generate_planned_response(
638        &mut self,
639        risk: &RiskAssessment,
640        team_members: &[String],
641        engagement: &AuditEngagement,
642    ) -> PlannedResponse {
643        let procedure_type = self.select_procedure_type(&risk.response_nature);
644        let assertion = risk.assertion.unwrap_or_else(|| self.random_assertion());
645        let procedure =
646            self.generate_procedure_description(procedure_type, &risk.account_or_process);
647
648        let days_offset = self.rng.random_range(7..45);
649        let target_date = engagement.fieldwork_start + Duration::days(days_offset);
650
651        let mut response = PlannedResponse::new(
652            &procedure,
653            procedure_type,
654            assertion,
655            &self.select_team_member(team_members, "staff"),
656            target_date,
657        );
658
659        // Maybe mark as in progress or complete
660        if self.rng.random::<f64>() < 0.2 {
661            response.status = ResponseStatus::InProgress;
662        }
663
664        response
665    }
666
667    /// Select procedure type based on response nature.
668    fn select_procedure_type(&mut self, nature: &ResponseNature) -> ResponseProcedureType {
669        match nature {
670            ResponseNature::ControlsReliance => {
671                if self.rng.random::<f64>() < 0.7 {
672                    ResponseProcedureType::TestOfControls
673                } else {
674                    ResponseProcedureType::Inquiry
675                }
676            }
677            ResponseNature::SubstantiveOnly => {
678                let types = [
679                    ResponseProcedureType::TestOfDetails,
680                    ResponseProcedureType::AnalyticalProcedure,
681                    ResponseProcedureType::Confirmation,
682                    ResponseProcedureType::PhysicalInspection,
683                ];
684                let idx = self.rng.random_range(0..types.len());
685                types[idx]
686            }
687            ResponseNature::Combined => {
688                let types = [
689                    ResponseProcedureType::TestOfControls,
690                    ResponseProcedureType::TestOfDetails,
691                    ResponseProcedureType::AnalyticalProcedure,
692                ];
693                let idx = self.rng.random_range(0..types.len());
694                types[idx]
695            }
696        }
697    }
698
699    /// Generate procedure description.
700    fn generate_procedure_description(
701        &mut self,
702        procedure_type: ResponseProcedureType,
703        account: &str,
704    ) -> String {
705        match procedure_type {
706            ResponseProcedureType::TestOfControls => {
707                format!("Test operating effectiveness of controls over {}", account)
708            }
709            ResponseProcedureType::TestOfDetails => {
710                format!(
711                    "Select sample of {} transactions and vouch to supporting documentation",
712                    account
713                )
714            }
715            ResponseProcedureType::AnalyticalProcedure => {
716                format!(
717                    "Perform analytical procedures on {} and investigate variances",
718                    account
719                )
720            }
721            ResponseProcedureType::Confirmation => {
722                format!("Send confirmations for {} balances", account)
723            }
724            ResponseProcedureType::PhysicalInspection => {
725                format!("Physically inspect {} items", account)
726            }
727            ResponseProcedureType::Inquiry => {
728                format!("Inquire of management regarding {} processes", account)
729            }
730        }
731    }
732
733    /// Select a team member.
734    fn select_team_member(&mut self, team_members: &[String], role_hint: &str) -> String {
735        let matching: Vec<&String> = team_members
736            .iter()
737            .filter(|m| m.to_lowercase().contains(role_hint))
738            .collect();
739
740        if let Some(&member) = matching.first() {
741            member.clone()
742        } else if !team_members.is_empty() {
743            let idx = self.rng.random_range(0..team_members.len());
744            team_members[idx].clone()
745        } else {
746            format!("{}001", role_hint.to_uppercase())
747        }
748    }
749
750    /// Generate a random assertion.
751    fn random_assertion(&mut self) -> Assertion {
752        let assertions = [
753            Assertion::Occurrence,
754            Assertion::Completeness,
755            Assertion::Accuracy,
756            Assertion::Cutoff,
757            Assertion::Existence,
758            Assertion::ValuationAndAllocation,
759        ];
760        let idx = self.rng.random_range(0..assertions.len());
761        assertions[idx]
762    }
763}
764
765#[cfg(test)]
766#[allow(clippy::unwrap_used)]
767mod tests {
768    use super::*;
769    use crate::audit::test_helpers::create_test_engagement;
770
771    #[test]
772    fn test_risk_generation() {
773        let mut generator = RiskAssessmentGenerator::new(42);
774        let engagement = create_test_engagement();
775        let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
776
777        let risks = generator.generate_risks_for_engagement(&engagement, &team, &[]);
778
779        assert!(risks.len() >= 2); // At least presumed risks
780
781        // Check for presumed risks
782        let has_revenue_fraud = risks.iter().any(|r| r.presumed_revenue_fraud_risk);
783        let has_mgmt_override = risks.iter().any(|r| r.presumed_management_override);
784        assert!(has_revenue_fraud);
785        assert!(has_mgmt_override);
786    }
787
788    #[test]
789    fn test_significant_risk() {
790        let mut generator = RiskAssessmentGenerator::new(42);
791        let engagement = create_test_engagement();
792        let team = vec!["STAFF001".into()];
793
794        let risks = generator.generate_risks_for_engagement(&engagement, &team, &[]);
795
796        // Presumed risks should be significant
797        let significant_risks: Vec<_> = risks.iter().filter(|r| r.is_significant_risk).collect();
798        assert!(significant_risks.len() >= 2);
799    }
800
801    #[test]
802    fn test_planned_responses() {
803        let mut generator = RiskAssessmentGenerator::new(42);
804        let engagement = create_test_engagement();
805        let team = vec!["STAFF001".into(), "SENIOR001".into()];
806
807        let risks = generator.generate_risks_for_engagement(&engagement, &team, &[]);
808
809        for risk in &risks {
810            assert!(!risk.planned_response.is_empty());
811        }
812    }
813
814    #[test]
815    fn test_fraud_factors() {
816        let config = RiskAssessmentGeneratorConfig {
817            fraud_factor_probability: 1.0,
818            ..Default::default()
819        };
820        let mut generator = RiskAssessmentGenerator::with_config(42, config);
821        let engagement = create_test_engagement();
822
823        let _risk =
824            generator.generate_risk_assessment(&engagement, "Inventory", &["STAFF001".into()]);
825
826        // May or may not have fraud factors depending on risk category
827        // But presumed risks always have them
828    }
829
830    #[test]
831    fn test_detection_risk() {
832        let mut generator = RiskAssessmentGenerator::new(42);
833        let engagement = create_test_engagement();
834
835        let risks = generator.generate_risks_for_engagement(&engagement, &["STAFF001".into()], &[]);
836
837        for risk in &risks {
838            let detection_risk = risk.required_detection_risk();
839            // High ROMM should require low detection risk
840            if matches!(
841                risk.risk_of_material_misstatement,
842                RiskLevel::High | RiskLevel::Significant
843            ) {
844                assert_eq!(detection_risk, DetectionRisk::Low);
845            }
846        }
847    }
848}