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