1use chrono::{DateTime, NaiveDate, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11use super::workpaper::Assertion;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AuditEvidence {
16 pub evidence_id: Uuid,
18 pub evidence_ref: String,
20 pub engagement_id: Uuid,
22 pub evidence_type: EvidenceType,
24 pub source_type: EvidenceSource,
26 pub title: String,
28 pub description: String,
30
31 pub obtained_date: NaiveDate,
34 pub obtained_by: String,
36 pub file_hash: Option<String>,
38 pub file_path: Option<String>,
40 pub file_size: Option<u64>,
42
43 pub reliability_assessment: ReliabilityAssessment,
46
47 pub assertions_addressed: Vec<Assertion>,
50 pub accounts_impacted: Vec<String>,
52 pub process_areas: Vec<String>,
54
55 pub linked_workpapers: Vec<Uuid>,
58 pub related_evidence: Vec<Uuid>,
60
61 pub ai_extracted_terms: Option<HashMap<String, String>>,
64 pub ai_confidence: Option<f64>,
66 pub ai_summary: Option<String>,
68
69 pub status: EvidenceStatus,
72
73 pub created_at: DateTime<Utc>,
75 pub updated_at: DateTime<Utc>,
76}
77
78impl AuditEvidence {
79 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 pub fn with_description(mut self, description: &str) -> Self {
117 self.description = description.into();
118 self
119 }
120
121 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 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 pub fn with_reliability(mut self, assessment: ReliabilityAssessment) -> Self {
138 self.reliability_assessment = assessment;
139 self
140 }
141
142 pub fn with_assertions(mut self, assertions: Vec<Assertion>) -> Self {
144 self.assertions_addressed = assertions;
145 self
146 }
147
148 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 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 pub fn overall_reliability(&self) -> ReliabilityLevel {
171 self.reliability_assessment.overall_reliability
172 }
173
174 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
188#[serde(rename_all = "snake_case")]
189pub enum EvidenceType {
190 Confirmation,
192 #[default]
194 Document,
195 Analysis,
197 SystemExtract,
199 Contract,
201 BankStatement,
203 Invoice,
205 Email,
207 MeetingMinutes,
209 ManagementRepresentation,
211 LegalLetter,
213 SpecialistReport,
215 PhysicalObservation,
217 Recalculation,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
223#[serde(rename_all = "snake_case")]
224pub enum EvidenceSource {
225 ExternalThirdParty,
227 #[default]
229 ExternalClientProvided,
230 InternalClientPrepared,
232 AuditorPrepared,
234}
235
236impl EvidenceSource {
237 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
260#[serde(rename_all = "snake_case")]
261pub enum EvidenceStatus {
262 Requested,
264 #[default]
266 Obtained,
267 UnderReview,
269 Accepted,
271 Rejected,
273 Superseded,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, Default)]
279pub struct ReliabilityAssessment {
280 pub independence_of_source: ReliabilityLevel,
282 pub effectiveness_of_controls: ReliabilityLevel,
284 pub qualifications_of_provider: ReliabilityLevel,
286 pub objectivity_of_provider: ReliabilityLevel,
288 pub overall_reliability: ReliabilityLevel,
290 pub notes: String,
292}
293
294impl ReliabilityAssessment {
295 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
367#[serde(rename_all = "snake_case")]
368pub enum ReliabilityLevel {
369 High,
371 #[default]
373 Medium,
374 Low,
376}
377
378impl ReliabilityLevel {
379 pub fn score(&self) -> u8 {
381 match self {
382 Self::High => 3,
383 Self::Medium => 2,
384 Self::Low => 1,
385 }
386 }
387
388 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#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct EvidenceSufficiency {
401 pub assertion: Assertion,
403 pub account_or_area: String,
405 pub evidence_count: u32,
407 pub total_reliability_score: f64,
409 pub risk_level: super::engagement::RiskLevel,
411 pub is_sufficient: bool,
413 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}