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