1use chrono::{Duration, NaiveDate};
7use datasynth_core::utils::seeded_rng;
8use rand::Rng;
9use rand_chacha::ChaCha8Rng;
10
11use datasynth_core::models::audit::{
12 AlternativeEvaluation, AuditEngagement, ConsultationRecord, InformationItem,
13 InformationReliability, InformationWeight, JudgmentStatus, JudgmentType, ProfessionalJudgment,
14 RiskLevel, SkepticismDocumentation,
15};
16
17#[derive(Debug, Clone)]
19pub struct JudgmentGeneratorConfig {
20 pub judgments_per_engagement: (u32, u32),
22 pub consultation_probability: f64,
24 pub information_items_range: (u32, u32),
26 pub alternatives_range: (u32, u32),
28}
29
30impl Default for JudgmentGeneratorConfig {
31 fn default() -> Self {
32 Self {
33 judgments_per_engagement: (5, 15),
34 consultation_probability: 0.25,
35 information_items_range: (2, 6),
36 alternatives_range: (2, 4),
37 }
38 }
39}
40
41pub struct JudgmentGenerator {
43 rng: ChaCha8Rng,
44 config: JudgmentGeneratorConfig,
45 judgment_counter: u32,
46 fiscal_year: u16,
47}
48
49impl JudgmentGenerator {
50 pub fn new(seed: u64) -> Self {
52 Self {
53 rng: seeded_rng(seed, 0),
54 config: JudgmentGeneratorConfig::default(),
55 judgment_counter: 0,
56 fiscal_year: 2025,
57 }
58 }
59
60 pub fn with_config(seed: u64, config: JudgmentGeneratorConfig) -> Self {
62 Self {
63 rng: seeded_rng(seed, 0),
64 config,
65 judgment_counter: 0,
66 fiscal_year: 2025,
67 }
68 }
69
70 pub fn generate_judgments_for_engagement(
72 &mut self,
73 engagement: &AuditEngagement,
74 team_members: &[String],
75 ) -> Vec<ProfessionalJudgment> {
76 self.fiscal_year = engagement.fiscal_year;
77
78 let count = self.rng.gen_range(
79 self.config.judgments_per_engagement.0..=self.config.judgments_per_engagement.1,
80 );
81
82 let mut judgments = Vec::with_capacity(count as usize);
83
84 judgments.push(self.generate_materiality_judgment(engagement, team_members));
86
87 for _ in 1..count {
89 let judgment = self.generate_judgment(engagement, team_members);
90 judgments.push(judgment);
91 }
92
93 judgments
94 }
95
96 pub fn generate_judgment(
98 &mut self,
99 engagement: &AuditEngagement,
100 team_members: &[String],
101 ) -> ProfessionalJudgment {
102 self.judgment_counter += 1;
103
104 let judgment_type = self.select_judgment_type();
105 let subject = self.generate_subject(judgment_type);
106
107 let mut judgment =
108 ProfessionalJudgment::new(engagement.engagement_id, judgment_type, &subject);
109
110 judgment.judgment_ref = format!("JDG-{}-{:03}", self.fiscal_year, self.judgment_counter);
111
112 let issue = self.generate_issue_description(judgment_type);
114 judgment = judgment.with_issue(&issue);
115
116 let info_count = self.rng.gen_range(
118 self.config.information_items_range.0..=self.config.information_items_range.1,
119 );
120 for _ in 0..info_count {
121 let item = self.generate_information_item(judgment_type);
122 judgment.add_information(item);
123 }
124
125 let alt_count = self
127 .rng
128 .gen_range(self.config.alternatives_range.0..=self.config.alternatives_range.1);
129 let alternatives = self.generate_alternatives(judgment_type, alt_count);
130 for alt in alternatives {
131 judgment.add_alternative(alt);
132 }
133
134 let skepticism = self.generate_skepticism_documentation(judgment_type);
136 judgment = judgment.with_skepticism(skepticism);
137
138 let (conclusion, rationale, residual_risk) = self.generate_conclusion(judgment_type);
140 judgment = judgment.with_conclusion(&conclusion, &rationale, &residual_risk);
141
142 let preparer = self.select_team_member(team_members, "manager");
144 let preparer_name = self.generate_name();
145 let preparer_date = engagement.planning_start + Duration::days(self.rng.gen_range(5..20));
146 judgment = judgment.with_preparer(&preparer, &preparer_name, preparer_date);
147
148 if self.rng.gen::<f64>() < 0.9 {
150 let reviewer = self.select_team_member(team_members, "senior");
151 let reviewer_name = self.generate_name();
152 let review_date = preparer_date + Duration::days(self.rng.gen_range(3..10));
153 judgment.add_review(&reviewer, &reviewer_name, review_date);
154 }
155
156 if judgment.partner_concurrence_required && self.rng.gen::<f64>() < 0.8 {
158 let partner = engagement.engagement_partner_id.clone();
159 let partner_date = preparer_date + Duration::days(self.rng.gen_range(7..14));
160 judgment.add_partner_concurrence(&partner, partner_date);
161 }
162
163 if judgment.consultation_required
165 || self.rng.gen::<f64>() < self.config.consultation_probability
166 {
167 let consultation = self.generate_consultation(judgment_type, preparer_date);
168 judgment.add_consultation(consultation);
169 }
170
171 judgment.status = if judgment.is_approved() {
173 JudgmentStatus::Approved
174 } else if judgment.reviewer_id.is_some() {
175 JudgmentStatus::Reviewed
176 } else {
177 JudgmentStatus::PendingReview
178 };
179
180 judgment
181 }
182
183 fn generate_materiality_judgment(
185 &mut self,
186 engagement: &AuditEngagement,
187 team_members: &[String],
188 ) -> ProfessionalJudgment {
189 self.judgment_counter += 1;
190
191 let mut judgment = ProfessionalJudgment::new(
192 engagement.engagement_id,
193 JudgmentType::MaterialityDetermination,
194 "Overall Audit Materiality",
195 );
196
197 judgment.judgment_ref = format!(
198 "JDG-{}-{:03}",
199 engagement.fiscal_year, self.judgment_counter
200 );
201
202 judgment = judgment.with_issue(
203 "Determination of overall materiality, performance materiality, and clearly trivial \
204 threshold for the audit of the financial statements.",
205 );
206
207 judgment.add_information(
209 InformationItem::new(
210 "Prior year audited financial statements",
211 "Audited financial statements",
212 InformationReliability::High,
213 "Establishes baseline for materiality calculation",
214 )
215 .with_weight(InformationWeight::High),
216 );
217
218 judgment.add_information(
219 InformationItem::new(
220 "Current year budget and forecasts",
221 "Management-prepared projections",
222 InformationReliability::Medium,
223 "Provides expectation for current year metrics",
224 )
225 .with_weight(InformationWeight::Moderate),
226 );
227
228 judgment.add_information(
229 InformationItem::new(
230 "Industry benchmarks for materiality",
231 "Firm guidance and industry data",
232 InformationReliability::High,
233 "Supports selection of appropriate percentage",
234 )
235 .with_weight(InformationWeight::High),
236 );
237
238 judgment.add_information(
239 InformationItem::new(
240 "User expectations and stakeholder considerations",
241 "Knowledge of the entity and environment",
242 InformationReliability::Medium,
243 "Informs selection of appropriate benchmark",
244 )
245 .with_weight(InformationWeight::Moderate),
246 );
247
248 judgment.add_alternative(
250 AlternativeEvaluation::new(
251 "Use total revenue as materiality base",
252 vec![
253 "Stable metric year over year".into(),
254 "Primary focus of financial statement users".into(),
255 "Consistent with prior year approach".into(),
256 ],
257 vec!["May not capture balance sheet focused risks".into()],
258 )
259 .select(),
260 );
261
262 judgment.add_alternative(
263 AlternativeEvaluation::new(
264 "Use total assets as materiality base",
265 vec!["Appropriate for asset-intensive industries".into()],
266 vec![
267 "Less relevant for this entity".into(),
268 "Assets more volatile than revenue".into(),
269 ],
270 )
271 .reject("Revenue is more relevant to primary users of the financial statements"),
272 );
273
274 judgment.add_alternative(
275 AlternativeEvaluation::new(
276 "Use net income as materiality base",
277 vec!["Direct measure of profitability".into()],
278 vec![
279 "Net income is volatile".into(),
280 "Not appropriate when near breakeven".into(),
281 ],
282 )
283 .reject("Net income volatility makes it unsuitable as a stable benchmark"),
284 );
285
286 judgment = judgment.with_skepticism(
288 SkepticismDocumentation::new(
289 "Materiality calculation and benchmark selection reviewed critically",
290 )
291 .with_contradictory_evidence(vec![
292 "Considered whether management might prefer higher materiality to reduce audit scope".into(),
293 ])
294 .with_bias_indicators(vec![
295 "Evaluated if selected benchmark minimizes likely misstatements".into(),
296 ])
297 .with_alternatives(vec![
298 "Considered multiple benchmarks and percentage ranges".into(),
299 ]),
300 );
301
302 let materiality_desc = format!(
304 "Set overall materiality at ${} based on {}% of {}",
305 engagement.materiality,
306 engagement.materiality_percentage * 100.0,
307 engagement.materiality_basis
308 );
309 judgment = judgment.with_conclusion(
310 &materiality_desc,
311 "Revenue is the most stable and relevant metric for the primary users of these \
312 financial statements. The selected percentage is within the acceptable range per \
313 firm guidance and appropriate given the risk profile of the engagement.",
314 "Misstatements below materiality threshold may still be significant to users \
315 in certain circumstances, which will be evaluated on a case-by-case basis.",
316 );
317
318 let preparer = self.select_team_member(team_members, "manager");
320 let preparer_name = self.generate_name();
321 judgment = judgment.with_preparer(&preparer, &preparer_name, engagement.planning_start);
322
323 let reviewer = self.select_team_member(team_members, "senior");
324 let reviewer_name = self.generate_name();
325 judgment.add_review(
326 &reviewer,
327 &reviewer_name,
328 engagement.planning_start + Duration::days(3),
329 );
330
331 judgment.add_partner_concurrence(
333 &engagement.engagement_partner_id,
334 engagement.planning_start + Duration::days(5),
335 );
336
337 judgment.status = JudgmentStatus::Approved;
338
339 judgment
340 }
341
342 fn select_judgment_type(&mut self) -> JudgmentType {
344 let types = [
345 (JudgmentType::RiskAssessment, 0.25),
346 (JudgmentType::ControlEvaluation, 0.15),
347 (JudgmentType::EstimateEvaluation, 0.15),
348 (JudgmentType::MisstatementEvaluation, 0.10),
349 (JudgmentType::SamplingDesign, 0.10),
350 (JudgmentType::GoingConcern, 0.05),
351 (JudgmentType::FraudRiskAssessment, 0.10),
352 (JudgmentType::RelatedPartyAssessment, 0.05),
353 (JudgmentType::SubsequentEvents, 0.05),
354 ];
355
356 let r: f64 = self.rng.gen();
357 let mut cumulative = 0.0;
358 for (jtype, probability) in types {
359 cumulative += probability;
360 if r < cumulative {
361 return jtype;
362 }
363 }
364 JudgmentType::RiskAssessment
365 }
366
367 fn generate_subject(&mut self, judgment_type: JudgmentType) -> String {
369 match judgment_type {
370 JudgmentType::MaterialityDetermination => "Overall Audit Materiality".into(),
371 JudgmentType::RiskAssessment => {
372 let areas = [
373 "Revenue",
374 "Inventory",
375 "Receivables",
376 "Fixed Assets",
377 "Payables",
378 ];
379 let idx = self.rng.gen_range(0..areas.len());
380 format!("{} Risk Assessment", areas[idx])
381 }
382 JudgmentType::ControlEvaluation => {
383 let controls = [
384 "Revenue Recognition",
385 "Disbursements",
386 "Payroll",
387 "IT General",
388 ];
389 let idx = self.rng.gen_range(0..controls.len());
390 format!("{} Controls Evaluation", controls[idx])
391 }
392 JudgmentType::EstimateEvaluation => {
393 let estimates = [
394 "Allowance for Doubtful Accounts",
395 "Inventory Obsolescence Reserve",
396 "Warranty Liability",
397 "Goodwill Impairment",
398 ];
399 let idx = self.rng.gen_range(0..estimates.len());
400 format!("{} Estimate", estimates[idx])
401 }
402 JudgmentType::GoingConcern => "Going Concern Assessment".into(),
403 JudgmentType::MisstatementEvaluation => "Evaluation of Identified Misstatements".into(),
404 JudgmentType::SamplingDesign => {
405 let areas = ["Revenue Cutoff", "Expense Testing", "AP Completeness"];
406 let idx = self.rng.gen_range(0..areas.len());
407 format!("{} Sample Design", areas[idx])
408 }
409 JudgmentType::FraudRiskAssessment => "Fraud Risk Assessment".into(),
410 JudgmentType::RelatedPartyAssessment => "Related Party Transactions".into(),
411 JudgmentType::SubsequentEvents => "Subsequent Events Evaluation".into(),
412 JudgmentType::ReportingDecision => "Audit Report Considerations".into(),
413 }
414 }
415
416 fn generate_issue_description(&mut self, judgment_type: JudgmentType) -> String {
418 match judgment_type {
419 JudgmentType::RiskAssessment => {
420 "Assessment of risk of material misstatement at the assertion level, \
421 considering inherent risk factors and the control environment."
422 .into()
423 }
424 JudgmentType::ControlEvaluation => {
425 "Evaluation of the design and operating effectiveness of internal controls \
426 to determine the extent of reliance for audit purposes."
427 .into()
428 }
429 JudgmentType::EstimateEvaluation => {
430 "Evaluation of management's accounting estimate, including assessment of \
431 methods, assumptions, and data used in developing the estimate."
432 .into()
433 }
434 JudgmentType::GoingConcern => {
435 "Assessment of whether conditions or events indicate substantial doubt \
436 about the entity's ability to continue as a going concern."
437 .into()
438 }
439 JudgmentType::MisstatementEvaluation => {
440 "Evaluation of identified misstatements to determine their effect on the \
441 audit and whether they are material, individually or in aggregate."
442 .into()
443 }
444 JudgmentType::SamplingDesign => {
445 "Determination of appropriate sample size and selection method to achieve \
446 the desired level of assurance for substantive testing."
447 .into()
448 }
449 JudgmentType::FraudRiskAssessment => {
450 "Assessment of fraud risk factors and determination of appropriate audit \
451 responses to address identified risks per ISA 240."
452 .into()
453 }
454 JudgmentType::RelatedPartyAssessment => {
455 "Evaluation of related party relationships and transactions to assess \
456 whether they have been appropriately identified and disclosed."
457 .into()
458 }
459 JudgmentType::SubsequentEvents => {
460 "Evaluation of events occurring after the balance sheet date to determine \
461 their effect on the financial statements."
462 .into()
463 }
464 _ => "Professional judgment required for this matter.".into(),
465 }
466 }
467
468 fn generate_information_item(&mut self, judgment_type: JudgmentType) -> InformationItem {
470 let items = match judgment_type {
471 JudgmentType::RiskAssessment => vec![
472 (
473 "Prior year audit findings",
474 "Prior year workpapers",
475 InformationReliability::High,
476 ),
477 (
478 "Industry risk factors",
479 "Industry research",
480 InformationReliability::High,
481 ),
482 (
483 "Management inquiries",
484 "Discussions with management",
485 InformationReliability::Medium,
486 ),
487 (
488 "Analytical procedures results",
489 "Auditor analysis",
490 InformationReliability::High,
491 ),
492 ],
493 JudgmentType::ControlEvaluation => vec![
494 (
495 "Control documentation",
496 "Client-prepared narratives",
497 InformationReliability::Medium,
498 ),
499 (
500 "Walkthrough results",
501 "Auditor observation",
502 InformationReliability::High,
503 ),
504 (
505 "Test of controls results",
506 "Auditor testing",
507 InformationReliability::High,
508 ),
509 (
510 "IT general controls assessment",
511 "IT audit specialists",
512 InformationReliability::High,
513 ),
514 ],
515 JudgmentType::EstimateEvaluation => vec![
516 (
517 "Historical accuracy of estimates",
518 "Prior year comparison",
519 InformationReliability::High,
520 ),
521 (
522 "Key assumptions documentation",
523 "Management memo",
524 InformationReliability::Medium,
525 ),
526 (
527 "Third-party data used",
528 "External sources",
529 InformationReliability::High,
530 ),
531 (
532 "Sensitivity analysis",
533 "Auditor recalculation",
534 InformationReliability::High,
535 ),
536 ],
537 _ => vec![
538 (
539 "Relevant audit evidence",
540 "Various sources",
541 InformationReliability::Medium,
542 ),
543 (
544 "Management representations",
545 "Inquiry responses",
546 InformationReliability::Medium,
547 ),
548 (
549 "External information",
550 "Third-party sources",
551 InformationReliability::High,
552 ),
553 ],
554 };
555
556 let idx = self.rng.gen_range(0..items.len());
557 let (desc, source, reliability) = items[idx];
558
559 let weight = match reliability {
560 InformationReliability::High => {
561 if self.rng.gen::<f64>() < 0.7 {
562 InformationWeight::High
563 } else {
564 InformationWeight::Moderate
565 }
566 }
567 InformationReliability::Medium => InformationWeight::Moderate,
568 InformationReliability::Low => InformationWeight::Low,
569 };
570
571 InformationItem::new(desc, source, reliability, "Relevant to the judgment")
572 .with_weight(weight)
573 }
574
575 fn generate_alternatives(
577 &mut self,
578 judgment_type: JudgmentType,
579 count: u32,
580 ) -> Vec<AlternativeEvaluation> {
581 let mut alternatives = Vec::new();
582
583 let options = match judgment_type {
584 JudgmentType::RiskAssessment => vec![
585 (
586 "Assess risk as high, perform extended substantive testing",
587 vec!["Conservative approach".into()],
588 vec!["May result in over-auditing".into()],
589 ),
590 (
591 "Assess risk as medium, perform combined approach",
592 vec!["Balanced approach".into(), "Cost-effective".into()],
593 vec!["Requires strong controls".into()],
594 ),
595 (
596 "Assess risk as low with controls reliance",
597 vec!["Efficient approach".into()],
598 vec!["Requires robust controls testing".into()],
599 ),
600 ],
601 JudgmentType::ControlEvaluation => vec![
602 (
603 "Rely on controls, reduce substantive testing",
604 vec!["Efficient".into()],
605 vec!["Requires strong ITGC".into()],
606 ),
607 (
608 "No reliance, substantive approach only",
609 vec!["Lower documentation".into()],
610 vec!["More substantive work".into()],
611 ),
612 (
613 "Partial reliance with moderate substantive testing",
614 vec!["Balanced".into()],
615 vec!["Moderate effort".into()],
616 ),
617 ],
618 JudgmentType::SamplingDesign => vec![
619 (
620 "Statistical sampling with 95% confidence",
621 vec!["Objective".into(), "Defensible".into()],
622 vec!["Larger samples".into()],
623 ),
624 (
625 "Non-statistical judgmental sampling",
626 vec!["Flexible".into()],
627 vec!["Less precise".into()],
628 ),
629 (
630 "MUS sampling approach",
631 vec!["Effective for overstatement".into()],
632 vec!["Complex calculations".into()],
633 ),
634 ],
635 _ => vec![
636 (
637 "Option A - Conservative approach",
638 vec!["Lower risk".into()],
639 vec!["More work".into()],
640 ),
641 (
642 "Option B - Standard approach",
643 vec!["Balanced".into()],
644 vec!["Moderate effort".into()],
645 ),
646 (
647 "Option C - Efficient approach",
648 vec!["Less work".into()],
649 vec!["Higher risk".into()],
650 ),
651 ],
652 };
653
654 let selected_idx = self.rng.gen_range(0..count.min(options.len() as u32)) as usize;
655
656 for (i, (desc, pros, cons)) in options.into_iter().take(count as usize).enumerate() {
657 let mut alt = AlternativeEvaluation::new(desc, pros, cons);
658 alt.risk_level = match i {
659 0 => RiskLevel::Low,
660 1 => RiskLevel::Medium,
661 _ => RiskLevel::High,
662 };
663
664 if i == selected_idx {
665 alt = alt.select();
666 } else {
667 alt = alt.reject("Alternative approach selected based on risk assessment");
668 }
669 alternatives.push(alt);
670 }
671
672 alternatives
673 }
674
675 fn generate_skepticism_documentation(
677 &mut self,
678 judgment_type: JudgmentType,
679 ) -> SkepticismDocumentation {
680 let assessment = match judgment_type {
681 JudgmentType::FraudRiskAssessment => {
682 "Maintained heightened skepticism given the presumed risks of fraud"
683 }
684 JudgmentType::EstimateEvaluation => {
685 "Critically evaluated management's assumptions and methods"
686 }
687 JudgmentType::GoingConcern => "Objectively assessed going concern indicators",
688 _ => "Applied appropriate professional skepticism throughout the evaluation",
689 };
690
691 let mut skepticism = SkepticismDocumentation::new(assessment);
692
693 skepticism.contradictory_evidence_considered = vec![
694 "Considered evidence that contradicts management's position".into(),
695 "Evaluated alternative explanations for observed conditions".into(),
696 ];
697
698 skepticism.management_bias_indicators =
699 vec!["Assessed whether management has incentives to bias the outcome".into()];
700
701 if judgment_type == JudgmentType::EstimateEvaluation {
702 skepticism.challenging_questions = vec![
703 "Why were these specific assumptions selected?".into(),
704 "What alternative methods were considered?".into(),
705 "How sensitive is the estimate to key assumptions?".into(),
706 ];
707 }
708
709 skepticism.corroboration_obtained =
710 "Corroborated key representations with independent evidence".into();
711
712 skepticism
713 }
714
715 fn generate_conclusion(&mut self, judgment_type: JudgmentType) -> (String, String, String) {
717 match judgment_type {
718 JudgmentType::RiskAssessment => (
719 "Risk of material misstatement assessed as medium based on inherent risk factors \
720 and the control environment"
721 .into(),
722 "Inherent risk factors are present but mitigated by effective controls. \
723 The combined approach is appropriate given the assessment."
724 .into(),
725 "Possibility that undetected misstatements exist below materiality threshold."
726 .into(),
727 ),
728 JudgmentType::ControlEvaluation => (
729 "Controls are designed appropriately and operating effectively. \
730 Reliance on controls is appropriate."
731 .into(),
732 "Testing demonstrated that controls operated consistently throughout the period. \
733 No significant deviations were identified."
734 .into(),
735 "Controls may not prevent or detect all misstatements.".into(),
736 ),
737 JudgmentType::EstimateEvaluation => (
738 "Management's estimate is reasonable based on the available information \
739 and falls within an acceptable range."
740 .into(),
741 "The methods and assumptions used are appropriate for the circumstances. \
742 Data inputs are reliable and the estimate is consistent with industry practices."
743 .into(),
744 "Estimation uncertainty remains due to inherent subjectivity in key assumptions."
745 .into(),
746 ),
747 JudgmentType::GoingConcern => (
748 "No substantial doubt about the entity's ability to continue as a going concern \
749 for at least twelve months from the balance sheet date."
750 .into(),
751 "Management's plans to address identified conditions are feasible and adequately \
752 disclosed. Cash flow projections support the conclusion."
753 .into(),
754 "Future events could impact the entity's ability to continue operations.".into(),
755 ),
756 JudgmentType::FraudRiskAssessment => (
757 "Fraud risk factors have been identified and appropriate audit responses \
758 have been designed to address those risks."
759 .into(),
760 "Presumed risks per ISA 240 have been addressed through specific procedures. \
761 No fraud was identified during our procedures."
762 .into(),
763 "Fraud is inherently difficult to detect; our procedures provide reasonable \
764 but not absolute assurance."
765 .into(),
766 ),
767 _ => (
768 "Professional judgment has been applied appropriately to this matter.".into(),
769 "The conclusion is supported by the audit evidence obtained.".into(),
770 "Inherent limitations exist in any judgment-based evaluation.".into(),
771 ),
772 }
773 }
774
775 fn generate_consultation(
777 &mut self,
778 judgment_type: JudgmentType,
779 base_date: NaiveDate,
780 ) -> ConsultationRecord {
781 let (consultant, role, is_external) = if self.rng.gen::<f64>() < 0.3 {
782 ("External Technical Partner", "Industry Specialist", true)
783 } else {
784 let roles = [
785 ("National Office", "Technical Accounting", false),
786 ("Quality Review Partner", "Quality Control", false),
787 ("Industry Specialist", "Sector Expert", false),
788 ];
789 let idx = self.rng.gen_range(0..roles.len());
790 (roles[idx].0, roles[idx].1, roles[idx].2)
791 };
792
793 let issue = match judgment_type {
794 JudgmentType::GoingConcern => {
795 "Assessment of going concern indicators and disclosure requirements"
796 }
797 JudgmentType::EstimateEvaluation => {
798 "Evaluation of complex accounting estimate methodology"
799 }
800 JudgmentType::FraudRiskAssessment => {
801 "Assessment of fraud risk indicators and response design"
802 }
803 _ => "Technical accounting matter requiring consultation",
804 };
805
806 ConsultationRecord::new(
807 consultant,
808 role,
809 is_external,
810 base_date + Duration::days(self.rng.gen_range(1..7)),
811 )
812 .with_content(
813 issue,
814 "Consultant provided guidance on the appropriate approach and key considerations",
815 "Guidance has been incorporated into the judgment documentation",
816 "Consultation supports the conclusion reached",
817 )
818 }
819
820 fn select_team_member(&mut self, team_members: &[String], role_hint: &str) -> String {
822 let matching: Vec<&String> = team_members
823 .iter()
824 .filter(|m| m.to_lowercase().contains(role_hint))
825 .collect();
826
827 if let Some(&member) = matching.first() {
828 member.clone()
829 } else if !team_members.is_empty() {
830 let idx = self.rng.gen_range(0..team_members.len());
831 team_members[idx].clone()
832 } else {
833 format!("{}001", role_hint.to_uppercase())
834 }
835 }
836
837 fn generate_name(&mut self) -> String {
839 let first_names = ["Michael", "Sarah", "David", "Jennifer", "Robert", "Emily"];
840 let last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Davis"];
841
842 let first_idx = self.rng.gen_range(0..first_names.len());
843 let last_idx = self.rng.gen_range(0..last_names.len());
844
845 format!("{} {}", first_names[first_idx], last_names[last_idx])
846 }
847}
848
849#[cfg(test)]
850#[allow(clippy::unwrap_used)]
851mod tests {
852 use super::*;
853 use crate::audit::test_helpers::create_test_engagement;
854
855 #[test]
856 fn test_judgment_generation() {
857 let mut generator = JudgmentGenerator::new(42);
858 let engagement = create_test_engagement();
859 let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
860
861 let judgments = generator.generate_judgments_for_engagement(&engagement, &team);
862
863 assert!(!judgments.is_empty());
864
865 assert_eq!(
867 judgments[0].judgment_type,
868 JudgmentType::MaterialityDetermination
869 );
870
871 for judgment in &judgments {
872 assert!(!judgment.issue_description.is_empty());
873 assert!(!judgment.conclusion.is_empty());
874 assert!(!judgment.information_considered.is_empty());
875 }
876 }
877
878 #[test]
879 fn test_materiality_judgment() {
880 let mut generator = JudgmentGenerator::new(42);
881 let engagement = create_test_engagement();
882 let team = vec!["MANAGER001".into()];
883
884 let judgments = generator.generate_judgments_for_engagement(&engagement, &team);
885 let materiality = &judgments[0];
886
887 assert_eq!(
888 materiality.judgment_type,
889 JudgmentType::MaterialityDetermination
890 );
891 assert!(materiality.partner_concurrence_id.is_some()); assert_eq!(materiality.status, JudgmentStatus::Approved);
893 assert!(!materiality.alternatives_evaluated.is_empty());
894 }
895
896 #[test]
897 fn test_judgment_approval_flow() {
898 let mut generator = JudgmentGenerator::new(42);
899 let engagement = create_test_engagement();
900 let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
901
902 let judgments = generator.generate_judgments_for_engagement(&engagement, &team);
903
904 for judgment in &judgments {
905 assert!(matches!(
907 judgment.status,
908 JudgmentStatus::Approved | JudgmentStatus::Reviewed | JudgmentStatus::PendingReview
909 ));
910 }
911 }
912
913 #[test]
914 fn test_skepticism_documentation() {
915 let mut generator = JudgmentGenerator::new(42);
916 let engagement = create_test_engagement();
917
918 let judgment = generator.generate_judgment(&engagement, &["STAFF001".into()]);
919
920 assert!(!judgment.skepticism_applied.skepticism_assessment.is_empty());
921 assert!(!judgment
922 .skepticism_applied
923 .contradictory_evidence_considered
924 .is_empty());
925 }
926
927 #[test]
928 fn test_consultation_generation() {
929 let config = JudgmentGeneratorConfig {
930 consultation_probability: 1.0,
931 ..Default::default()
932 };
933 let mut generator = JudgmentGenerator::with_config(42, config);
934 let engagement = create_test_engagement();
935
936 let judgment = generator.generate_judgment(&engagement, &["STAFF001".into()]);
937
938 if judgment.consultation.is_some() {
941 let consultation = judgment.consultation.as_ref().unwrap();
942 assert!(!consultation.consultant.is_empty());
943 assert!(!consultation.issue_presented.is_empty());
944 }
945 }
946}