Skip to main content

datasynth_core/models/audit/
evidence.rs

1//! Evidence models per ISA 500.
2//!
3//! Audit evidence is all information used by the auditor to arrive at
4//! conclusions on which the auditor's opinion is based.
5
6use chrono::{DateTime, NaiveDate, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11use super::workpaper::Assertion;
12
13/// Audit evidence representing supporting documentation.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AuditEvidence {
16    /// Unique evidence ID
17    pub evidence_id: Uuid,
18    /// External reference
19    pub evidence_ref: String,
20    /// Engagement ID
21    pub engagement_id: Uuid,
22    /// Type of evidence
23    pub evidence_type: EvidenceType,
24    /// Source of evidence
25    pub source_type: EvidenceSource,
26    /// Evidence title
27    pub title: String,
28    /// Description
29    pub description: String,
30
31    // === Obtaining Information ===
32    /// Date evidence was obtained
33    pub obtained_date: NaiveDate,
34    /// Who obtained the evidence
35    pub obtained_by: String,
36    /// File hash for integrity verification
37    pub file_hash: Option<String>,
38    /// File path or storage location
39    pub file_path: Option<String>,
40    /// File size in bytes
41    pub file_size: Option<u64>,
42
43    // === Reliability Assessment per ISA 500.A31 ===
44    /// Reliability assessment
45    pub reliability_assessment: ReliabilityAssessment,
46
47    // === Relevance ===
48    /// Assertions addressed by this evidence
49    pub assertions_addressed: Vec<Assertion>,
50    /// Account IDs impacted
51    pub accounts_impacted: Vec<String>,
52    /// Process areas covered
53    pub process_areas: Vec<String>,
54
55    // === Cross-References ===
56    /// Linked workpaper IDs
57    pub linked_workpapers: Vec<Uuid>,
58    /// Related evidence IDs
59    pub related_evidence: Vec<Uuid>,
60
61    // === AI Extraction (optional) ===
62    /// AI-extracted key terms
63    pub ai_extracted_terms: Option<HashMap<String, String>>,
64    /// AI extraction confidence
65    pub ai_confidence: Option<f64>,
66    /// AI summary
67    pub ai_summary: Option<String>,
68
69    // === Status ===
70    /// Current status of this evidence item
71    pub status: EvidenceStatus,
72
73    // === Metadata ===
74    #[serde(with = "crate::serde_timestamp::utc")]
75    pub created_at: DateTime<Utc>,
76    #[serde(with = "crate::serde_timestamp::utc")]
77    pub updated_at: DateTime<Utc>,
78}
79
80impl AuditEvidence {
81    /// Create new audit evidence.
82    pub fn new(
83        engagement_id: Uuid,
84        evidence_type: EvidenceType,
85        source_type: EvidenceSource,
86        title: &str,
87    ) -> Self {
88        let now = Utc::now();
89        Self {
90            evidence_id: Uuid::new_v4(),
91            evidence_ref: format!("EV-{}", Uuid::new_v4().simple()),
92            engagement_id,
93            evidence_type,
94            source_type,
95            title: title.into(),
96            description: String::new(),
97            obtained_date: now.date_naive(),
98            obtained_by: String::new(),
99            file_hash: None,
100            file_path: None,
101            file_size: None,
102            reliability_assessment: ReliabilityAssessment::default(),
103            assertions_addressed: Vec::new(),
104            accounts_impacted: Vec::new(),
105            process_areas: Vec::new(),
106            linked_workpapers: Vec::new(),
107            related_evidence: Vec::new(),
108            ai_extracted_terms: None,
109            ai_confidence: None,
110            ai_summary: None,
111            status: EvidenceStatus::Obtained,
112            created_at: now,
113            updated_at: now,
114        }
115    }
116
117    /// Set the description.
118    pub fn with_description(mut self, description: &str) -> Self {
119        self.description = description.into();
120        self
121    }
122
123    /// Set who obtained the evidence.
124    pub fn with_obtained_by(mut self, obtained_by: &str, date: NaiveDate) -> Self {
125        self.obtained_by = obtained_by.into();
126        self.obtained_date = date;
127        self
128    }
129
130    /// Set file information.
131    pub fn with_file_info(mut self, path: &str, hash: &str, size: u64) -> Self {
132        self.file_path = Some(path.into());
133        self.file_hash = Some(hash.into());
134        self.file_size = Some(size);
135        self
136    }
137
138    /// Set reliability assessment.
139    pub fn with_reliability(mut self, assessment: ReliabilityAssessment) -> Self {
140        self.reliability_assessment = assessment;
141        self
142    }
143
144    /// Add assertions addressed.
145    pub fn with_assertions(mut self, assertions: Vec<Assertion>) -> Self {
146        self.assertions_addressed = assertions;
147        self
148    }
149
150    /// Add AI extraction results.
151    pub fn with_ai_extraction(
152        mut self,
153        terms: HashMap<String, String>,
154        confidence: f64,
155        summary: &str,
156    ) -> Self {
157        self.ai_extracted_terms = Some(terms);
158        self.ai_confidence = Some(confidence);
159        self.ai_summary = Some(summary.into());
160        self
161    }
162
163    /// Link to a workpaper.
164    pub fn link_workpaper(&mut self, workpaper_id: Uuid) {
165        if !self.linked_workpapers.contains(&workpaper_id) {
166            self.linked_workpapers.push(workpaper_id);
167            self.updated_at = Utc::now();
168        }
169    }
170
171    /// Get the overall reliability level.
172    pub fn overall_reliability(&self) -> ReliabilityLevel {
173        self.reliability_assessment.overall_reliability
174    }
175
176    /// Check if this is high-quality evidence.
177    pub fn is_high_quality(&self) -> bool {
178        matches!(
179            self.reliability_assessment.overall_reliability,
180            ReliabilityLevel::High
181        ) && matches!(
182            self.source_type,
183            EvidenceSource::ExternalThirdParty | EvidenceSource::AuditorPrepared
184        )
185    }
186}
187
188/// Type of audit evidence.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
190#[serde(rename_all = "snake_case")]
191pub enum EvidenceType {
192    /// Confirmation from third party
193    Confirmation,
194    /// Client-prepared document
195    #[default]
196    Document,
197    /// Auditor-prepared analysis
198    Analysis,
199    /// Screenshot or system extract
200    SystemExtract,
201    /// Contract or agreement
202    Contract,
203    /// Bank statement
204    BankStatement,
205    /// Invoice
206    Invoice,
207    /// Email correspondence
208    Email,
209    /// Meeting minutes
210    MeetingMinutes,
211    /// Management representation
212    ManagementRepresentation,
213    /// Legal letter
214    LegalLetter,
215    /// Specialist report
216    SpecialistReport,
217    /// Physical inventory observation
218    PhysicalObservation,
219    /// Recalculation spreadsheet
220    Recalculation,
221}
222
223/// Source of audit evidence per ISA 500.A31.
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
225#[serde(rename_all = "snake_case")]
226pub enum EvidenceSource {
227    /// External source, directly from third party
228    ExternalThirdParty,
229    /// External source, provided by client
230    #[default]
231    ExternalClientProvided,
232    /// Internal source, client prepared
233    InternalClientPrepared,
234    /// Auditor prepared
235    AuditorPrepared,
236}
237
238impl EvidenceSource {
239    /// Get the inherent reliability of this source type.
240    pub fn inherent_reliability(&self) -> ReliabilityLevel {
241        match self {
242            Self::ExternalThirdParty => ReliabilityLevel::High,
243            Self::AuditorPrepared => ReliabilityLevel::High,
244            Self::ExternalClientProvided => ReliabilityLevel::Medium,
245            Self::InternalClientPrepared => ReliabilityLevel::Low,
246        }
247    }
248
249    /// Get a description for ISA documentation.
250    pub fn description(&self) -> &'static str {
251        match self {
252            Self::ExternalThirdParty => "Obtained directly from independent external source",
253            Self::AuditorPrepared => "Prepared by the auditor",
254            Self::ExternalClientProvided => "External evidence provided by client",
255            Self::InternalClientPrepared => "Prepared internally by client personnel",
256        }
257    }
258}
259
260/// Status of an evidence item through its lifecycle.
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
262#[serde(rename_all = "snake_case")]
263pub enum EvidenceStatus {
264    /// Evidence has been requested but not yet received
265    Requested,
266    /// Evidence has been obtained
267    #[default]
268    Obtained,
269    /// Evidence is under review
270    UnderReview,
271    /// Evidence has been accepted
272    Accepted,
273    /// Evidence has been rejected
274    Rejected,
275    /// Evidence has been superseded by newer evidence
276    Superseded,
277}
278
279/// Reliability assessment per ISA 500.A31.
280#[derive(Debug, Clone, Serialize, Deserialize, Default)]
281pub struct ReliabilityAssessment {
282    /// Independence of source
283    pub independence_of_source: ReliabilityLevel,
284    /// Effectiveness of related controls
285    pub effectiveness_of_controls: ReliabilityLevel,
286    /// Qualifications of information provider
287    pub qualifications_of_provider: ReliabilityLevel,
288    /// Objectivity of information provider
289    pub objectivity_of_provider: ReliabilityLevel,
290    /// Overall reliability conclusion
291    pub overall_reliability: ReliabilityLevel,
292    /// Assessment notes
293    pub notes: String,
294}
295
296impl ReliabilityAssessment {
297    /// Create a new reliability assessment.
298    pub fn new(
299        independence: ReliabilityLevel,
300        controls: ReliabilityLevel,
301        qualifications: ReliabilityLevel,
302        objectivity: ReliabilityLevel,
303        notes: &str,
304    ) -> Self {
305        let overall = Self::calculate_overall(independence, controls, qualifications, objectivity);
306        Self {
307            independence_of_source: independence,
308            effectiveness_of_controls: controls,
309            qualifications_of_provider: qualifications,
310            objectivity_of_provider: objectivity,
311            overall_reliability: overall,
312            notes: notes.into(),
313        }
314    }
315
316    /// Calculate overall reliability from components.
317    fn calculate_overall(
318        independence: ReliabilityLevel,
319        controls: ReliabilityLevel,
320        qualifications: ReliabilityLevel,
321        objectivity: ReliabilityLevel,
322    ) -> ReliabilityLevel {
323        let scores = [
324            independence.score(),
325            controls.score(),
326            qualifications.score(),
327            objectivity.score(),
328        ];
329        let avg = scores.iter().sum::<u8>() / 4;
330        ReliabilityLevel::from_score(avg)
331    }
332
333    /// Create a high reliability assessment.
334    pub fn high(notes: &str) -> Self {
335        Self::new(
336            ReliabilityLevel::High,
337            ReliabilityLevel::High,
338            ReliabilityLevel::High,
339            ReliabilityLevel::High,
340            notes,
341        )
342    }
343
344    /// Create a medium reliability assessment.
345    pub fn medium(notes: &str) -> Self {
346        Self::new(
347            ReliabilityLevel::Medium,
348            ReliabilityLevel::Medium,
349            ReliabilityLevel::Medium,
350            ReliabilityLevel::Medium,
351            notes,
352        )
353    }
354
355    /// Create a low reliability assessment.
356    pub fn low(notes: &str) -> Self {
357        Self::new(
358            ReliabilityLevel::Low,
359            ReliabilityLevel::Low,
360            ReliabilityLevel::Low,
361            ReliabilityLevel::Low,
362            notes,
363        )
364    }
365}
366
367/// Reliability level for evidence assessment.
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
369#[serde(rename_all = "snake_case")]
370pub enum ReliabilityLevel {
371    /// High reliability
372    High,
373    /// Medium reliability
374    #[default]
375    Medium,
376    /// Low reliability
377    Low,
378}
379
380impl ReliabilityLevel {
381    /// Get numeric score.
382    pub fn score(&self) -> u8 {
383        match self {
384            Self::High => 3,
385            Self::Medium => 2,
386            Self::Low => 1,
387        }
388    }
389
390    /// Create from score.
391    pub fn from_score(score: u8) -> Self {
392        match score {
393            0..=1 => Self::Low,
394            2 => Self::Medium,
395            _ => Self::High,
396        }
397    }
398}
399
400/// Evidence sufficiency evaluation per ISA 500.
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct EvidenceSufficiency {
403    /// Assertion being evaluated
404    pub assertion: Assertion,
405    /// Account or area
406    pub account_or_area: String,
407    /// Evidence pieces collected
408    pub evidence_count: u32,
409    /// Total reliability score
410    pub total_reliability_score: f64,
411    /// Risk level being addressed
412    pub risk_level: super::engagement::RiskLevel,
413    /// Is evidence sufficient?
414    pub is_sufficient: bool,
415    /// Sufficiency conclusion notes
416    pub conclusion_notes: String,
417}
418
419#[cfg(test)]
420#[allow(clippy::unwrap_used)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_evidence_creation() {
426        let evidence = AuditEvidence::new(
427            Uuid::new_v4(),
428            EvidenceType::Confirmation,
429            EvidenceSource::ExternalThirdParty,
430            "Bank Confirmation",
431        );
432
433        assert_eq!(evidence.evidence_type, EvidenceType::Confirmation);
434        assert_eq!(evidence.source_type, EvidenceSource::ExternalThirdParty);
435    }
436
437    #[test]
438    fn test_reliability_assessment() {
439        let assessment = ReliabilityAssessment::new(
440            ReliabilityLevel::High,
441            ReliabilityLevel::Medium,
442            ReliabilityLevel::High,
443            ReliabilityLevel::Medium,
444            "External confirmation with good controls",
445        );
446
447        assert_eq!(assessment.overall_reliability, ReliabilityLevel::Medium);
448    }
449
450    #[test]
451    fn test_source_reliability() {
452        assert_eq!(
453            EvidenceSource::ExternalThirdParty.inherent_reliability(),
454            ReliabilityLevel::High
455        );
456        assert_eq!(
457            EvidenceSource::InternalClientPrepared.inherent_reliability(),
458            ReliabilityLevel::Low
459        );
460    }
461
462    #[test]
463    fn test_evidence_quality() {
464        let evidence = AuditEvidence::new(
465            Uuid::new_v4(),
466            EvidenceType::Confirmation,
467            EvidenceSource::ExternalThirdParty,
468            "Bank Confirmation",
469        )
470        .with_reliability(ReliabilityAssessment::high("Direct confirmation"));
471
472        assert!(evidence.is_high_quality());
473    }
474}