Skip to main content

datasynth_generators/audit/
judgment_generator.rs

1//! Professional judgment generator for audit engagements.
2//!
3//! Generates professional judgment documentation with structured reasoning,
4//! skepticism documentation, and consultation records per ISA 200.
5
6use 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/// Configuration for judgment generation.
18#[derive(Debug, Clone)]
19pub struct JudgmentGeneratorConfig {
20    /// Number of judgments per engagement (min, max)
21    pub judgments_per_engagement: (u32, u32),
22    /// Probability of requiring consultation
23    pub consultation_probability: f64,
24    /// Number of information items per judgment (min, max)
25    pub information_items_range: (u32, u32),
26    /// Number of alternatives evaluated (min, max)
27    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
41/// Generator for professional judgments.
42pub struct JudgmentGenerator {
43    rng: ChaCha8Rng,
44    config: JudgmentGeneratorConfig,
45    judgment_counter: u32,
46    fiscal_year: u16,
47}
48
49impl JudgmentGenerator {
50    /// Create a new generator with the given seed.
51    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    /// Create a new generator with custom configuration.
61    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    /// Generate judgments for an engagement.
71    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        // Always include materiality judgment
85        judgments.push(self.generate_materiality_judgment(engagement, team_members));
86
87        // Generate additional judgments
88        for _ in 1..count {
89            let judgment = self.generate_judgment(engagement, team_members);
90            judgments.push(judgment);
91        }
92
93        judgments
94    }
95
96    /// Generate a single professional judgment.
97    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        // Set issue description
113        let issue = self.generate_issue_description(judgment_type);
114        judgment = judgment.with_issue(&issue);
115
116        // Add information items
117        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        // Add alternative evaluations
126        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        // Set skepticism documentation
135        let skepticism = self.generate_skepticism_documentation(judgment_type);
136        judgment = judgment.with_skepticism(skepticism);
137
138        // Set conclusion
139        let (conclusion, rationale, residual_risk) = self.generate_conclusion(judgment_type);
140        judgment = judgment.with_conclusion(&conclusion, &rationale, &residual_risk);
141
142        // Set preparer
143        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        // Add reviewer
149        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        // Maybe add partner concurrence
157        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        // Maybe add consultation
164        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        // Update status
172        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    /// Generate materiality judgment (always included).
184    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        // Add information items
208        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        // Add alternatives
249        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        // Skepticism documentation
287        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        // Conclusion
303        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        // Set preparer and reviews
319        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        // Partner concurrence required for materiality
332        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    /// Select judgment type.
343    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    /// Generate subject based on judgment type.
368    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    /// Generate issue description.
417    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    /// Generate information item.
469    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    /// Generate alternative evaluations.
576    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    /// Generate skepticism documentation.
676    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    /// Generate conclusion.
716    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    /// Generate consultation record.
776    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    /// Select team member.
821    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    /// Generate a name.
838    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        // First judgment should be materiality
866        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()); // Partner concurrence required
892        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            // Most judgments should be at least reviewed
906            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        // Judgment should have consultation (either required or by probability)
939        // Note: Some judgment types don't require consultation, so check if added
940        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}