Skip to main content

datasynth_standards/audit/
audit_trail.rs

1//! Audit Trail for Complete Traceability.
2//!
3//! Provides structures for maintaining a complete audit trail from
4//! risk assessment through to conclusions, enabling traceability
5//! across the entire audit process.
6
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::isa_reference::IsaRequirement;
11
12/// Complete audit trail for an assertion or audit area.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct AuditTrail {
15    /// Unique trail identifier.
16    pub trail_id: Uuid,
17
18    /// Engagement ID.
19    pub engagement_id: Uuid,
20
21    /// Account or audit area.
22    pub account_or_area: String,
23
24    /// Assertion being addressed.
25    pub assertion: Assertion,
26
27    /// Risk assessment phase.
28    pub risk_assessment: RiskTrailNode,
29
30    /// Planned audit responses.
31    pub planned_responses: Vec<ResponseTrailNode>,
32
33    /// Procedures actually performed.
34    pub procedures_performed: Vec<ProcedureTrailNode>,
35
36    /// Evidence obtained.
37    pub evidence_obtained: Vec<EvidenceTrailNode>,
38
39    /// Conclusion reached.
40    pub conclusion: ConclusionTrailNode,
41
42    /// Gaps identified in the audit trail.
43    pub gaps_identified: Vec<TrailGap>,
44
45    /// ISA requirements addressed.
46    pub isa_coverage: Vec<IsaRequirement>,
47}
48
49impl AuditTrail {
50    /// Create a new audit trail.
51    pub fn new(
52        engagement_id: Uuid,
53        account_or_area: impl Into<String>,
54        assertion: Assertion,
55    ) -> Self {
56        Self {
57            trail_id: Uuid::now_v7(),
58            engagement_id,
59            account_or_area: account_or_area.into(),
60            assertion,
61            risk_assessment: RiskTrailNode::default(),
62            planned_responses: Vec::new(),
63            procedures_performed: Vec::new(),
64            evidence_obtained: Vec::new(),
65            conclusion: ConclusionTrailNode::default(),
66            gaps_identified: Vec::new(),
67            isa_coverage: Vec::new(),
68        }
69    }
70
71    /// Check if trail is complete (no gaps).
72    pub fn is_complete(&self) -> bool {
73        self.gaps_identified.is_empty()
74            && self.conclusion.conclusion_reached
75            && !self.evidence_obtained.is_empty()
76    }
77
78    /// Identify gaps in the audit trail.
79    pub fn identify_gaps(&mut self) {
80        self.gaps_identified.clear();
81
82        // Check for risk assessment gaps
83        if !self.risk_assessment.risk_identified {
84            self.gaps_identified.push(TrailGap {
85                gap_type: GapType::RiskAssessment,
86                description: "Risk of material misstatement not documented".to_string(),
87                severity: GapSeverity::High,
88                remediation_required: true,
89            });
90        }
91
92        // Check for response gaps
93        if self.planned_responses.is_empty() {
94            self.gaps_identified.push(TrailGap {
95                gap_type: GapType::PlannedResponse,
96                description: "No audit responses planned".to_string(),
97                severity: GapSeverity::High,
98                remediation_required: true,
99            });
100        }
101
102        // Check for procedures gap
103        if self.procedures_performed.is_empty() {
104            self.gaps_identified.push(TrailGap {
105                gap_type: GapType::ProceduresPerformed,
106                description: "No audit procedures performed".to_string(),
107                severity: GapSeverity::High,
108                remediation_required: true,
109            });
110        }
111
112        // Check for evidence gap
113        if self.evidence_obtained.is_empty() {
114            self.gaps_identified.push(TrailGap {
115                gap_type: GapType::Evidence,
116                description: "No audit evidence documented".to_string(),
117                severity: GapSeverity::High,
118                remediation_required: true,
119            });
120        }
121
122        // Check for conclusion gap
123        if !self.conclusion.conclusion_reached {
124            self.gaps_identified.push(TrailGap {
125                gap_type: GapType::Conclusion,
126                description: "No conclusion documented".to_string(),
127                severity: GapSeverity::High,
128                remediation_required: true,
129            });
130        }
131
132        // Check response-to-risk linkage
133        for response in &self.planned_responses {
134            if !self
135                .procedures_performed
136                .iter()
137                .any(|p| p.response_id == Some(response.response_id))
138            {
139                self.gaps_identified.push(TrailGap {
140                    gap_type: GapType::Linkage,
141                    description: format!(
142                        "Planned response '{}' not linked to performed procedure",
143                        response.response_description
144                    ),
145                    severity: GapSeverity::Medium,
146                    remediation_required: true,
147                });
148            }
149        }
150    }
151}
152
153/// Financial statement assertion.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
155#[serde(rename_all = "snake_case")]
156pub enum Assertion {
157    // Transaction assertions
158    /// Transactions and events occurred.
159    #[default]
160    Occurrence,
161    /// All transactions are recorded (none omitted).
162    Completeness,
163    /// Transactions are recorded in the correct period.
164    Cutoff,
165    /// Transactions are recorded at correct amounts.
166    Accuracy,
167    /// Transactions are recorded in proper accounts.
168    Classification,
169
170    // Balance assertions
171    /// Assets and liabilities exist.
172    Existence,
173    /// Entity has rights to assets and obligations for liabilities.
174    RightsAndObligations,
175    /// Assets and liabilities are recorded at appropriate amounts.
176    Valuation,
177
178    // Disclosure assertions
179    /// Disclosures are understandable.
180    Understandability,
181    /// Information is appropriately classified and described.
182    ClassificationAndUnderstandability,
183    /// Amounts are accurate and appropriately measured.
184    AccuracyAndValuation,
185}
186
187impl std::fmt::Display for Assertion {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        match self {
190            Self::Occurrence => write!(f, "Occurrence"),
191            Self::Completeness => write!(f, "Completeness"),
192            Self::Cutoff => write!(f, "Cutoff"),
193            Self::Accuracy => write!(f, "Accuracy"),
194            Self::Classification => write!(f, "Classification"),
195            Self::Existence => write!(f, "Existence"),
196            Self::RightsAndObligations => write!(f, "Rights and Obligations"),
197            Self::Valuation => write!(f, "Valuation"),
198            Self::Understandability => write!(f, "Understandability"),
199            Self::ClassificationAndUnderstandability => {
200                write!(f, "Classification and Understandability")
201            }
202            Self::AccuracyAndValuation => write!(f, "Accuracy and Valuation"),
203        }
204    }
205}
206
207/// Risk assessment trail node.
208#[derive(Debug, Clone, Default, Serialize, Deserialize)]
209pub struct RiskTrailNode {
210    /// Risk identified.
211    pub risk_identified: bool,
212
213    /// Risk description.
214    pub risk_description: String,
215
216    /// Risk level (inherent risk).
217    pub inherent_risk_level: AuditRiskLevel,
218
219    /// Control risk level.
220    pub control_risk_level: AuditRiskLevel,
221
222    /// Combined assessment (RoMM).
223    pub romm_level: AuditRiskLevel,
224
225    /// Significant risk designation.
226    pub is_significant_risk: bool,
227
228    /// Fraud risk identified.
229    pub fraud_risk_identified: bool,
230
231    /// Understanding of entity obtained.
232    pub understanding_documented: bool,
233
234    /// Internal controls evaluated.
235    pub controls_evaluated: bool,
236
237    /// Risk assessment workpaper reference.
238    pub workpaper_reference: Option<String>,
239}
240
241/// Risk level.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
243#[serde(rename_all = "snake_case")]
244pub enum AuditRiskLevel {
245    Low,
246    #[default]
247    Medium,
248    High,
249    Maximum,
250}
251
252impl std::fmt::Display for AuditRiskLevel {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        match self {
255            Self::Low => write!(f, "Low"),
256            Self::Medium => write!(f, "Medium"),
257            Self::High => write!(f, "High"),
258            Self::Maximum => write!(f, "Maximum"),
259        }
260    }
261}
262
263/// Planned response trail node.
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct ResponseTrailNode {
266    /// Response ID.
267    pub response_id: Uuid,
268
269    /// Response description.
270    pub response_description: String,
271
272    /// Type of response.
273    pub response_type: ResponseType,
274
275    /// Risk being addressed.
276    pub risk_addressed: String,
277
278    /// Nature of procedure.
279    pub procedure_nature: ProcedureNature,
280
281    /// Timing of procedure.
282    pub procedure_timing: ProcedureTiming,
283
284    /// Extent of procedure.
285    pub procedure_extent: String,
286
287    /// Staff assigned.
288    pub staff_assigned: Vec<String>,
289
290    /// Budgeted hours.
291    pub budgeted_hours: Option<f64>,
292}
293
294impl ResponseTrailNode {
295    /// Create a new response trail node.
296    pub fn new(response_description: impl Into<String>, response_type: ResponseType) -> Self {
297        Self {
298            response_id: Uuid::now_v7(),
299            response_description: response_description.into(),
300            response_type,
301            risk_addressed: String::new(),
302            procedure_nature: ProcedureNature::Substantive,
303            procedure_timing: ProcedureTiming::YearEnd,
304            procedure_extent: String::new(),
305            staff_assigned: Vec::new(),
306            budgeted_hours: None,
307        }
308    }
309}
310
311/// Type of audit response.
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
313#[serde(rename_all = "snake_case")]
314pub enum ResponseType {
315    /// Test of controls.
316    TestOfControls,
317    /// Substantive procedures.
318    #[default]
319    Substantive,
320    /// Combined approach.
321    Combined,
322    /// Overall response.
323    Overall,
324}
325
326/// Nature of audit procedure.
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
328#[serde(rename_all = "snake_case")]
329pub enum ProcedureNature {
330    /// Inspection of records/documents.
331    Inspection,
332    /// Physical observation.
333    Observation,
334    /// External confirmation.
335    Confirmation,
336    /// Recalculation.
337    Recalculation,
338    /// Reperformance.
339    Reperformance,
340    /// Analytical procedures.
341    Analytical,
342    /// Inquiry.
343    Inquiry,
344    #[default]
345    /// Substantive testing.
346    Substantive,
347}
348
349/// Timing of audit procedure.
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
351#[serde(rename_all = "snake_case")]
352pub enum ProcedureTiming {
353    /// At interim date.
354    Interim,
355    /// At year-end.
356    #[default]
357    YearEnd,
358    /// Roll-forward from interim to year-end.
359    RollForward,
360    /// Throughout the period.
361    Continuous,
362}
363
364/// Procedure performed trail node.
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct ProcedureTrailNode {
367    /// Procedure ID.
368    pub procedure_id: Uuid,
369
370    /// Linked response ID.
371    pub response_id: Option<Uuid>,
372
373    /// Procedure description.
374    pub procedure_description: String,
375
376    /// Date performed.
377    pub date_performed: chrono::NaiveDate,
378
379    /// Performed by.
380    pub performed_by: String,
381
382    /// Reviewed by.
383    pub reviewed_by: Option<String>,
384
385    /// Hours spent.
386    pub hours_spent: Option<f64>,
387
388    /// Population tested.
389    pub population_size: Option<u64>,
390
391    /// Sample size.
392    pub sample_size: Option<u64>,
393
394    /// Exceptions found.
395    pub exceptions_found: u32,
396
397    /// Results summary.
398    pub results_summary: String,
399
400    /// Workpaper reference.
401    pub workpaper_reference: Option<String>,
402}
403
404impl ProcedureTrailNode {
405    /// Create a new procedure trail node.
406    pub fn new(
407        procedure_description: impl Into<String>,
408        date_performed: chrono::NaiveDate,
409        performed_by: impl Into<String>,
410    ) -> Self {
411        Self {
412            procedure_id: Uuid::now_v7(),
413            response_id: None,
414            procedure_description: procedure_description.into(),
415            date_performed,
416            performed_by: performed_by.into(),
417            reviewed_by: None,
418            hours_spent: None,
419            population_size: None,
420            sample_size: None,
421            exceptions_found: 0,
422            results_summary: String::new(),
423            workpaper_reference: None,
424        }
425    }
426}
427
428/// Evidence trail node.
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct EvidenceTrailNode {
431    /// Evidence ID.
432    pub evidence_id: Uuid,
433
434    /// Linked procedure ID.
435    pub procedure_id: Option<Uuid>,
436
437    /// Evidence type.
438    pub evidence_type: EvidenceType,
439
440    /// Evidence description.
441    pub evidence_description: String,
442
443    /// Source of evidence.
444    pub source: EvidenceSource,
445
446    /// Reliability assessment.
447    pub reliability: EvidenceReliability,
448
449    /// Relevance to assertion.
450    pub relevance: EvidenceRelevance,
451
452    /// Document reference.
453    pub document_reference: Option<String>,
454
455    /// Date obtained.
456    pub date_obtained: chrono::NaiveDate,
457}
458
459impl EvidenceTrailNode {
460    /// Create a new evidence trail node.
461    pub fn new(
462        evidence_type: EvidenceType,
463        evidence_description: impl Into<String>,
464        source: EvidenceSource,
465    ) -> Self {
466        Self {
467            evidence_id: Uuid::now_v7(),
468            procedure_id: None,
469            evidence_type,
470            evidence_description: evidence_description.into(),
471            source,
472            reliability: EvidenceReliability::Moderate,
473            relevance: EvidenceRelevance::Relevant,
474            document_reference: None,
475            date_obtained: chrono::Utc::now().date_naive(),
476        }
477    }
478}
479
480/// Type of audit evidence.
481#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
482#[serde(rename_all = "snake_case")]
483pub enum EvidenceType {
484    /// Physical examination.
485    Physical,
486    /// External confirmation.
487    Confirmation,
488    /// Documentary - external source.
489    DocumentaryExternal,
490    /// Documentary - internal source.
491    DocumentaryInternal,
492    /// Mathematical recalculation.
493    Recalculation,
494    /// Analytical evidence.
495    Analytical,
496    /// Management representation.
497    Representation,
498    /// Observation.
499    Observation,
500    /// Inquiry response.
501    Inquiry,
502}
503
504/// Source of audit evidence.
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
506#[serde(rename_all = "snake_case")]
507pub enum EvidenceSource {
508    /// External third party.
509    ExternalThirdParty,
510    /// External - client's records of external transactions.
511    ExternalClientRecords,
512    /// Internal to the entity.
513    #[default]
514    Internal,
515    /// Auditor-generated.
516    AuditorGenerated,
517}
518
519/// Reliability of audit evidence.
520#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
521#[serde(rename_all = "snake_case")]
522pub enum EvidenceReliability {
523    /// Low reliability.
524    Low,
525    /// Moderate reliability.
526    #[default]
527    Moderate,
528    /// High reliability.
529    High,
530}
531
532/// Relevance of audit evidence.
533#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
534#[serde(rename_all = "snake_case")]
535pub enum EvidenceRelevance {
536    /// Not relevant.
537    NotRelevant,
538    /// Partially relevant.
539    PartiallyRelevant,
540    /// Relevant.
541    #[default]
542    Relevant,
543    /// Directly relevant.
544    DirectlyRelevant,
545}
546
547/// Conclusion trail node.
548#[derive(Debug, Clone, Default, Serialize, Deserialize)]
549pub struct ConclusionTrailNode {
550    /// Conclusion reached.
551    pub conclusion_reached: bool,
552
553    /// Conclusion text.
554    pub conclusion_text: String,
555
556    /// Conclusion type.
557    pub conclusion_type: ConclusionType,
558
559    /// Misstatements identified.
560    pub misstatements_identified: Vec<MisstatementReference>,
561
562    /// Sufficient appropriate evidence obtained.
563    pub sufficient_evidence: bool,
564
565    /// Further procedures required.
566    pub further_procedures_required: bool,
567
568    /// Reference to summary memo.
569    pub summary_memo_reference: Option<String>,
570
571    /// Preparer.
572    pub prepared_by: String,
573
574    /// Reviewer.
575    pub reviewed_by: Option<String>,
576
577    /// Date concluded.
578    pub conclusion_date: Option<chrono::NaiveDate>,
579}
580
581/// Type of conclusion.
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
583#[serde(rename_all = "snake_case")]
584pub enum ConclusionType {
585    /// No exceptions noted.
586    #[default]
587    Satisfactory,
588    /// Minor issues, not material.
589    SatisfactoryWithMinorIssues,
590    /// Potential misstatement identified.
591    PotentialMisstatement,
592    /// Misstatement identified.
593    MisstatementIdentified,
594    /// Unable to conclude.
595    UnableToConclude,
596}
597
598/// Misstatement reference in conclusion.
599#[derive(Debug, Clone, Serialize, Deserialize)]
600pub struct MisstatementReference {
601    /// Misstatement ID.
602    pub misstatement_id: Uuid,
603
604    /// Description.
605    pub description: String,
606
607    /// Amount (if quantified).
608    pub amount: Option<rust_decimal::Decimal>,
609
610    /// Is it factual, judgmental, or projected.
611    pub misstatement_type: MisstatementType,
612}
613
614/// Type of misstatement.
615#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
616#[serde(rename_all = "snake_case")]
617pub enum MisstatementType {
618    /// Factual misstatement - no doubt.
619    Factual,
620    /// Judgmental misstatement - differences in estimates.
621    Judgmental,
622    /// Projected misstatement - extrapolated from sample.
623    Projected,
624}
625
626/// Gap in the audit trail.
627#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct TrailGap {
629    /// Type of gap.
630    pub gap_type: GapType,
631
632    /// Description of the gap.
633    pub description: String,
634
635    /// Severity of the gap.
636    pub severity: GapSeverity,
637
638    /// Whether remediation is required.
639    pub remediation_required: bool,
640}
641
642/// Type of audit trail gap.
643#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
644#[serde(rename_all = "snake_case")]
645pub enum GapType {
646    /// Gap in risk assessment.
647    RiskAssessment,
648    /// Gap in planned responses.
649    PlannedResponse,
650    /// Gap in procedures performed.
651    ProceduresPerformed,
652    /// Gap in evidence.
653    Evidence,
654    /// Gap in conclusion.
655    Conclusion,
656    /// Gap in linkage between elements.
657    Linkage,
658    /// Gap in documentation.
659    Documentation,
660}
661
662/// Severity of audit trail gap.
663#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
664#[serde(rename_all = "snake_case")]
665pub enum GapSeverity {
666    /// Low severity - documentation issue only.
667    Low,
668    /// Medium severity - could affect conclusions.
669    Medium,
670    /// High severity - significant audit quality concern.
671    High,
672}
673
674#[cfg(test)]
675#[allow(clippy::unwrap_used)]
676mod tests {
677    use super::*;
678
679    #[test]
680    fn test_audit_trail_creation() {
681        let trail = AuditTrail::new(Uuid::now_v7(), "Revenue", Assertion::Occurrence);
682
683        assert_eq!(trail.account_or_area, "Revenue");
684        assert_eq!(trail.assertion, Assertion::Occurrence);
685        assert!(!trail.is_complete());
686    }
687
688    #[test]
689    fn test_gap_identification() {
690        let mut trail = AuditTrail::new(Uuid::now_v7(), "Inventory", Assertion::Existence);
691
692        trail.identify_gaps();
693
694        // Should have gaps for all elements
695        assert!(!trail.gaps_identified.is_empty());
696        assert!(trail
697            .gaps_identified
698            .iter()
699            .any(|g| matches!(g.gap_type, GapType::RiskAssessment)));
700        assert!(trail
701            .gaps_identified
702            .iter()
703            .any(|g| matches!(g.gap_type, GapType::Evidence)));
704    }
705
706    #[test]
707    fn test_complete_trail() {
708        let mut trail = AuditTrail::new(Uuid::now_v7(), "Cash", Assertion::Existence);
709
710        // Populate all elements
711        trail.risk_assessment.risk_identified = true;
712        trail.risk_assessment.risk_description = "Risk of misappropriation".to_string();
713
714        let response =
715            ResponseTrailNode::new("Perform bank reconciliation", ResponseType::Substantive);
716        let response_id = response.response_id;
717        trail.planned_responses.push(response);
718
719        let mut procedure = ProcedureTrailNode::new(
720            "Reconciled bank to GL",
721            chrono::NaiveDate::from_ymd_opt(2024, 1, 31).unwrap(),
722            "Auditor A",
723        );
724        procedure.response_id = Some(response_id);
725        trail.procedures_performed.push(procedure);
726
727        trail.evidence_obtained.push(EvidenceTrailNode::new(
728            EvidenceType::DocumentaryExternal,
729            "Bank statement obtained",
730            EvidenceSource::ExternalThirdParty,
731        ));
732
733        trail.conclusion.conclusion_reached = true;
734        trail.conclusion.conclusion_type = ConclusionType::Satisfactory;
735        trail.conclusion.sufficient_evidence = true;
736
737        trail.identify_gaps();
738
739        assert!(trail.is_complete());
740        assert!(trail.gaps_identified.is_empty());
741    }
742}