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 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 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 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 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 let assessor = self.select_team_member(team_members, "senior");
164 risk = risk.with_assessed_by(&assessor, engagement.planning_start + Duration::days(7));
165
166 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 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 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 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 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 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 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 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 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 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 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 fn generate_control_risk(&mut self, inherent_risk: &RiskLevel) -> RiskLevel {
453 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 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 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 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 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 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 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 if self.rng.random::<f64>() < 0.2 {
655 response.status = ResponseStatus::InProgress;
656 }
657
658 response
659 }
660
661 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 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 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 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); 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 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 }
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 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}