datasynth_core/models/audit/
workpaper.rs

1//! Workpaper models per ISA 230.
2//!
3//! Workpapers document audit procedures performed, evidence obtained,
4//! and conclusions reached.
5
6use chrono::{DateTime, NaiveDate, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::engagement::RiskLevel;
11
12/// Audit workpaper representing documented audit work.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Workpaper {
15    /// Unique workpaper ID
16    pub workpaper_id: Uuid,
17    /// Workpaper reference (e.g., "A-100", "B-200")
18    pub workpaper_ref: String,
19    /// Engagement ID this workpaper belongs to
20    pub engagement_id: Uuid,
21    /// Workpaper title
22    pub title: String,
23    /// Section/area of the audit
24    pub section: WorkpaperSection,
25    /// Audit objective addressed
26    pub objective: String,
27    /// Financial statement assertions tested
28    pub assertions_tested: Vec<Assertion>,
29    /// Procedure performed
30    pub procedure_performed: String,
31    /// Procedure type
32    pub procedure_type: ProcedureType,
33
34    // === Scope ===
35    /// Testing scope
36    pub scope: WorkpaperScope,
37    /// Population size (total items)
38    pub population_size: u64,
39    /// Sample size (items tested)
40    pub sample_size: u32,
41    /// Sampling method used
42    pub sampling_method: SamplingMethod,
43
44    // === Results ===
45    /// Summary of results
46    pub results_summary: String,
47    /// Number of exceptions found
48    pub exceptions_found: u32,
49    /// Exception rate
50    pub exception_rate: f64,
51    /// Conclusion reached
52    pub conclusion: WorkpaperConclusion,
53    /// Risk level addressed
54    pub risk_level_addressed: RiskLevel,
55
56    // === References ===
57    /// Evidence reference IDs
58    pub evidence_refs: Vec<Uuid>,
59    /// Cross-references to other workpapers
60    pub cross_references: Vec<String>,
61    /// Related account IDs
62    pub account_ids: Vec<String>,
63
64    // === Sign-offs ===
65    /// Preparer user ID
66    pub preparer_id: String,
67    /// Preparer name
68    pub preparer_name: String,
69    /// Date prepared
70    pub preparer_date: NaiveDate,
71    /// First reviewer ID
72    pub reviewer_id: Option<String>,
73    /// First reviewer name
74    pub reviewer_name: Option<String>,
75    /// First review date
76    pub reviewer_date: Option<NaiveDate>,
77    /// Second reviewer (manager) ID
78    pub second_reviewer_id: Option<String>,
79    /// Second reviewer name
80    pub second_reviewer_name: Option<String>,
81    /// Second review date
82    pub second_reviewer_date: Option<NaiveDate>,
83
84    // === Status ===
85    /// Workpaper status
86    pub status: WorkpaperStatus,
87    /// Version number
88    pub version: u32,
89    /// Review notes
90    pub review_notes: Vec<ReviewNote>,
91
92    // === Timestamps ===
93    pub created_at: DateTime<Utc>,
94    pub updated_at: DateTime<Utc>,
95}
96
97impl Workpaper {
98    /// Create a new workpaper.
99    pub fn new(
100        engagement_id: Uuid,
101        workpaper_ref: &str,
102        title: &str,
103        section: WorkpaperSection,
104    ) -> Self {
105        let now = Utc::now();
106        Self {
107            workpaper_id: Uuid::new_v4(),
108            workpaper_ref: workpaper_ref.into(),
109            engagement_id,
110            title: title.into(),
111            section,
112            objective: String::new(),
113            assertions_tested: Vec::new(),
114            procedure_performed: String::new(),
115            procedure_type: ProcedureType::InquiryObservation,
116            scope: WorkpaperScope::default(),
117            population_size: 0,
118            sample_size: 0,
119            sampling_method: SamplingMethod::Judgmental,
120            results_summary: String::new(),
121            exceptions_found: 0,
122            exception_rate: 0.0,
123            conclusion: WorkpaperConclusion::Satisfactory,
124            risk_level_addressed: RiskLevel::Medium,
125            evidence_refs: Vec::new(),
126            cross_references: Vec::new(),
127            account_ids: Vec::new(),
128            preparer_id: String::new(),
129            preparer_name: String::new(),
130            preparer_date: now.date_naive(),
131            reviewer_id: None,
132            reviewer_name: None,
133            reviewer_date: None,
134            second_reviewer_id: None,
135            second_reviewer_name: None,
136            second_reviewer_date: None,
137            status: WorkpaperStatus::Draft,
138            version: 1,
139            review_notes: Vec::new(),
140            created_at: now,
141            updated_at: now,
142        }
143    }
144
145    /// Set the objective and assertions.
146    pub fn with_objective(mut self, objective: &str, assertions: Vec<Assertion>) -> Self {
147        self.objective = objective.into();
148        self.assertions_tested = assertions;
149        self
150    }
151
152    /// Set the procedure.
153    pub fn with_procedure(mut self, procedure: &str, procedure_type: ProcedureType) -> Self {
154        self.procedure_performed = procedure.into();
155        self.procedure_type = procedure_type;
156        self
157    }
158
159    /// Set the scope and sampling.
160    pub fn with_scope(
161        mut self,
162        scope: WorkpaperScope,
163        population: u64,
164        sample: u32,
165        method: SamplingMethod,
166    ) -> Self {
167        self.scope = scope;
168        self.population_size = population;
169        self.sample_size = sample;
170        self.sampling_method = method;
171        self
172    }
173
174    /// Set the results.
175    pub fn with_results(
176        mut self,
177        summary: &str,
178        exceptions: u32,
179        conclusion: WorkpaperConclusion,
180    ) -> Self {
181        self.results_summary = summary.into();
182        self.exceptions_found = exceptions;
183        self.exception_rate = if self.sample_size > 0 {
184            exceptions as f64 / self.sample_size as f64
185        } else {
186            0.0
187        };
188        self.conclusion = conclusion;
189        self
190    }
191
192    /// Set the preparer.
193    pub fn with_preparer(mut self, id: &str, name: &str, date: NaiveDate) -> Self {
194        self.preparer_id = id.into();
195        self.preparer_name = name.into();
196        self.preparer_date = date;
197        self
198    }
199
200    /// Add first reviewer sign-off.
201    pub fn add_first_review(&mut self, id: &str, name: &str, date: NaiveDate) {
202        self.reviewer_id = Some(id.into());
203        self.reviewer_name = Some(name.into());
204        self.reviewer_date = Some(date);
205        self.status = WorkpaperStatus::FirstReviewComplete;
206        self.updated_at = Utc::now();
207    }
208
209    /// Add second reviewer sign-off.
210    pub fn add_second_review(&mut self, id: &str, name: &str, date: NaiveDate) {
211        self.second_reviewer_id = Some(id.into());
212        self.second_reviewer_name = Some(name.into());
213        self.second_reviewer_date = Some(date);
214        self.status = WorkpaperStatus::Complete;
215        self.updated_at = Utc::now();
216    }
217
218    /// Add a review note.
219    pub fn add_review_note(&mut self, reviewer: &str, note: &str) {
220        self.review_notes.push(ReviewNote {
221            note_id: Uuid::new_v4(),
222            reviewer_id: reviewer.into(),
223            note: note.into(),
224            status: ReviewNoteStatus::Open,
225            created_at: Utc::now(),
226            resolved_at: None,
227        });
228        self.updated_at = Utc::now();
229    }
230
231    /// Check if the workpaper is complete.
232    pub fn is_complete(&self) -> bool {
233        matches!(self.status, WorkpaperStatus::Complete)
234    }
235
236    /// Check if all review notes are resolved.
237    pub fn all_notes_resolved(&self) -> bool {
238        self.review_notes.iter().all(|n| {
239            matches!(
240                n.status,
241                ReviewNoteStatus::Resolved | ReviewNoteStatus::NotApplicable
242            )
243        })
244    }
245}
246
247/// Workpaper section/area.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
249#[serde(rename_all = "snake_case")]
250pub enum WorkpaperSection {
251    /// Planning documentation
252    #[default]
253    Planning,
254    /// Risk assessment procedures
255    RiskAssessment,
256    /// Internal control testing
257    ControlTesting,
258    /// Substantive testing
259    SubstantiveTesting,
260    /// Completion procedures
261    Completion,
262    /// Reporting
263    Reporting,
264    /// Permanent file
265    PermanentFile,
266}
267
268impl WorkpaperSection {
269    /// Get the typical workpaper reference prefix for this section.
270    pub fn reference_prefix(&self) -> &'static str {
271        match self {
272            Self::Planning => "A",
273            Self::RiskAssessment => "B",
274            Self::ControlTesting => "C",
275            Self::SubstantiveTesting => "D",
276            Self::Completion => "E",
277            Self::Reporting => "F",
278            Self::PermanentFile => "P",
279        }
280    }
281}
282
283/// Financial statement assertions per ISA 315.
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum Assertion {
287    // Transaction assertions
288    /// Transactions occurred and relate to the entity
289    Occurrence,
290    /// All transactions that should have been recorded have been recorded
291    Completeness,
292    /// Amounts and data relating to transactions have been recorded appropriately
293    Accuracy,
294    /// Transactions have been recorded in the correct accounting period
295    Cutoff,
296    /// Transactions have been recorded in the proper accounts
297    Classification,
298
299    // Balance assertions
300    /// Assets, liabilities, and equity interests exist
301    Existence,
302    /// The entity holds rights to assets and liabilities are obligations
303    RightsAndObligations,
304    /// Assets, liabilities, and equity interests are included at appropriate amounts
305    ValuationAndAllocation,
306
307    // Presentation assertions
308    /// Financial information is appropriately presented and described
309    PresentationAndDisclosure,
310}
311
312impl Assertion {
313    /// Get all transaction-level assertions.
314    pub fn transaction_assertions() -> Vec<Self> {
315        vec![
316            Self::Occurrence,
317            Self::Completeness,
318            Self::Accuracy,
319            Self::Cutoff,
320            Self::Classification,
321        ]
322    }
323
324    /// Get all balance-level assertions.
325    pub fn balance_assertions() -> Vec<Self> {
326        vec![
327            Self::Existence,
328            Self::Completeness,
329            Self::RightsAndObligations,
330            Self::ValuationAndAllocation,
331        ]
332    }
333
334    /// Get a human-readable description.
335    pub fn description(&self) -> &'static str {
336        match self {
337            Self::Occurrence => "Transactions and events have occurred and pertain to the entity",
338            Self::Completeness => {
339                "All transactions and events that should have been recorded have been recorded"
340            }
341            Self::Accuracy => "Amounts and other data have been recorded appropriately",
342            Self::Cutoff => "Transactions and events have been recorded in the correct period",
343            Self::Classification => {
344                "Transactions and events have been recorded in the proper accounts"
345            }
346            Self::Existence => "Assets, liabilities, and equity interests exist",
347            Self::RightsAndObligations => {
348                "The entity holds rights to assets and liabilities are obligations of the entity"
349            }
350            Self::ValuationAndAllocation => {
351                "Assets, liabilities, and equity interests are included at appropriate amounts"
352            }
353            Self::PresentationAndDisclosure => {
354                "Financial information is appropriately presented and described"
355            }
356        }
357    }
358}
359
360/// Type of audit procedure.
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
362#[serde(rename_all = "snake_case")]
363pub enum ProcedureType {
364    /// Inquiry and observation
365    #[default]
366    InquiryObservation,
367    /// Inspection of records
368    Inspection,
369    /// External confirmation
370    Confirmation,
371    /// Recalculation
372    Recalculation,
373    /// Reperformance
374    Reperformance,
375    /// Analytical procedures
376    AnalyticalProcedures,
377    /// Test of controls
378    TestOfControls,
379    /// Substantive test of details
380    SubstantiveTest,
381    /// Combined approach
382    Combined,
383}
384
385/// Testing scope.
386#[derive(Debug, Clone, Serialize, Deserialize, Default)]
387pub struct WorkpaperScope {
388    /// Coverage percentage
389    pub coverage_percentage: f64,
390    /// Period covered start
391    pub period_start: Option<NaiveDate>,
392    /// Period covered end
393    pub period_end: Option<NaiveDate>,
394    /// Scope limitations
395    pub limitations: Vec<String>,
396}
397
398/// Sampling method used.
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
400#[serde(rename_all = "snake_case")]
401pub enum SamplingMethod {
402    /// Statistical random sampling
403    StatisticalRandom,
404    /// Monetary unit sampling
405    MonetaryUnit,
406    /// Judgmental selection
407    #[default]
408    Judgmental,
409    /// Haphazard selection
410    Haphazard,
411    /// Block selection
412    Block,
413    /// All items (100% testing)
414    AllItems,
415}
416
417/// Workpaper conclusion.
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
419#[serde(rename_all = "snake_case")]
420pub enum WorkpaperConclusion {
421    /// Satisfactory - no exceptions or immaterial exceptions
422    #[default]
423    Satisfactory,
424    /// Satisfactory with exceptions noted
425    SatisfactoryWithExceptions,
426    /// Unsatisfactory - material exceptions
427    Unsatisfactory,
428    /// Unable to conclude - scope limitation
429    UnableToConclude,
430    /// Additional procedures required
431    AdditionalProceduresRequired,
432}
433
434/// Workpaper status.
435#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
436#[serde(rename_all = "snake_case")]
437pub enum WorkpaperStatus {
438    /// Draft
439    #[default]
440    Draft,
441    /// Pending review
442    PendingReview,
443    /// First review complete
444    FirstReviewComplete,
445    /// Pending second review
446    PendingSecondReview,
447    /// Complete
448    Complete,
449    /// Superseded
450    Superseded,
451}
452
453/// Review note on a workpaper.
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct ReviewNote {
456    /// Note ID
457    pub note_id: Uuid,
458    /// Reviewer who added the note
459    pub reviewer_id: String,
460    /// Note content
461    pub note: String,
462    /// Note status
463    pub status: ReviewNoteStatus,
464    /// When the note was created
465    pub created_at: DateTime<Utc>,
466    /// When the note was resolved
467    pub resolved_at: Option<DateTime<Utc>>,
468}
469
470/// Review note status.
471#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
472#[serde(rename_all = "snake_case")]
473pub enum ReviewNoteStatus {
474    /// Open - needs action
475    #[default]
476    Open,
477    /// In progress
478    InProgress,
479    /// Resolved
480    Resolved,
481    /// Not applicable
482    NotApplicable,
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    #[test]
490    fn test_workpaper_creation() {
491        let wp = Workpaper::new(
492            Uuid::new_v4(),
493            "C-100",
494            "Revenue Recognition Testing",
495            WorkpaperSection::SubstantiveTesting,
496        );
497
498        assert_eq!(wp.workpaper_ref, "C-100");
499        assert_eq!(wp.section, WorkpaperSection::SubstantiveTesting);
500        assert_eq!(wp.status, WorkpaperStatus::Draft);
501    }
502
503    #[test]
504    fn test_workpaper_with_results() {
505        let wp = Workpaper::new(
506            Uuid::new_v4(),
507            "D-100",
508            "Accounts Receivable Confirmation",
509            WorkpaperSection::SubstantiveTesting,
510        )
511        .with_scope(
512            WorkpaperScope::default(),
513            1000,
514            50,
515            SamplingMethod::StatisticalRandom,
516        )
517        .with_results(
518            "Confirmed 50 balances with 2 exceptions",
519            2,
520            WorkpaperConclusion::SatisfactoryWithExceptions,
521        );
522
523        assert_eq!(wp.exception_rate, 0.04);
524        assert_eq!(
525            wp.conclusion,
526            WorkpaperConclusion::SatisfactoryWithExceptions
527        );
528    }
529
530    #[test]
531    fn test_review_signoff() {
532        let mut wp = Workpaper::new(
533            Uuid::new_v4(),
534            "A-100",
535            "Planning Memo",
536            WorkpaperSection::Planning,
537        );
538
539        wp.add_first_review(
540            "reviewer1",
541            "John Smith",
542            NaiveDate::from_ymd_opt(2025, 1, 15).unwrap(),
543        );
544        assert_eq!(wp.status, WorkpaperStatus::FirstReviewComplete);
545
546        wp.add_second_review(
547            "manager1",
548            "Jane Doe",
549            NaiveDate::from_ymd_opt(2025, 1, 16).unwrap(),
550        );
551        assert_eq!(wp.status, WorkpaperStatus::Complete);
552        assert!(wp.is_complete());
553    }
554
555    #[test]
556    fn test_assertions() {
557        let txn_assertions = Assertion::transaction_assertions();
558        assert_eq!(txn_assertions.len(), 5);
559        assert!(txn_assertions.contains(&Assertion::Occurrence));
560
561        let bal_assertions = Assertion::balance_assertions();
562        assert_eq!(bal_assertions.len(), 4);
563        assert!(bal_assertions.contains(&Assertion::Existence));
564    }
565}