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