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