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