1use chrono::{DateTime, NaiveDate, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::engagement::RiskLevel;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Workpaper {
15 pub workpaper_id: Uuid,
17 pub workpaper_ref: String,
19 pub engagement_id: Uuid,
21 pub title: String,
23 pub section: WorkpaperSection,
25 pub objective: String,
27 pub assertions_tested: Vec<Assertion>,
29 pub procedure_performed: String,
31 pub procedure_type: ProcedureType,
33
34 pub scope: WorkpaperScope,
37 pub population_size: u64,
39 pub sample_size: u32,
41 pub sampling_method: SamplingMethod,
43
44 pub results_summary: String,
47 pub exceptions_found: u32,
49 pub exception_rate: f64,
51 pub conclusion: WorkpaperConclusion,
53 pub risk_level_addressed: RiskLevel,
55
56 pub evidence_refs: Vec<Uuid>,
59 pub cross_references: Vec<String>,
61 pub account_ids: Vec<String>,
63
64 pub preparer_id: String,
67 pub preparer_name: String,
69 pub preparer_date: NaiveDate,
71 pub reviewer_id: Option<String>,
73 pub reviewer_name: Option<String>,
75 pub reviewer_date: Option<NaiveDate>,
77 pub second_reviewer_id: Option<String>,
79 pub second_reviewer_name: Option<String>,
81 pub second_reviewer_date: Option<NaiveDate>,
83
84 pub status: WorkpaperStatus,
87 pub version: u32,
89 pub review_notes: Vec<ReviewNote>,
91
92 #[serde(with = "crate::serde_timestamp::utc")]
94 pub created_at: DateTime<Utc>,
95 #[serde(with = "crate::serde_timestamp::utc")]
96 pub updated_at: DateTime<Utc>,
97}
98
99impl Workpaper {
100 pub fn new(
102 engagement_id: Uuid,
103 workpaper_ref: &str,
104 title: &str,
105 section: WorkpaperSection,
106 ) -> Self {
107 let now = Utc::now();
108 Self {
109 workpaper_id: Uuid::new_v4(),
110 workpaper_ref: workpaper_ref.into(),
111 engagement_id,
112 title: title.into(),
113 section,
114 objective: String::new(),
115 assertions_tested: Vec::new(),
116 procedure_performed: String::new(),
117 procedure_type: ProcedureType::InquiryObservation,
118 scope: WorkpaperScope::default(),
119 population_size: 0,
120 sample_size: 0,
121 sampling_method: SamplingMethod::Judgmental,
122 results_summary: String::new(),
123 exceptions_found: 0,
124 exception_rate: 0.0,
125 conclusion: WorkpaperConclusion::Satisfactory,
126 risk_level_addressed: RiskLevel::Medium,
127 evidence_refs: Vec::new(),
128 cross_references: Vec::new(),
129 account_ids: Vec::new(),
130 preparer_id: String::new(),
131 preparer_name: String::new(),
132 preparer_date: now.date_naive(),
133 reviewer_id: None,
134 reviewer_name: None,
135 reviewer_date: None,
136 second_reviewer_id: None,
137 second_reviewer_name: None,
138 second_reviewer_date: None,
139 status: WorkpaperStatus::Draft,
140 version: 1,
141 review_notes: Vec::new(),
142 created_at: now,
143 updated_at: now,
144 }
145 }
146
147 pub fn with_objective(mut self, objective: &str, assertions: Vec<Assertion>) -> Self {
149 self.objective = objective.into();
150 self.assertions_tested = assertions;
151 self
152 }
153
154 pub fn with_procedure(mut self, procedure: &str, procedure_type: ProcedureType) -> Self {
156 self.procedure_performed = procedure.into();
157 self.procedure_type = procedure_type;
158 self
159 }
160
161 pub fn with_scope(
163 mut self,
164 scope: WorkpaperScope,
165 population: u64,
166 sample: u32,
167 method: SamplingMethod,
168 ) -> Self {
169 self.scope = scope;
170 self.population_size = population;
171 self.sample_size = sample;
172 self.sampling_method = method;
173 self
174 }
175
176 pub fn with_results(
178 mut self,
179 summary: &str,
180 exceptions: u32,
181 conclusion: WorkpaperConclusion,
182 ) -> Self {
183 self.results_summary = summary.into();
184 self.exceptions_found = exceptions;
185 self.exception_rate = if self.sample_size > 0 {
186 exceptions as f64 / self.sample_size as f64
187 } else {
188 0.0
189 };
190 self.conclusion = conclusion;
191 self
192 }
193
194 pub fn with_preparer(mut self, id: &str, name: &str, date: NaiveDate) -> Self {
196 self.preparer_id = id.into();
197 self.preparer_name = name.into();
198 self.preparer_date = date;
199 self
200 }
201
202 pub fn add_first_review(&mut self, id: &str, name: &str, date: NaiveDate) {
204 self.reviewer_id = Some(id.into());
205 self.reviewer_name = Some(name.into());
206 self.reviewer_date = Some(date);
207 self.status = WorkpaperStatus::FirstReviewComplete;
208 self.updated_at = Utc::now();
209 }
210
211 pub fn add_second_review(&mut self, id: &str, name: &str, date: NaiveDate) {
213 self.second_reviewer_id = Some(id.into());
214 self.second_reviewer_name = Some(name.into());
215 self.second_reviewer_date = Some(date);
216 self.status = WorkpaperStatus::Complete;
217 self.updated_at = Utc::now();
218 }
219
220 pub fn add_review_note(&mut self, reviewer: &str, note: &str) {
222 self.review_notes.push(ReviewNote {
223 note_id: Uuid::new_v4(),
224 reviewer_id: reviewer.into(),
225 note: note.into(),
226 status: ReviewNoteStatus::Open,
227 created_at: Utc::now(),
228 resolved_at: None,
229 });
230 self.updated_at = Utc::now();
231 }
232
233 pub fn is_complete(&self) -> bool {
235 matches!(self.status, WorkpaperStatus::Complete)
236 }
237
238 pub fn all_notes_resolved(&self) -> bool {
240 self.review_notes.iter().all(|n| {
241 matches!(
242 n.status,
243 ReviewNoteStatus::Resolved | ReviewNoteStatus::NotApplicable
244 )
245 })
246 }
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
251#[serde(rename_all = "snake_case")]
252pub enum WorkpaperSection {
253 #[default]
255 Planning,
256 RiskAssessment,
258 ControlTesting,
260 SubstantiveTesting,
262 Completion,
264 Reporting,
266 PermanentFile,
268}
269
270impl WorkpaperSection {
271 pub fn reference_prefix(&self) -> &'static str {
273 match self {
274 Self::Planning => "A",
275 Self::RiskAssessment => "B",
276 Self::ControlTesting => "C",
277 Self::SubstantiveTesting => "D",
278 Self::Completion => "E",
279 Self::Reporting => "F",
280 Self::PermanentFile => "P",
281 }
282 }
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
287#[serde(rename_all = "snake_case")]
288pub enum Assertion {
289 Occurrence,
292 Completeness,
294 Accuracy,
296 Cutoff,
298 Classification,
300
301 Existence,
304 RightsAndObligations,
306 ValuationAndAllocation,
308
309 PresentationAndDisclosure,
312}
313
314impl Assertion {
315 pub fn transaction_assertions() -> Vec<Self> {
317 vec![
318 Self::Occurrence,
319 Self::Completeness,
320 Self::Accuracy,
321 Self::Cutoff,
322 Self::Classification,
323 ]
324 }
325
326 pub fn balance_assertions() -> Vec<Self> {
328 vec![
329 Self::Existence,
330 Self::Completeness,
331 Self::RightsAndObligations,
332 Self::ValuationAndAllocation,
333 ]
334 }
335
336 pub fn description(&self) -> &'static str {
338 match self {
339 Self::Occurrence => "Transactions and events have occurred and pertain to the entity",
340 Self::Completeness => {
341 "All transactions and events that should have been recorded have been recorded"
342 }
343 Self::Accuracy => "Amounts and other data have been recorded appropriately",
344 Self::Cutoff => "Transactions and events have been recorded in the correct period",
345 Self::Classification => {
346 "Transactions and events have been recorded in the proper accounts"
347 }
348 Self::Existence => "Assets, liabilities, and equity interests exist",
349 Self::RightsAndObligations => {
350 "The entity holds rights to assets and liabilities are obligations of the entity"
351 }
352 Self::ValuationAndAllocation => {
353 "Assets, liabilities, and equity interests are included at appropriate amounts"
354 }
355 Self::PresentationAndDisclosure => {
356 "Financial information is appropriately presented and described"
357 }
358 }
359 }
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
364#[serde(rename_all = "snake_case")]
365pub enum ProcedureType {
366 #[default]
368 InquiryObservation,
369 Inspection,
371 Confirmation,
373 Recalculation,
375 Reperformance,
377 AnalyticalProcedures,
379 TestOfControls,
381 SubstantiveTest,
383 Combined,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize, Default)]
389pub struct WorkpaperScope {
390 pub coverage_percentage: f64,
392 pub period_start: Option<NaiveDate>,
394 pub period_end: Option<NaiveDate>,
396 pub limitations: Vec<String>,
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
402#[serde(rename_all = "snake_case")]
403pub enum SamplingMethod {
404 StatisticalRandom,
406 MonetaryUnit,
408 #[default]
410 Judgmental,
411 Haphazard,
413 Block,
415 AllItems,
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
421#[serde(rename_all = "snake_case")]
422pub enum WorkpaperConclusion {
423 #[default]
425 Satisfactory,
426 SatisfactoryWithExceptions,
428 Unsatisfactory,
430 UnableToConclude,
432 AdditionalProceduresRequired,
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
438#[serde(rename_all = "snake_case")]
439pub enum WorkpaperStatus {
440 #[default]
442 Draft,
443 PendingReview,
445 FirstReviewComplete,
447 PendingSecondReview,
449 Complete,
451 Superseded,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct ReviewNote {
458 pub note_id: Uuid,
460 pub reviewer_id: String,
462 pub note: String,
464 pub status: ReviewNoteStatus,
466 #[serde(with = "crate::serde_timestamp::utc")]
468 pub created_at: DateTime<Utc>,
469 #[serde(default, with = "crate::serde_timestamp::utc::option")]
471 pub resolved_at: Option<DateTime<Utc>>,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
476#[serde(rename_all = "snake_case")]
477pub enum ReviewNoteStatus {
478 #[default]
480 Open,
481 InProgress,
483 Resolved,
485 NotApplicable,
487}
488
489#[cfg(test)]
490#[allow(clippy::unwrap_used)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn test_workpaper_creation() {
496 let wp = Workpaper::new(
497 Uuid::new_v4(),
498 "C-100",
499 "Revenue Recognition Testing",
500 WorkpaperSection::SubstantiveTesting,
501 );
502
503 assert_eq!(wp.workpaper_ref, "C-100");
504 assert_eq!(wp.section, WorkpaperSection::SubstantiveTesting);
505 assert_eq!(wp.status, WorkpaperStatus::Draft);
506 }
507
508 #[test]
509 fn test_workpaper_with_results() {
510 let wp = Workpaper::new(
511 Uuid::new_v4(),
512 "D-100",
513 "Accounts Receivable Confirmation",
514 WorkpaperSection::SubstantiveTesting,
515 )
516 .with_scope(
517 WorkpaperScope::default(),
518 1000,
519 50,
520 SamplingMethod::StatisticalRandom,
521 )
522 .with_results(
523 "Confirmed 50 balances with 2 exceptions",
524 2,
525 WorkpaperConclusion::SatisfactoryWithExceptions,
526 );
527
528 assert_eq!(wp.exception_rate, 0.04);
529 assert_eq!(
530 wp.conclusion,
531 WorkpaperConclusion::SatisfactoryWithExceptions
532 );
533 }
534
535 #[test]
536 fn test_review_signoff() {
537 let mut wp = Workpaper::new(
538 Uuid::new_v4(),
539 "A-100",
540 "Planning Memo",
541 WorkpaperSection::Planning,
542 );
543
544 wp.add_first_review(
545 "reviewer1",
546 "John Smith",
547 NaiveDate::from_ymd_opt(2025, 1, 15).unwrap(),
548 );
549 assert_eq!(wp.status, WorkpaperStatus::FirstReviewComplete);
550
551 wp.add_second_review(
552 "manager1",
553 "Jane Doe",
554 NaiveDate::from_ymd_opt(2025, 1, 16).unwrap(),
555 );
556 assert_eq!(wp.status, WorkpaperStatus::Complete);
557 assert!(wp.is_complete());
558 }
559
560 #[test]
561 fn test_assertions() {
562 let txn_assertions = Assertion::transaction_assertions();
563 assert_eq!(txn_assertions.len(), 5);
564 assert!(txn_assertions.contains(&Assertion::Occurrence));
565
566 let bal_assertions = Assertion::balance_assertions();
567 assert_eq!(bal_assertions.len(), 4);
568 assert!(bal_assertions.contains(&Assertion::Existence));
569 }
570}