use crate::error::EvalResult;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditThresholds {
pub min_evidence_mapping: f64,
pub min_risk_procedure_mapping: f64,
pub min_workpaper_completeness: f64,
}
impl Default for AuditThresholds {
fn default() -> Self {
Self {
min_evidence_mapping: 0.90,
min_risk_procedure_mapping: 0.90,
min_workpaper_completeness: 0.85,
}
}
}
#[derive(Debug, Clone)]
pub struct AuditFindingData {
pub finding_id: String,
pub has_evidence: bool,
pub evidence_count: usize,
}
#[derive(Debug, Clone)]
pub struct AuditRiskData {
pub risk_id: String,
pub has_procedures: bool,
pub procedure_count: usize,
}
#[derive(Debug, Clone)]
pub struct WorkpaperData {
pub workpaper_id: String,
pub has_conclusion: bool,
pub has_references: bool,
pub has_preparer: bool,
pub has_reviewer: bool,
}
#[derive(Debug, Clone)]
pub struct MaterialityData {
pub overall_materiality: f64,
pub performance_materiality: f64,
pub clearly_trivial: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvaluation {
pub evidence_to_finding_rate: f64,
pub risk_to_procedure_rate: f64,
pub workpaper_completeness: f64,
pub materiality_hierarchy_valid: bool,
pub total_findings: usize,
pub total_risks: usize,
pub total_workpapers: usize,
pub passes: bool,
pub issues: Vec<String>,
}
pub struct AuditEvaluator {
thresholds: AuditThresholds,
}
impl AuditEvaluator {
pub fn new() -> Self {
Self {
thresholds: AuditThresholds::default(),
}
}
pub fn with_thresholds(thresholds: AuditThresholds) -> Self {
Self { thresholds }
}
pub fn evaluate(
&self,
findings: &[AuditFindingData],
risks: &[AuditRiskData],
workpapers: &[WorkpaperData],
materiality: &Option<MaterialityData>,
) -> EvalResult<AuditEvaluation> {
let mut issues = Vec::new();
let findings_with_evidence = findings.iter().filter(|f| f.has_evidence).count();
let evidence_to_finding_rate = if findings.is_empty() {
1.0
} else {
findings_with_evidence as f64 / findings.len() as f64
};
let risks_with_procedures = risks.iter().filter(|r| r.has_procedures).count();
let risk_to_procedure_rate = if risks.is_empty() {
1.0
} else {
risks_with_procedures as f64 / risks.len() as f64
};
let complete_workpapers = workpapers
.iter()
.filter(|w| w.has_conclusion && w.has_references)
.count();
let workpaper_completeness = if workpapers.is_empty() {
1.0
} else {
complete_workpapers as f64 / workpapers.len() as f64
};
let materiality_hierarchy_valid = if let Some(ref mat) = materiality {
mat.overall_materiality > mat.performance_materiality
&& mat.performance_materiality > mat.clearly_trivial
&& mat.clearly_trivial >= 0.0
} else {
true };
if evidence_to_finding_rate < self.thresholds.min_evidence_mapping {
issues.push(format!(
"Evidence-to-finding rate {:.3} < {:.3}",
evidence_to_finding_rate, self.thresholds.min_evidence_mapping
));
}
if risk_to_procedure_rate < self.thresholds.min_risk_procedure_mapping {
issues.push(format!(
"Risk-to-procedure rate {:.3} < {:.3}",
risk_to_procedure_rate, self.thresholds.min_risk_procedure_mapping
));
}
if workpaper_completeness < self.thresholds.min_workpaper_completeness {
issues.push(format!(
"Workpaper completeness {:.3} < {:.3}",
workpaper_completeness, self.thresholds.min_workpaper_completeness
));
}
if !materiality_hierarchy_valid {
issues.push(
"Materiality hierarchy invalid: expected overall > performance > trivial"
.to_string(),
);
}
let passes = issues.is_empty();
Ok(AuditEvaluation {
evidence_to_finding_rate,
risk_to_procedure_rate,
workpaper_completeness,
materiality_hierarchy_valid,
total_findings: findings.len(),
total_risks: risks.len(),
total_workpapers: workpapers.len(),
passes,
issues,
})
}
}
impl Default for AuditEvaluator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_valid_audit() {
let evaluator = AuditEvaluator::new();
let findings = vec![AuditFindingData {
finding_id: "F001".to_string(),
has_evidence: true,
evidence_count: 3,
}];
let risks = vec![AuditRiskData {
risk_id: "R001".to_string(),
has_procedures: true,
procedure_count: 2,
}];
let workpapers = vec![WorkpaperData {
workpaper_id: "WP001".to_string(),
has_conclusion: true,
has_references: true,
has_preparer: true,
has_reviewer: true,
}];
let materiality = Some(MaterialityData {
overall_materiality: 100_000.0,
performance_materiality: 75_000.0,
clearly_trivial: 5_000.0,
});
let result = evaluator
.evaluate(&findings, &risks, &workpapers, &materiality)
.unwrap();
assert!(result.passes);
assert!(result.materiality_hierarchy_valid);
}
#[test]
fn test_missing_evidence() {
let evaluator = AuditEvaluator::new();
let findings = vec![
AuditFindingData {
finding_id: "F001".to_string(),
has_evidence: false,
evidence_count: 0,
},
AuditFindingData {
finding_id: "F002".to_string(),
has_evidence: false,
evidence_count: 0,
},
];
let result = evaluator.evaluate(&findings, &[], &[], &None).unwrap();
assert!(!result.passes);
assert_eq!(result.evidence_to_finding_rate, 0.0);
}
#[test]
fn test_invalid_materiality() {
let evaluator = AuditEvaluator::new();
let materiality = Some(MaterialityData {
overall_materiality: 50_000.0,
performance_materiality: 100_000.0, clearly_trivial: 5_000.0,
});
let result = evaluator.evaluate(&[], &[], &[], &materiality).unwrap();
assert!(!result.materiality_hierarchy_valid);
assert!(!result.passes);
}
#[test]
fn test_empty_data() {
let evaluator = AuditEvaluator::new();
let result = evaluator.evaluate(&[], &[], &[], &None).unwrap();
assert!(result.passes);
}
}