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