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