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 pub created_at: DateTime<Utc>,
94 pub updated_at: DateTime<Utc>,
95}
96
97impl Workpaper {
98 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 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 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 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 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 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 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 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 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 pub fn is_complete(&self) -> bool {
233 matches!(self.status, WorkpaperStatus::Complete)
234 }
235
236 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
249#[serde(rename_all = "snake_case")]
250pub enum WorkpaperSection {
251 #[default]
253 Planning,
254 RiskAssessment,
256 ControlTesting,
258 SubstantiveTesting,
260 Completion,
262 Reporting,
264 PermanentFile,
266}
267
268impl WorkpaperSection {
269 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum Assertion {
287 Occurrence,
290 Completeness,
292 Accuracy,
294 Cutoff,
296 Classification,
298
299 Existence,
302 RightsAndObligations,
304 ValuationAndAllocation,
306
307 PresentationAndDisclosure,
310}
311
312impl Assertion {
313 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 pub fn balance_assertions() -> Vec<Self> {
326 vec![
327 Self::Existence,
328 Self::Completeness,
329 Self::RightsAndObligations,
330 Self::ValuationAndAllocation,
331 ]
332 }
333
334 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
362#[serde(rename_all = "snake_case")]
363pub enum ProcedureType {
364 #[default]
366 InquiryObservation,
367 Inspection,
369 Confirmation,
371 Recalculation,
373 Reperformance,
375 AnalyticalProcedures,
377 TestOfControls,
379 SubstantiveTest,
381 Combined,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize, Default)]
387pub struct WorkpaperScope {
388 pub coverage_percentage: f64,
390 pub period_start: Option<NaiveDate>,
392 pub period_end: Option<NaiveDate>,
394 pub limitations: Vec<String>,
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
400#[serde(rename_all = "snake_case")]
401pub enum SamplingMethod {
402 StatisticalRandom,
404 MonetaryUnit,
406 #[default]
408 Judgmental,
409 Haphazard,
411 Block,
413 AllItems,
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
419#[serde(rename_all = "snake_case")]
420pub enum WorkpaperConclusion {
421 #[default]
423 Satisfactory,
424 SatisfactoryWithExceptions,
426 Unsatisfactory,
428 UnableToConclude,
430 AdditionalProceduresRequired,
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
436#[serde(rename_all = "snake_case")]
437pub enum WorkpaperStatus {
438 #[default]
440 Draft,
441 PendingReview,
443 FirstReviewComplete,
445 PendingSecondReview,
447 Complete,
449 Superseded,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct ReviewNote {
456 pub note_id: Uuid,
458 pub reviewer_id: String,
460 pub note: String,
462 pub status: ReviewNoteStatus,
464 pub created_at: DateTime<Utc>,
466 pub resolved_at: Option<DateTime<Utc>>,
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
472#[serde(rename_all = "snake_case")]
473pub enum ReviewNoteStatus {
474 #[default]
476 Open,
477 InProgress,
479 Resolved,
481 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}