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 {account_or_process} is materially misstated due to {assertion_name}"
397                )
398            }
399            RiskCategory::FinancialStatementLevel => {
400                format!(
401                    "Pervasive risk affecting {account_or_process} due to control environment weaknesses"
402                )
403            }
404            RiskCategory::EstimateRisk => {
405                format!(
406                    "Risk of material misstatement in {account_or_process} estimates due to estimation uncertainty"
407                )
408            }
409            RiskCategory::ItGeneralControl => {
410                format!(
411                    "IT general control risk affecting {account_or_process} data integrity and processing"
412                )
413            }
414            RiskCategory::RelatedParty => {
415                format!(
416                    "Risk of undisclosed related party transactions affecting {account_or_process}"
417                )
418            }
419            RiskCategory::GoingConcern => {
420                "Risk that the entity may not continue as a going concern".into()
421            }
422            RiskCategory::RegulatoryCompliance => {
423                format!(
424                    "Risk of non-compliance with laws and regulations affecting {account_or_process}"
425                )
426            }
427            RiskCategory::FraudRisk => {
428                format!("Fraud risk in {account_or_process}")
429            }
430        };
431
432        let assertion_opt = match category {
433            RiskCategory::AssertionLevel | RiskCategory::EstimateRisk => Some(assertion),
434            _ => None,
435        };
436
437        (description, assertion_opt)
438    }
439
440    /// Generate inherent risk level.
441    fn generate_inherent_risk(&mut self) -> RiskLevel {
442        if self.rng.random::<f64>() < self.config.high_inherent_risk_probability {
443            RiskLevel::High
444        } else if self.rng.random::<f64>() < 0.5 {
445            RiskLevel::Medium
446        } else {
447            RiskLevel::Low
448        }
449    }
450
451    /// Generate control risk based on inherent risk.
452    fn generate_control_risk(&mut self, inherent_risk: &RiskLevel) -> RiskLevel {
453        // Control risk tends to be correlated with inherent risk
454        match inherent_risk {
455            RiskLevel::High | RiskLevel::Significant => {
456                if self.rng.random::<f64>() < 0.6 {
457                    RiskLevel::High
458                } else {
459                    RiskLevel::Medium
460                }
461            }
462            RiskLevel::Medium => {
463                if self.rng.random::<f64>() < 0.4 {
464                    RiskLevel::Medium
465                } else if self.rng.random::<f64>() < 0.7 {
466                    RiskLevel::Low
467                } else {
468                    RiskLevel::High
469                }
470            }
471            RiskLevel::Low => {
472                if self.rng.random::<f64>() < 0.7 {
473                    RiskLevel::Low
474                } else {
475                    RiskLevel::Medium
476                }
477            }
478        }
479    }
480
481    /// Generate significant risk rationale.
482    fn generate_significant_risk_rationale(&mut self, category: RiskCategory) -> String {
483        match category {
484            RiskCategory::FraudRisk => {
485                "Fraud risk requiring special audit consideration per ISA 240".into()
486            }
487            RiskCategory::EstimateRisk => {
488                "High estimation uncertainty requiring special audit consideration per ISA 540"
489                    .into()
490            }
491            RiskCategory::RelatedParty => {
492                "Related party transactions outside normal course of business per ISA 550".into()
493            }
494            RiskCategory::GoingConcern => {
495                "Significant doubt about going concern per ISA 570".into()
496            }
497            _ => {
498                let rationales = [
499                    "High inherent risk combined with weak control environment",
500                    "Significant management judgment involved",
501                    "Complex transactions requiring specialized knowledge",
502                    "History of misstatements in this area",
503                    "New accounting standard implementation",
504                ];
505                let idx = self.rng.random_range(0..rationales.len());
506                rationales[idx].into()
507            }
508        }
509    }
510
511    /// Generate fraud risk factors.
512    fn generate_fraud_factors(&mut self) -> Vec<FraudRiskFactor> {
513        let mut factors = Vec::new();
514        let count = self.rng.random_range(1..=3);
515
516        let pressure_indicators = [
517            "Financial pressure from debt covenants",
518            "Compensation tied to financial targets",
519            "Industry decline affecting profitability",
520            "Unrealistic budget expectations",
521        ];
522
523        let opportunity_indicators = [
524            "Weak segregation of duties",
525            "Lack of independent oversight",
526            "Complex organizational structure",
527            "Inadequate monitoring of controls",
528        ];
529
530        let rationalization_indicators = [
531            "History of management explanations for variances",
532            "Aggressive accounting policies",
533            "Frequent disputes with auditors",
534            "Strained relationship with regulators",
535        ];
536
537        for _ in 0..count {
538            let element = match self.rng.random_range(0..3) {
539                0 => {
540                    let idx = self.rng.random_range(0..pressure_indicators.len());
541                    FraudRiskFactor::new(
542                        FraudTriangleElement::Pressure,
543                        pressure_indicators[idx],
544                        self.rng.random_range(40..90),
545                        "Risk assessment procedures",
546                    )
547                }
548                1 => {
549                    let idx = self.rng.random_range(0..opportunity_indicators.len());
550                    FraudRiskFactor::new(
551                        FraudTriangleElement::Opportunity,
552                        opportunity_indicators[idx],
553                        self.rng.random_range(40..90),
554                        "Control environment assessment",
555                    )
556                }
557                _ => {
558                    let idx = self.rng.random_range(0..rationalization_indicators.len());
559                    FraudRiskFactor::new(
560                        FraudTriangleElement::Rationalization,
561                        rationalization_indicators[idx],
562                        self.rng.random_range(30..70),
563                        "Management inquiries",
564                    )
565                }
566            };
567
568            let trend = match self.rng.random_range(0..3) {
569                0 => Trend::Increasing,
570                1 => Trend::Stable,
571                _ => Trend::Decreasing,
572            };
573
574            factors.push(element.with_trend(trend));
575        }
576
577        factors
578    }
579
580    /// Select response nature based on risk.
581    fn select_response_nature(&mut self, risk: &RiskAssessment) -> ResponseNature {
582        match risk.risk_of_material_misstatement {
583            RiskLevel::High | RiskLevel::Significant => ResponseNature::SubstantiveOnly,
584            RiskLevel::Medium => {
585                if self.rng.random::<f64>() < 0.6 {
586                    ResponseNature::Combined
587                } else {
588                    ResponseNature::SubstantiveOnly
589                }
590            }
591            RiskLevel::Low => {
592                if self.rng.random::<f64>() < 0.4 {
593                    ResponseNature::ControlsReliance
594                } else {
595                    ResponseNature::Combined
596                }
597            }
598        }
599    }
600
601    /// Select response timing.
602    fn select_response_timing(&mut self, engagement: &AuditEngagement) -> ResponseTiming {
603        match engagement.current_phase {
604            EngagementPhase::Planning | EngagementPhase::RiskAssessment => {
605                if self.rng.random::<f64>() < 0.3 {
606                    ResponseTiming::Interim
607                } else {
608                    ResponseTiming::YearEnd
609                }
610            }
611            _ => ResponseTiming::YearEnd,
612        }
613    }
614
615    /// Generate response extent description.
616    fn generate_response_extent(&mut self, risk: &RiskAssessment) -> String {
617        match risk.required_detection_risk() {
618            DetectionRisk::Low => {
619                "Extended testing with larger sample sizes and unpredictable procedures".into()
620            }
621            DetectionRisk::Medium => {
622                "Moderate sample sizes with standard testing procedures".into()
623            }
624            DetectionRisk::High => {
625                "Reduced testing extent with reliance on analytical procedures".into()
626            }
627        }
628    }
629
630    /// Generate a planned response.
631    fn generate_planned_response(
632        &mut self,
633        risk: &RiskAssessment,
634        team_members: &[String],
635        engagement: &AuditEngagement,
636    ) -> PlannedResponse {
637        let procedure_type = self.select_procedure_type(&risk.response_nature);
638        let assertion = risk.assertion.unwrap_or_else(|| self.random_assertion());
639        let procedure =
640            self.generate_procedure_description(procedure_type, &risk.account_or_process);
641
642        let days_offset = self.rng.random_range(7..45);
643        let target_date = engagement.fieldwork_start + Duration::days(days_offset);
644
645        let mut response = PlannedResponse::new(
646            &procedure,
647            procedure_type,
648            assertion,
649            &self.select_team_member(team_members, "staff"),
650            target_date,
651        );
652
653        // Maybe mark as in progress or complete
654        if self.rng.random::<f64>() < 0.2 {
655            response.status = ResponseStatus::InProgress;
656        }
657
658        response
659    }
660
661    /// Select procedure type based on response nature.
662    fn select_procedure_type(&mut self, nature: &ResponseNature) -> ResponseProcedureType {
663        match nature {
664            ResponseNature::ControlsReliance => {
665                if self.rng.random::<f64>() < 0.7 {
666                    ResponseProcedureType::TestOfControls
667                } else {
668                    ResponseProcedureType::Inquiry
669                }
670            }
671            ResponseNature::SubstantiveOnly => {
672                let types = [
673                    ResponseProcedureType::TestOfDetails,
674                    ResponseProcedureType::AnalyticalProcedure,
675                    ResponseProcedureType::Confirmation,
676                    ResponseProcedureType::PhysicalInspection,
677                ];
678                let idx = self.rng.random_range(0..types.len());
679                types[idx]
680            }
681            ResponseNature::Combined => {
682                let types = [
683                    ResponseProcedureType::TestOfControls,
684                    ResponseProcedureType::TestOfDetails,
685                    ResponseProcedureType::AnalyticalProcedure,
686                ];
687                let idx = self.rng.random_range(0..types.len());
688                types[idx]
689            }
690        }
691    }
692
693    /// Generate procedure description.
694    fn generate_procedure_description(
695        &mut self,
696        procedure_type: ResponseProcedureType,
697        account: &str,
698    ) -> String {
699        match procedure_type {
700            ResponseProcedureType::TestOfControls => {
701                format!("Test operating effectiveness of controls over {account}")
702            }
703            ResponseProcedureType::TestOfDetails => {
704                format!(
705                    "Select sample of {account} transactions and vouch to supporting documentation"
706                )
707            }
708            ResponseProcedureType::AnalyticalProcedure => {
709                format!("Perform analytical procedures on {account} and investigate variances")
710            }
711            ResponseProcedureType::Confirmation => {
712                format!("Send confirmations for {account} balances")
713            }
714            ResponseProcedureType::PhysicalInspection => {
715                format!("Physically inspect {account} items")
716            }
717            ResponseProcedureType::Inquiry => {
718                format!("Inquire of management regarding {account} processes")
719            }
720        }
721    }
722
723    /// Select a team member.
724    fn select_team_member(&mut self, team_members: &[String], role_hint: &str) -> String {
725        let matching: Vec<&String> = team_members
726            .iter()
727            .filter(|m| m.to_lowercase().contains(role_hint))
728            .collect();
729
730        if let Some(&member) = matching.first() {
731            member.clone()
732        } else if !team_members.is_empty() {
733            let idx = self.rng.random_range(0..team_members.len());
734            team_members[idx].clone()
735        } else {
736            format!("{}001", role_hint.to_uppercase())
737        }
738    }
739
740    /// Generate a random assertion.
741    fn random_assertion(&mut self) -> Assertion {
742        let assertions = [
743            Assertion::Occurrence,
744            Assertion::Completeness,
745            Assertion::Accuracy,
746            Assertion::Cutoff,
747            Assertion::Existence,
748            Assertion::ValuationAndAllocation,
749        ];
750        let idx = self.rng.random_range(0..assertions.len());
751        assertions[idx]
752    }
753}
754
755#[cfg(test)]
756#[allow(clippy::unwrap_used)]
757mod tests {
758    use super::*;
759    use crate::audit::test_helpers::create_test_engagement;
760
761    #[test]
762    fn test_risk_generation() {
763        let mut generator = RiskAssessmentGenerator::new(42);
764        let engagement = create_test_engagement();
765        let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
766
767        let risks = generator.generate_risks_for_engagement(&engagement, &team, &[]);
768
769        assert!(risks.len() >= 2); // At least presumed risks
770
771        // Check for presumed risks
772        let has_revenue_fraud = risks.iter().any(|r| r.presumed_revenue_fraud_risk);
773        let has_mgmt_override = risks.iter().any(|r| r.presumed_management_override);
774        assert!(has_revenue_fraud);
775        assert!(has_mgmt_override);
776    }
777
778    #[test]
779    fn test_significant_risk() {
780        let mut generator = RiskAssessmentGenerator::new(42);
781        let engagement = create_test_engagement();
782        let team = vec!["STAFF001".into()];
783
784        let risks = generator.generate_risks_for_engagement(&engagement, &team, &[]);
785
786        // Presumed risks should be significant
787        let significant_risks: Vec<_> = risks.iter().filter(|r| r.is_significant_risk).collect();
788        assert!(significant_risks.len() >= 2);
789    }
790
791    #[test]
792    fn test_planned_responses() {
793        let mut generator = RiskAssessmentGenerator::new(42);
794        let engagement = create_test_engagement();
795        let team = vec!["STAFF001".into(), "SENIOR001".into()];
796
797        let risks = generator.generate_risks_for_engagement(&engagement, &team, &[]);
798
799        for risk in &risks {
800            assert!(!risk.planned_response.is_empty());
801        }
802    }
803
804    #[test]
805    fn test_fraud_factors() {
806        let config = RiskAssessmentGeneratorConfig {
807            fraud_factor_probability: 1.0,
808            ..Default::default()
809        };
810        let mut generator = RiskAssessmentGenerator::with_config(42, config);
811        let engagement = create_test_engagement();
812
813        let _risk =
814            generator.generate_risk_assessment(&engagement, "Inventory", &["STAFF001".into()]);
815
816        // May or may not have fraud factors depending on risk category
817        // But presumed risks always have them
818    }
819
820    #[test]
821    fn test_detection_risk() {
822        let mut generator = RiskAssessmentGenerator::new(42);
823        let engagement = create_test_engagement();
824
825        let risks = generator.generate_risks_for_engagement(&engagement, &["STAFF001".into()], &[]);
826
827        for risk in &risks {
828            let detection_risk = risk.required_detection_risk();
829            // High ROMM should require low detection risk
830            if matches!(
831                risk.risk_of_material_misstatement,
832                RiskLevel::High | RiskLevel::Significant
833            ) {
834                assert_eq!(detection_risk, DetectionRisk::Low);
835            }
836        }
837    }
838}