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 #[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 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 pub fn with_description(mut self, description: &str) -> Self {
119 self.description = description.into();
120 self
121 }
122
123 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 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 pub fn with_reliability(mut self, assessment: ReliabilityAssessment) -> Self {
140 self.reliability_assessment = assessment;
141 self
142 }
143
144 pub fn with_assertions(mut self, assertions: Vec<Assertion>) -> Self {
146 self.assertions_addressed = assertions;
147 self
148 }
149
150 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 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 pub fn overall_reliability(&self) -> ReliabilityLevel {
173 self.reliability_assessment.overall_reliability
174 }
175
176 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
190#[serde(rename_all = "snake_case")]
191pub enum EvidenceType {
192 Confirmation,
194 #[default]
196 Document,
197 Analysis,
199 SystemExtract,
201 Contract,
203 BankStatement,
205 Invoice,
207 Email,
209 MeetingMinutes,
211 ManagementRepresentation,
213 LegalLetter,
215 SpecialistReport,
217 PhysicalObservation,
219 Recalculation,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
225#[serde(rename_all = "snake_case")]
226pub enum EvidenceSource {
227 ExternalThirdParty,
229 #[default]
231 ExternalClientProvided,
232 InternalClientPrepared,
234 AuditorPrepared,
236}
237
238impl EvidenceSource {
239 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
262#[serde(rename_all = "snake_case")]
263pub enum EvidenceStatus {
264 Requested,
266 #[default]
268 Obtained,
269 UnderReview,
271 Accepted,
273 Rejected,
275 Superseded,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize, Default)]
281pub struct ReliabilityAssessment {
282 pub independence_of_source: ReliabilityLevel,
284 pub effectiveness_of_controls: ReliabilityLevel,
286 pub qualifications_of_provider: ReliabilityLevel,
288 pub objectivity_of_provider: ReliabilityLevel,
290 pub overall_reliability: ReliabilityLevel,
292 pub notes: String,
294}
295
296impl ReliabilityAssessment {
297 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
369#[serde(rename_all = "snake_case")]
370pub enum ReliabilityLevel {
371 High,
373 #[default]
375 Medium,
376 Low,
378}
379
380impl ReliabilityLevel {
381 pub fn score(&self) -> u8 {
383 match self {
384 Self::High => 3,
385 Self::Medium => 2,
386 Self::Low => 1,
387 }
388 }
389
390 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#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct EvidenceSufficiency {
403 pub assertion: Assertion,
405 pub account_or_area: String,
407 pub evidence_count: u32,
409 pub total_reliability_score: f64,
411 pub risk_level: super::engagement::RiskLevel,
413 pub is_sufficient: bool,
415 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}