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