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