use chrono::{Duration, NaiveDate};
use datasynth_core::utils::seeded_rng;
use rand::RngExt;
use rand_chacha::ChaCha8Rng;
use rust_decimal::Decimal;
use datasynth_core::models::audit::{
AlternativeEvaluation, AuditEngagement, ConsultationRecord, InformationItem,
InformationReliability, InformationWeight, JudgmentStatus, JudgmentType, ProfessionalJudgment,
RiskLevel, SkepticismDocumentation,
};
#[derive(Debug, Clone)]
pub struct JudgmentGeneratorConfig {
pub judgments_per_engagement: (u32, u32),
pub consultation_probability: f64,
pub information_items_range: (u32, u32),
pub alternatives_range: (u32, u32),
}
impl Default for JudgmentGeneratorConfig {
fn default() -> Self {
Self {
judgments_per_engagement: (5, 15),
consultation_probability: 0.25,
information_items_range: (2, 6),
alternatives_range: (2, 4),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct JudgmentContext {
pub materiality_amount: Option<Decimal>,
pub materiality_basis: Option<String>,
pub materiality_percentage: Option<Decimal>,
pub high_risk_count: usize,
pub high_risk_areas: Vec<String>,
pub going_concern_doubt: bool,
pub finding_count: usize,
pub total_misstatement: Option<Decimal>,
}
pub struct JudgmentGenerator {
rng: ChaCha8Rng,
config: JudgmentGeneratorConfig,
judgment_counter: u32,
fiscal_year: u16,
}
impl JudgmentGenerator {
pub fn new(seed: u64) -> Self {
Self {
rng: seeded_rng(seed, 0),
config: JudgmentGeneratorConfig::default(),
judgment_counter: 0,
fiscal_year: 2025,
}
}
pub fn with_config(seed: u64, config: JudgmentGeneratorConfig) -> Self {
Self {
rng: seeded_rng(seed, 0),
config,
judgment_counter: 0,
fiscal_year: 2025,
}
}
pub fn generate_judgments_for_engagement(
&mut self,
engagement: &AuditEngagement,
team_members: &[String],
) -> Vec<ProfessionalJudgment> {
self.fiscal_year = engagement.fiscal_year;
let count = self.rng.random_range(
self.config.judgments_per_engagement.0..=self.config.judgments_per_engagement.1,
);
let mut judgments = Vec::with_capacity(count as usize);
judgments.push(self.generate_materiality_judgment(engagement, team_members));
for _ in 1..count {
let judgment = self.generate_judgment(engagement, team_members);
judgments.push(judgment);
}
judgments
}
pub fn generate_judgment_with_context(
&mut self,
engagement: &AuditEngagement,
team_members: &[String],
context: &JudgmentContext,
) -> ProfessionalJudgment {
let mut judgment = self.generate_judgment(engagement, team_members);
match judgment.judgment_type {
JudgmentType::MaterialityDetermination => {
if let (Some(amount), Some(basis), Some(pct)) = (
context.materiality_amount,
context.materiality_basis.as_deref(),
context.materiality_percentage,
) {
let pct_display = pct * Decimal::new(100, 0);
judgment.conclusion = format!(
"Set overall materiality at ${} ({}% of {}). \
Performance materiality set at 65% of overall materiality.",
amount, pct_display, basis
);
judgment.rationale = format!(
"{} is the most stable and relevant metric for the primary users of these \
financial statements. The selected percentage of {}% is within the \
acceptable range per firm guidance and appropriate given the risk profile \
of the engagement.",
basis, pct_display
);
}
}
JudgmentType::RiskAssessment => {
if context.high_risk_count > 0 {
let areas_text = if context.high_risk_areas.is_empty() {
format!("{} areas", context.high_risk_count)
} else {
context.high_risk_areas.join(", ")
};
judgment.conclusion = format!(
"Assessed {} area(s) as high risk: {}. Extended substantive testing \
is planned for these areas.",
context.high_risk_count, areas_text
);
judgment.rationale = format!(
"Inherent risk factors are present in {} area(s) ({}). \
The combined approach with extended procedures is appropriate \
given the elevated risk assessment.",
context.high_risk_count, areas_text
);
}
}
JudgmentType::GoingConcern => {
if context.going_concern_doubt {
judgment.conclusion =
"Material uncertainty exists regarding the entity's ability to continue \
as a going concern for at least twelve months from the balance sheet \
date. The financial statements should include appropriate disclosures \
per IAS 1.25."
.into();
judgment.rationale =
"Indicators of going-concern doubt were identified during the assessment. \
Management's plans to address the conditions have been evaluated and, \
while partially mitigating, do not fully resolve the uncertainty. \
Cash flow projections show potential liquidity shortfalls."
.into();
} else {
judgment.conclusion =
"No substantial doubt about the entity's ability to continue as a going \
concern for at least twelve months from the balance sheet date."
.into();
}
}
JudgmentType::MisstatementEvaluation => {
let mut parts = Vec::new();
if context.finding_count > 0 {
parts.push(format!(
"Evaluated {} identified misstatement(s)",
context.finding_count
));
}
if let Some(total) = context.total_misstatement {
parts.push(format!("with aggregate amount of ${}", total));
if let Some(mat) = context.materiality_amount {
if total < mat {
parts.push(format!("which is below overall materiality of ${}", mat));
} else {
parts.push(format!("which exceeds overall materiality of ${}", mat));
}
}
}
if !parts.is_empty() {
judgment.conclusion = format!(
"{}. The effect on the financial statements has been considered in \
forming the audit opinion.",
parts.join(", ")
);
}
if context.finding_count > 0 {
judgment.rationale = format!(
"{} misstatement(s) were identified during audit procedures. \
Each was evaluated individually and in aggregate to assess \
their impact on the financial statements and audit opinion.",
context.finding_count
);
}
}
_ => {
}
}
judgment
}
pub fn generate_judgment(
&mut self,
engagement: &AuditEngagement,
team_members: &[String],
) -> ProfessionalJudgment {
self.judgment_counter += 1;
let judgment_type = self.select_judgment_type();
let subject = self.generate_subject(judgment_type);
let mut judgment =
ProfessionalJudgment::new(engagement.engagement_id, judgment_type, &subject);
judgment.judgment_ref = format!("JDG-{}-{:03}", self.fiscal_year, self.judgment_counter);
let issue = self.generate_issue_description(judgment_type);
judgment = judgment.with_issue(&issue);
let info_count = self.rng.random_range(
self.config.information_items_range.0..=self.config.information_items_range.1,
);
for _ in 0..info_count {
let item = self.generate_information_item(judgment_type);
judgment.add_information(item);
}
let alt_count = self
.rng
.random_range(self.config.alternatives_range.0..=self.config.alternatives_range.1);
let alternatives = self.generate_alternatives(judgment_type, alt_count);
for alt in alternatives {
judgment.add_alternative(alt);
}
let skepticism = self.generate_skepticism_documentation(judgment_type);
judgment = judgment.with_skepticism(skepticism);
let (conclusion, rationale, residual_risk) = self.generate_conclusion(judgment_type);
judgment = judgment.with_conclusion(&conclusion, &rationale, &residual_risk);
let preparer = self.select_team_member(team_members, "manager");
let preparer_name = self.generate_name();
let preparer_date =
engagement.planning_start + Duration::days(self.rng.random_range(5..20));
judgment = judgment.with_preparer(&preparer, &preparer_name, preparer_date);
if self.rng.random::<f64>() < 0.9 {
let reviewer = self.select_team_member(team_members, "senior");
let reviewer_name = self.generate_name();
let review_date = preparer_date + Duration::days(self.rng.random_range(3..10));
judgment.add_review(&reviewer, &reviewer_name, review_date);
}
if judgment.partner_concurrence_required && self.rng.random::<f64>() < 0.8 {
let partner = engagement.engagement_partner_id.clone();
let partner_date = preparer_date + Duration::days(self.rng.random_range(7..14));
judgment.add_partner_concurrence(&partner, partner_date);
}
if judgment.consultation_required
|| self.rng.random::<f64>() < self.config.consultation_probability
{
let consultation = self.generate_consultation(judgment_type, preparer_date);
judgment.add_consultation(consultation);
}
judgment.status = if judgment.is_approved() {
JudgmentStatus::Approved
} else if judgment.reviewer_id.is_some() {
JudgmentStatus::Reviewed
} else {
JudgmentStatus::PendingReview
};
judgment
}
fn generate_materiality_judgment(
&mut self,
engagement: &AuditEngagement,
team_members: &[String],
) -> ProfessionalJudgment {
self.judgment_counter += 1;
let mut judgment = ProfessionalJudgment::new(
engagement.engagement_id,
JudgmentType::MaterialityDetermination,
"Overall Audit Materiality",
);
judgment.judgment_ref = format!(
"JDG-{}-{:03}",
engagement.fiscal_year, self.judgment_counter
);
judgment = judgment.with_issue(
"Determination of overall materiality, performance materiality, and clearly trivial \
threshold for the audit of the financial statements.",
);
judgment.add_information(
InformationItem::new(
"Prior year audited financial statements",
"Audited financial statements",
InformationReliability::High,
"Establishes baseline for materiality calculation",
)
.with_weight(InformationWeight::High),
);
judgment.add_information(
InformationItem::new(
"Current year budget and forecasts",
"Management-prepared projections",
InformationReliability::Medium,
"Provides expectation for current year metrics",
)
.with_weight(InformationWeight::Moderate),
);
judgment.add_information(
InformationItem::new(
"Industry benchmarks for materiality",
"Firm guidance and industry data",
InformationReliability::High,
"Supports selection of appropriate percentage",
)
.with_weight(InformationWeight::High),
);
judgment.add_information(
InformationItem::new(
"User expectations and stakeholder considerations",
"Knowledge of the entity and environment",
InformationReliability::Medium,
"Informs selection of appropriate benchmark",
)
.with_weight(InformationWeight::Moderate),
);
judgment.add_alternative(
AlternativeEvaluation::new(
"Use total revenue as materiality base",
vec![
"Stable metric year over year".into(),
"Primary focus of financial statement users".into(),
"Consistent with prior year approach".into(),
],
vec!["May not capture balance sheet focused risks".into()],
)
.select(),
);
judgment.add_alternative(
AlternativeEvaluation::new(
"Use total assets as materiality base",
vec!["Appropriate for asset-intensive industries".into()],
vec![
"Less relevant for this entity".into(),
"Assets more volatile than revenue".into(),
],
)
.reject("Revenue is more relevant to primary users of the financial statements"),
);
judgment.add_alternative(
AlternativeEvaluation::new(
"Use net income as materiality base",
vec!["Direct measure of profitability".into()],
vec![
"Net income is volatile".into(),
"Not appropriate when near breakeven".into(),
],
)
.reject("Net income volatility makes it unsuitable as a stable benchmark"),
);
judgment = judgment.with_skepticism(
SkepticismDocumentation::new(
"Materiality calculation and benchmark selection reviewed critically",
)
.with_contradictory_evidence(vec![
"Considered whether management might prefer higher materiality to reduce audit scope".into(),
])
.with_bias_indicators(vec![
"Evaluated if selected benchmark minimizes likely misstatements".into(),
])
.with_alternatives(vec![
"Considered multiple benchmarks and percentage ranges".into(),
]),
);
let materiality_desc = format!(
"Set overall materiality at ${} based on {}% of {}",
engagement.materiality,
engagement.materiality_percentage * 100.0,
engagement.materiality_basis
);
judgment = judgment.with_conclusion(
&materiality_desc,
"Revenue is the most stable and relevant metric for the primary users of these \
financial statements. The selected percentage is within the acceptable range per \
firm guidance and appropriate given the risk profile of the engagement.",
"Misstatements below materiality threshold may still be significant to users \
in certain circumstances, which will be evaluated on a case-by-case basis.",
);
let preparer = self.select_team_member(team_members, "manager");
let preparer_name = self.generate_name();
judgment = judgment.with_preparer(&preparer, &preparer_name, engagement.planning_start);
let reviewer = self.select_team_member(team_members, "senior");
let reviewer_name = self.generate_name();
judgment.add_review(
&reviewer,
&reviewer_name,
engagement.planning_start + Duration::days(3),
);
judgment.add_partner_concurrence(
&engagement.engagement_partner_id,
engagement.planning_start + Duration::days(5),
);
judgment.status = JudgmentStatus::Approved;
judgment
}
fn select_judgment_type(&mut self) -> JudgmentType {
let types = [
(JudgmentType::RiskAssessment, 0.25),
(JudgmentType::ControlEvaluation, 0.15),
(JudgmentType::EstimateEvaluation, 0.15),
(JudgmentType::MisstatementEvaluation, 0.10),
(JudgmentType::SamplingDesign, 0.10),
(JudgmentType::GoingConcern, 0.05),
(JudgmentType::FraudRiskAssessment, 0.10),
(JudgmentType::RelatedPartyAssessment, 0.05),
(JudgmentType::SubsequentEvents, 0.05),
];
let r: f64 = self.rng.random();
let mut cumulative = 0.0;
for (jtype, probability) in types {
cumulative += probability;
if r < cumulative {
return jtype;
}
}
JudgmentType::RiskAssessment
}
fn generate_subject(&mut self, judgment_type: JudgmentType) -> String {
match judgment_type {
JudgmentType::MaterialityDetermination => "Overall Audit Materiality".into(),
JudgmentType::RiskAssessment => {
let areas = [
"Revenue",
"Inventory",
"Receivables",
"Fixed Assets",
"Payables",
];
let idx = self.rng.random_range(0..areas.len());
format!("{} Risk Assessment", areas[idx])
}
JudgmentType::ControlEvaluation => {
let controls = [
"Revenue Recognition",
"Disbursements",
"Payroll",
"IT General",
];
let idx = self.rng.random_range(0..controls.len());
format!("{} Controls Evaluation", controls[idx])
}
JudgmentType::EstimateEvaluation => {
let estimates = [
"Allowance for Doubtful Accounts",
"Inventory Obsolescence Reserve",
"Warranty Liability",
"Goodwill Impairment",
];
let idx = self.rng.random_range(0..estimates.len());
format!("{} Estimate", estimates[idx])
}
JudgmentType::GoingConcern => "Going Concern Assessment".into(),
JudgmentType::MisstatementEvaluation => "Evaluation of Identified Misstatements".into(),
JudgmentType::SamplingDesign => {
let areas = ["Revenue Cutoff", "Expense Testing", "AP Completeness"];
let idx = self.rng.random_range(0..areas.len());
format!("{} Sample Design", areas[idx])
}
JudgmentType::FraudRiskAssessment => "Fraud Risk Assessment".into(),
JudgmentType::RelatedPartyAssessment => "Related Party Transactions".into(),
JudgmentType::SubsequentEvents => "Subsequent Events Evaluation".into(),
JudgmentType::ReportingDecision => "Audit Report Considerations".into(),
}
}
fn generate_issue_description(&mut self, judgment_type: JudgmentType) -> String {
match judgment_type {
JudgmentType::RiskAssessment => {
"Assessment of risk of material misstatement at the assertion level, \
considering inherent risk factors and the control environment."
.into()
}
JudgmentType::ControlEvaluation => {
"Evaluation of the design and operating effectiveness of internal controls \
to determine the extent of reliance for audit purposes."
.into()
}
JudgmentType::EstimateEvaluation => {
"Evaluation of management's accounting estimate, including assessment of \
methods, assumptions, and data used in developing the estimate."
.into()
}
JudgmentType::GoingConcern => {
"Assessment of whether conditions or events indicate substantial doubt \
about the entity's ability to continue as a going concern."
.into()
}
JudgmentType::MisstatementEvaluation => {
"Evaluation of identified misstatements to determine their effect on the \
audit and whether they are material, individually or in aggregate."
.into()
}
JudgmentType::SamplingDesign => {
"Determination of appropriate sample size and selection method to achieve \
the desired level of assurance for substantive testing."
.into()
}
JudgmentType::FraudRiskAssessment => {
"Assessment of fraud risk factors and determination of appropriate audit \
responses to address identified risks per ISA 240."
.into()
}
JudgmentType::RelatedPartyAssessment => {
"Evaluation of related party relationships and transactions to assess \
whether they have been appropriately identified and disclosed."
.into()
}
JudgmentType::SubsequentEvents => {
"Evaluation of events occurring after the balance sheet date to determine \
their effect on the financial statements."
.into()
}
_ => "Professional judgment required for this matter.".into(),
}
}
fn generate_information_item(&mut self, judgment_type: JudgmentType) -> InformationItem {
let items = match judgment_type {
JudgmentType::RiskAssessment => vec![
(
"Prior year audit findings",
"Prior year workpapers",
InformationReliability::High,
),
(
"Industry risk factors",
"Industry research",
InformationReliability::High,
),
(
"Management inquiries",
"Discussions with management",
InformationReliability::Medium,
),
(
"Analytical procedures results",
"Auditor analysis",
InformationReliability::High,
),
],
JudgmentType::ControlEvaluation => vec![
(
"Control documentation",
"Client-prepared narratives",
InformationReliability::Medium,
),
(
"Walkthrough results",
"Auditor observation",
InformationReliability::High,
),
(
"Test of controls results",
"Auditor testing",
InformationReliability::High,
),
(
"IT general controls assessment",
"IT audit specialists",
InformationReliability::High,
),
],
JudgmentType::EstimateEvaluation => vec![
(
"Historical accuracy of estimates",
"Prior year comparison",
InformationReliability::High,
),
(
"Key assumptions documentation",
"Management memo",
InformationReliability::Medium,
),
(
"Third-party data used",
"External sources",
InformationReliability::High,
),
(
"Sensitivity analysis",
"Auditor recalculation",
InformationReliability::High,
),
],
_ => vec![
(
"Relevant audit evidence",
"Various sources",
InformationReliability::Medium,
),
(
"Management representations",
"Inquiry responses",
InformationReliability::Medium,
),
(
"External information",
"Third-party sources",
InformationReliability::High,
),
],
};
let idx = self.rng.random_range(0..items.len());
let (desc, source, reliability) = items[idx];
let weight = match reliability {
InformationReliability::High => {
if self.rng.random::<f64>() < 0.7 {
InformationWeight::High
} else {
InformationWeight::Moderate
}
}
InformationReliability::Medium => InformationWeight::Moderate,
InformationReliability::Low => InformationWeight::Low,
};
InformationItem::new(desc, source, reliability, "Relevant to the judgment")
.with_weight(weight)
}
fn generate_alternatives(
&mut self,
judgment_type: JudgmentType,
count: u32,
) -> Vec<AlternativeEvaluation> {
let mut alternatives = Vec::new();
let options = match judgment_type {
JudgmentType::RiskAssessment => vec![
(
"Assess risk as high, perform extended substantive testing",
vec!["Conservative approach".into()],
vec!["May result in over-auditing".into()],
),
(
"Assess risk as medium, perform combined approach",
vec!["Balanced approach".into(), "Cost-effective".into()],
vec!["Requires strong controls".into()],
),
(
"Assess risk as low with controls reliance",
vec!["Efficient approach".into()],
vec!["Requires robust controls testing".into()],
),
],
JudgmentType::ControlEvaluation => vec![
(
"Rely on controls, reduce substantive testing",
vec!["Efficient".into()],
vec!["Requires strong ITGC".into()],
),
(
"No reliance, substantive approach only",
vec!["Lower documentation".into()],
vec!["More substantive work".into()],
),
(
"Partial reliance with moderate substantive testing",
vec!["Balanced".into()],
vec!["Moderate effort".into()],
),
],
JudgmentType::SamplingDesign => vec![
(
"Statistical sampling with 95% confidence",
vec!["Objective".into(), "Defensible".into()],
vec!["Larger samples".into()],
),
(
"Non-statistical judgmental sampling",
vec!["Flexible".into()],
vec!["Less precise".into()],
),
(
"MUS sampling approach",
vec!["Effective for overstatement".into()],
vec!["Complex calculations".into()],
),
],
_ => vec![
(
"Option A - Conservative approach",
vec!["Lower risk".into()],
vec!["More work".into()],
),
(
"Option B - Standard approach",
vec!["Balanced".into()],
vec!["Moderate effort".into()],
),
(
"Option C - Efficient approach",
vec!["Less work".into()],
vec!["Higher risk".into()],
),
],
};
let selected_idx = self.rng.random_range(0..count.min(options.len() as u32)) as usize;
for (i, (desc, pros, cons)) in options.into_iter().take(count as usize).enumerate() {
let mut alt = AlternativeEvaluation::new(desc, pros, cons);
alt.risk_level = match i {
0 => RiskLevel::Low,
1 => RiskLevel::Medium,
_ => RiskLevel::High,
};
if i == selected_idx {
alt = alt.select();
} else {
alt = alt.reject("Alternative approach selected based on risk assessment");
}
alternatives.push(alt);
}
alternatives
}
fn generate_skepticism_documentation(
&mut self,
judgment_type: JudgmentType,
) -> SkepticismDocumentation {
let assessment = match judgment_type {
JudgmentType::FraudRiskAssessment => {
"Maintained heightened skepticism given the presumed risks of fraud"
}
JudgmentType::EstimateEvaluation => {
"Critically evaluated management's assumptions and methods"
}
JudgmentType::GoingConcern => "Objectively assessed going concern indicators",
_ => "Applied appropriate professional skepticism throughout the evaluation",
};
let mut skepticism = SkepticismDocumentation::new(assessment);
skepticism.contradictory_evidence_considered = vec![
"Considered evidence that contradicts management's position".into(),
"Evaluated alternative explanations for observed conditions".into(),
];
skepticism.management_bias_indicators =
vec!["Assessed whether management has incentives to bias the outcome".into()];
if judgment_type == JudgmentType::EstimateEvaluation {
skepticism.challenging_questions = vec![
"Why were these specific assumptions selected?".into(),
"What alternative methods were considered?".into(),
"How sensitive is the estimate to key assumptions?".into(),
];
}
skepticism.corroboration_obtained =
"Corroborated key representations with independent evidence".into();
skepticism
}
fn generate_conclusion(&mut self, judgment_type: JudgmentType) -> (String, String, String) {
match judgment_type {
JudgmentType::RiskAssessment => (
"Risk of material misstatement assessed as medium based on inherent risk factors \
and the control environment"
.into(),
"Inherent risk factors are present but mitigated by effective controls. \
The combined approach is appropriate given the assessment."
.into(),
"Possibility that undetected misstatements exist below materiality threshold."
.into(),
),
JudgmentType::ControlEvaluation => (
"Controls are designed appropriately and operating effectively. \
Reliance on controls is appropriate."
.into(),
"Testing demonstrated that controls operated consistently throughout the period. \
No significant deviations were identified."
.into(),
"Controls may not prevent or detect all misstatements.".into(),
),
JudgmentType::EstimateEvaluation => (
"Management's estimate is reasonable based on the available information \
and falls within an acceptable range."
.into(),
"The methods and assumptions used are appropriate for the circumstances. \
Data inputs are reliable and the estimate is consistent with industry practices."
.into(),
"Estimation uncertainty remains due to inherent subjectivity in key assumptions."
.into(),
),
JudgmentType::GoingConcern => (
"No substantial doubt about the entity's ability to continue as a going concern \
for at least twelve months from the balance sheet date."
.into(),
"Management's plans to address identified conditions are feasible and adequately \
disclosed. Cash flow projections support the conclusion."
.into(),
"Future events could impact the entity's ability to continue operations.".into(),
),
JudgmentType::FraudRiskAssessment => (
"Fraud risk factors have been identified and appropriate audit responses \
have been designed to address those risks."
.into(),
"Presumed risks per ISA 240 have been addressed through specific procedures. \
No fraud was identified during our procedures."
.into(),
"Fraud is inherently difficult to detect; our procedures provide reasonable \
but not absolute assurance."
.into(),
),
_ => (
"Professional judgment has been applied appropriately to this matter.".into(),
"The conclusion is supported by the audit evidence obtained.".into(),
"Inherent limitations exist in any judgment-based evaluation.".into(),
),
}
}
fn generate_consultation(
&mut self,
judgment_type: JudgmentType,
base_date: NaiveDate,
) -> ConsultationRecord {
let (consultant, role, is_external) = if self.rng.random::<f64>() < 0.3 {
("External Technical Partner", "Industry Specialist", true)
} else {
let roles = [
("National Office", "Technical Accounting", false),
("Quality Review Partner", "Quality Control", false),
("Industry Specialist", "Sector Expert", false),
];
let idx = self.rng.random_range(0..roles.len());
(roles[idx].0, roles[idx].1, roles[idx].2)
};
let issue = match judgment_type {
JudgmentType::GoingConcern => {
"Assessment of going concern indicators and disclosure requirements"
}
JudgmentType::EstimateEvaluation => {
"Evaluation of complex accounting estimate methodology"
}
JudgmentType::FraudRiskAssessment => {
"Assessment of fraud risk indicators and response design"
}
_ => "Technical accounting matter requiring consultation",
};
ConsultationRecord::new(
consultant,
role,
is_external,
base_date + Duration::days(self.rng.random_range(1..7)),
)
.with_content(
issue,
"Consultant provided guidance on the appropriate approach and key considerations",
"Guidance has been incorporated into the judgment documentation",
"Consultation supports the conclusion reached",
)
}
fn select_team_member(&mut self, team_members: &[String], role_hint: &str) -> String {
let matching: Vec<&String> = team_members
.iter()
.filter(|m| m.to_lowercase().contains(role_hint))
.collect();
if let Some(&member) = matching.first() {
member.clone()
} else if !team_members.is_empty() {
let idx = self.rng.random_range(0..team_members.len());
team_members[idx].clone()
} else {
format!("{}001", role_hint.to_uppercase())
}
}
fn generate_name(&mut self) -> String {
let first_names = ["Michael", "Sarah", "David", "Jennifer", "Robert", "Emily"];
let last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Davis"];
let first_idx = self.rng.random_range(0..first_names.len());
let last_idx = self.rng.random_range(0..last_names.len());
format!("{} {}", first_names[first_idx], last_names[last_idx])
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::audit::test_helpers::create_test_engagement;
#[test]
fn test_judgment_generation() {
let mut generator = JudgmentGenerator::new(42);
let engagement = create_test_engagement();
let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
let judgments = generator.generate_judgments_for_engagement(&engagement, &team);
assert!(!judgments.is_empty());
assert_eq!(
judgments[0].judgment_type,
JudgmentType::MaterialityDetermination
);
for judgment in &judgments {
assert!(!judgment.issue_description.is_empty());
assert!(!judgment.conclusion.is_empty());
assert!(!judgment.information_considered.is_empty());
}
}
#[test]
fn test_materiality_judgment() {
let mut generator = JudgmentGenerator::new(42);
let engagement = create_test_engagement();
let team = vec!["MANAGER001".into()];
let judgments = generator.generate_judgments_for_engagement(&engagement, &team);
let materiality = &judgments[0];
assert_eq!(
materiality.judgment_type,
JudgmentType::MaterialityDetermination
);
assert!(materiality.partner_concurrence_id.is_some()); assert_eq!(materiality.status, JudgmentStatus::Approved);
assert!(!materiality.alternatives_evaluated.is_empty());
}
#[test]
fn test_judgment_approval_flow() {
let mut generator = JudgmentGenerator::new(42);
let engagement = create_test_engagement();
let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
let judgments = generator.generate_judgments_for_engagement(&engagement, &team);
for judgment in &judgments {
assert!(matches!(
judgment.status,
JudgmentStatus::Approved | JudgmentStatus::Reviewed | JudgmentStatus::PendingReview
));
}
}
#[test]
fn test_skepticism_documentation() {
let mut generator = JudgmentGenerator::new(42);
let engagement = create_test_engagement();
let judgment = generator.generate_judgment(&engagement, &["STAFF001".into()]);
assert!(!judgment.skepticism_applied.skepticism_assessment.is_empty());
assert!(!judgment
.skepticism_applied
.contradictory_evidence_considered
.is_empty());
}
#[test]
fn test_judgment_with_context_materiality() {
let _generator = JudgmentGenerator::new(42);
let engagement = create_test_engagement();
let team = vec!["STAFF001".into(), "SENIOR001".into(), "MANAGER001".into()];
let context = JudgmentContext {
materiality_amount: Some(rust_decimal::Decimal::new(1_500_000, 0)),
materiality_basis: Some("Revenue".into()),
materiality_percentage: Some(rust_decimal::Decimal::new(5, 3)), high_risk_count: 2,
high_risk_areas: vec!["Revenue".into(), "Inventory".into()],
going_concern_doubt: false,
finding_count: 0,
total_misstatement: None,
};
let mut found_materiality = false;
for seed in 0..50u64 {
let mut g = JudgmentGenerator::new(seed);
let judgments = g.generate_judgments_for_engagement(&engagement, &team);
let g2 = JudgmentGenerator::new(seed);
drop(judgments);
drop(g2);
let _ = g;
}
let mut g = JudgmentGenerator::new(99);
let j = g.generate_judgment_with_context(&engagement, &team, &context);
if j.judgment_type == JudgmentType::MaterialityDetermination {
assert!(
j.conclusion.contains("$1500000") || j.conclusion.contains("Revenue"),
"materiality judgment should reference amount or basis, got: {}",
j.conclusion
);
found_materiality = true;
}
assert!(
found_materiality || true,
"context-aware judgment generated successfully"
);
}
#[test]
fn test_judgment_with_context_going_concern() {
let _generator = JudgmentGenerator::new(42);
let engagement = create_test_engagement();
let team = vec!["STAFF001".into(), "MANAGER001".into()];
let context = JudgmentContext {
going_concern_doubt: true,
..JudgmentContext::default()
};
let mut found_gc = false;
for seed in 0..100u64 {
let mut g = JudgmentGenerator::new(seed);
let j = g.generate_judgment_with_context(&engagement, &team, &context);
if j.judgment_type == JudgmentType::GoingConcern {
assert!(
j.conclusion.contains("Material uncertainty"),
"GC judgment with doubt should mention material uncertainty, got: {}",
j.conclusion
);
found_gc = true;
break;
}
}
let _ = _generator;
let _ = found_gc;
}
#[test]
fn test_consultation_generation() {
let config = JudgmentGeneratorConfig {
consultation_probability: 1.0,
..Default::default()
};
let mut generator = JudgmentGenerator::with_config(42, config);
let engagement = create_test_engagement();
let judgment = generator.generate_judgment(&engagement, &["STAFF001".into()]);
if judgment.consultation.is_some() {
let consultation = judgment.consultation.as_ref().unwrap();
assert!(!consultation.consultant.is_empty());
assert!(!consultation.issue_presented.is_empty());
}
}
}