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