use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use super::workpaper::Assertion;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvidence {
pub evidence_id: Uuid,
pub evidence_ref: String,
pub engagement_id: Uuid,
pub evidence_type: EvidenceType,
pub source_type: EvidenceSource,
pub title: String,
pub description: String,
pub obtained_date: NaiveDate,
pub obtained_by: String,
pub file_hash: Option<String>,
pub file_path: Option<String>,
pub file_size: Option<u64>,
pub reliability_assessment: ReliabilityAssessment,
pub assertions_addressed: Vec<Assertion>,
pub accounts_impacted: Vec<String>,
pub process_areas: Vec<String>,
pub linked_workpapers: Vec<Uuid>,
pub related_evidence: Vec<Uuid>,
pub ai_extracted_terms: Option<HashMap<String, String>>,
pub ai_confidence: Option<f64>,
pub ai_summary: Option<String>,
pub status: EvidenceStatus,
#[serde(with = "crate::serde_timestamp::utc")]
pub created_at: DateTime<Utc>,
#[serde(with = "crate::serde_timestamp::utc")]
pub updated_at: DateTime<Utc>,
}
impl AuditEvidence {
pub fn new(
engagement_id: Uuid,
evidence_type: EvidenceType,
source_type: EvidenceSource,
title: &str,
) -> Self {
let now = Utc::now();
Self {
evidence_id: Uuid::new_v4(),
evidence_ref: format!("EV-{}", Uuid::new_v4().simple()),
engagement_id,
evidence_type,
source_type,
title: title.into(),
description: String::new(),
obtained_date: now.date_naive(),
obtained_by: String::new(),
file_hash: None,
file_path: None,
file_size: None,
reliability_assessment: ReliabilityAssessment::default(),
assertions_addressed: Vec::new(),
accounts_impacted: Vec::new(),
process_areas: Vec::new(),
linked_workpapers: Vec::new(),
related_evidence: Vec::new(),
ai_extracted_terms: None,
ai_confidence: None,
ai_summary: None,
status: EvidenceStatus::Obtained,
created_at: now,
updated_at: now,
}
}
pub fn with_description(mut self, description: &str) -> Self {
self.description = description.into();
self
}
pub fn with_obtained_by(mut self, obtained_by: &str, date: NaiveDate) -> Self {
self.obtained_by = obtained_by.into();
self.obtained_date = date;
self
}
pub fn with_file_info(mut self, path: &str, hash: &str, size: u64) -> Self {
self.file_path = Some(path.into());
self.file_hash = Some(hash.into());
self.file_size = Some(size);
self
}
pub fn with_reliability(mut self, assessment: ReliabilityAssessment) -> Self {
self.reliability_assessment = assessment;
self
}
pub fn with_assertions(mut self, assertions: Vec<Assertion>) -> Self {
self.assertions_addressed = assertions;
self
}
pub fn with_ai_extraction(
mut self,
terms: HashMap<String, String>,
confidence: f64,
summary: &str,
) -> Self {
self.ai_extracted_terms = Some(terms);
self.ai_confidence = Some(confidence);
self.ai_summary = Some(summary.into());
self
}
pub fn link_workpaper(&mut self, workpaper_id: Uuid) {
if !self.linked_workpapers.contains(&workpaper_id) {
self.linked_workpapers.push(workpaper_id);
self.updated_at = Utc::now();
}
}
pub fn overall_reliability(&self) -> ReliabilityLevel {
self.reliability_assessment.overall_reliability
}
pub fn is_high_quality(&self) -> bool {
matches!(
self.reliability_assessment.overall_reliability,
ReliabilityLevel::High
) && matches!(
self.source_type,
EvidenceSource::ExternalThirdParty | EvidenceSource::AuditorPrepared
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceType {
Confirmation,
#[default]
Document,
Analysis,
SystemExtract,
Contract,
BankStatement,
Invoice,
Email,
MeetingMinutes,
ManagementRepresentation,
LegalLetter,
SpecialistReport,
PhysicalObservation,
Recalculation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceSource {
ExternalThirdParty,
#[default]
ExternalClientProvided,
InternalClientPrepared,
AuditorPrepared,
}
impl EvidenceSource {
pub fn inherent_reliability(&self) -> ReliabilityLevel {
match self {
Self::ExternalThirdParty => ReliabilityLevel::High,
Self::AuditorPrepared => ReliabilityLevel::High,
Self::ExternalClientProvided => ReliabilityLevel::Medium,
Self::InternalClientPrepared => ReliabilityLevel::Low,
}
}
pub fn description(&self) -> &'static str {
match self {
Self::ExternalThirdParty => "Obtained directly from independent external source",
Self::AuditorPrepared => "Prepared by the auditor",
Self::ExternalClientProvided => "External evidence provided by client",
Self::InternalClientPrepared => "Prepared internally by client personnel",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceStatus {
Requested,
#[default]
Obtained,
UnderReview,
Accepted,
Rejected,
Superseded,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ReliabilityAssessment {
pub independence_of_source: ReliabilityLevel,
pub effectiveness_of_controls: ReliabilityLevel,
pub qualifications_of_provider: ReliabilityLevel,
pub objectivity_of_provider: ReliabilityLevel,
pub overall_reliability: ReliabilityLevel,
pub notes: String,
}
impl ReliabilityAssessment {
pub fn new(
independence: ReliabilityLevel,
controls: ReliabilityLevel,
qualifications: ReliabilityLevel,
objectivity: ReliabilityLevel,
notes: &str,
) -> Self {
let overall = Self::calculate_overall(independence, controls, qualifications, objectivity);
Self {
independence_of_source: independence,
effectiveness_of_controls: controls,
qualifications_of_provider: qualifications,
objectivity_of_provider: objectivity,
overall_reliability: overall,
notes: notes.into(),
}
}
fn calculate_overall(
independence: ReliabilityLevel,
controls: ReliabilityLevel,
qualifications: ReliabilityLevel,
objectivity: ReliabilityLevel,
) -> ReliabilityLevel {
let scores = [
independence.score(),
controls.score(),
qualifications.score(),
objectivity.score(),
];
let avg = scores.iter().sum::<u8>() / 4;
ReliabilityLevel::from_score(avg)
}
pub fn high(notes: &str) -> Self {
Self::new(
ReliabilityLevel::High,
ReliabilityLevel::High,
ReliabilityLevel::High,
ReliabilityLevel::High,
notes,
)
}
pub fn medium(notes: &str) -> Self {
Self::new(
ReliabilityLevel::Medium,
ReliabilityLevel::Medium,
ReliabilityLevel::Medium,
ReliabilityLevel::Medium,
notes,
)
}
pub fn low(notes: &str) -> Self {
Self::new(
ReliabilityLevel::Low,
ReliabilityLevel::Low,
ReliabilityLevel::Low,
ReliabilityLevel::Low,
notes,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReliabilityLevel {
High,
#[default]
Medium,
Low,
}
impl ReliabilityLevel {
pub fn score(&self) -> u8 {
match self {
Self::High => 3,
Self::Medium => 2,
Self::Low => 1,
}
}
pub fn from_score(score: u8) -> Self {
match score {
0..=1 => Self::Low,
2 => Self::Medium,
_ => Self::High,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceSufficiency {
pub assertion: Assertion,
pub account_or_area: String,
pub evidence_count: u32,
pub total_reliability_score: f64,
pub risk_level: super::engagement::RiskLevel,
pub is_sufficient: bool,
pub conclusion_notes: String,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_evidence_creation() {
let evidence = AuditEvidence::new(
Uuid::new_v4(),
EvidenceType::Confirmation,
EvidenceSource::ExternalThirdParty,
"Bank Confirmation",
);
assert_eq!(evidence.evidence_type, EvidenceType::Confirmation);
assert_eq!(evidence.source_type, EvidenceSource::ExternalThirdParty);
}
#[test]
fn test_reliability_assessment() {
let assessment = ReliabilityAssessment::new(
ReliabilityLevel::High,
ReliabilityLevel::Medium,
ReliabilityLevel::High,
ReliabilityLevel::Medium,
"External confirmation with good controls",
);
assert_eq!(assessment.overall_reliability, ReliabilityLevel::Medium);
}
#[test]
fn test_source_reliability() {
assert_eq!(
EvidenceSource::ExternalThirdParty.inherent_reliability(),
ReliabilityLevel::High
);
assert_eq!(
EvidenceSource::InternalClientPrepared.inherent_reliability(),
ReliabilityLevel::Low
);
}
#[test]
fn test_evidence_quality() {
let evidence = AuditEvidence::new(
Uuid::new_v4(),
EvidenceType::Confirmation,
EvidenceSource::ExternalThirdParty,
"Bank Confirmation",
)
.with_reliability(ReliabilityAssessment::high("Direct confirmation"));
assert!(evidence.is_high_quality());
}
}