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