1use 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#[derive(Debug, Clone)]
18pub struct RiskAssessmentGeneratorConfig {
19 pub risks_per_engagement: (u32, u32),
21 pub significant_risk_probability: f64,
23 pub high_inherent_risk_probability: f64,
25 pub fraud_factor_probability: f64,
27 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
43pub struct RiskAssessmentGenerator {
45 rng: ChaCha8Rng,
46 config: RiskAssessmentGeneratorConfig,
47 risk_counter: u32,
48}
49
50impl RiskAssessmentGenerator {
51 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 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 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 risks.push(self.generate_revenue_fraud_risk(engagement, team_members));
84 risks.push(self.generate_management_override_risk(engagement, team_members));
85
86 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 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 if let Some(assertion) = assertion {
120 risk = risk.with_assertion(assertion);
121 }
122
123 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 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 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 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 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 let assessor = self.select_team_member(team_members, "senior");
163 risk = risk.with_assessed_by(&assessor, engagement.planning_start + Duration::days(7));
164
165 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 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 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 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 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 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 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 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 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 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 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 fn generate_control_risk(&mut self, inherent_risk: &RiskLevel) -> RiskLevel {
458 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 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 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 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 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 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 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 if self.rng.gen::<f64>() < 0.2 {
660 response.status = ResponseStatus::InProgress;
661 }
662
663 response
664 }
665
666 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 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 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 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); 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 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 }
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 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}