use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::engagement::RiskLevel;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workpaper {
pub workpaper_id: Uuid,
pub workpaper_ref: String,
pub engagement_id: Uuid,
pub title: String,
pub section: WorkpaperSection,
pub objective: String,
pub assertions_tested: Vec<Assertion>,
pub procedure_performed: String,
pub procedure_type: ProcedureType,
pub scope: WorkpaperScope,
pub population_size: u64,
pub sample_size: u32,
pub sampling_method: SamplingMethod,
pub results_summary: String,
pub exceptions_found: u32,
pub exception_rate: f64,
pub conclusion: WorkpaperConclusion,
pub risk_level_addressed: RiskLevel,
pub evidence_refs: Vec<Uuid>,
pub cross_references: Vec<String>,
pub account_ids: Vec<String>,
pub preparer_id: String,
pub preparer_name: String,
pub preparer_date: NaiveDate,
pub reviewer_id: Option<String>,
pub reviewer_name: Option<String>,
pub reviewer_date: Option<NaiveDate>,
pub second_reviewer_id: Option<String>,
pub second_reviewer_name: Option<String>,
pub second_reviewer_date: Option<NaiveDate>,
pub status: WorkpaperStatus,
pub version: u32,
pub review_notes: Vec<ReviewNote>,
#[serde(with = "crate::serde_timestamp::utc")]
pub created_at: DateTime<Utc>,
#[serde(with = "crate::serde_timestamp::utc")]
pub updated_at: DateTime<Utc>,
}
impl Workpaper {
pub fn new(
engagement_id: Uuid,
workpaper_ref: &str,
title: &str,
section: WorkpaperSection,
) -> Self {
let now = Utc::now();
Self {
workpaper_id: Uuid::new_v4(),
workpaper_ref: workpaper_ref.into(),
engagement_id,
title: title.into(),
section,
objective: String::new(),
assertions_tested: Vec::new(),
procedure_performed: String::new(),
procedure_type: ProcedureType::InquiryObservation,
scope: WorkpaperScope::default(),
population_size: 0,
sample_size: 0,
sampling_method: SamplingMethod::Judgmental,
results_summary: String::new(),
exceptions_found: 0,
exception_rate: 0.0,
conclusion: WorkpaperConclusion::Satisfactory,
risk_level_addressed: RiskLevel::Medium,
evidence_refs: Vec::new(),
cross_references: Vec::new(),
account_ids: Vec::new(),
preparer_id: String::new(),
preparer_name: String::new(),
preparer_date: now.date_naive(),
reviewer_id: None,
reviewer_name: None,
reviewer_date: None,
second_reviewer_id: None,
second_reviewer_name: None,
second_reviewer_date: None,
status: WorkpaperStatus::Draft,
version: 1,
review_notes: Vec::new(),
created_at: now,
updated_at: now,
}
}
pub fn with_objective(mut self, objective: &str, assertions: Vec<Assertion>) -> Self {
self.objective = objective.into();
self.assertions_tested = assertions;
self
}
pub fn with_procedure(mut self, procedure: &str, procedure_type: ProcedureType) -> Self {
self.procedure_performed = procedure.into();
self.procedure_type = procedure_type;
self
}
pub fn with_scope(
mut self,
scope: WorkpaperScope,
population: u64,
sample: u32,
method: SamplingMethod,
) -> Self {
self.scope = scope;
self.population_size = population;
self.sample_size = sample;
self.sampling_method = method;
self
}
pub fn with_results(
mut self,
summary: &str,
exceptions: u32,
conclusion: WorkpaperConclusion,
) -> Self {
self.results_summary = summary.into();
self.exceptions_found = exceptions;
self.exception_rate = if self.sample_size > 0 {
exceptions as f64 / self.sample_size as f64
} else {
0.0
};
self.conclusion = conclusion;
self
}
pub fn with_preparer(mut self, id: &str, name: &str, date: NaiveDate) -> Self {
self.preparer_id = id.into();
self.preparer_name = name.into();
self.preparer_date = date;
self
}
pub fn add_first_review(&mut self, id: &str, name: &str, date: NaiveDate) {
self.reviewer_id = Some(id.into());
self.reviewer_name = Some(name.into());
self.reviewer_date = Some(date);
self.status = WorkpaperStatus::FirstReviewComplete;
self.updated_at = Utc::now();
}
pub fn add_second_review(&mut self, id: &str, name: &str, date: NaiveDate) {
self.second_reviewer_id = Some(id.into());
self.second_reviewer_name = Some(name.into());
self.second_reviewer_date = Some(date);
self.status = WorkpaperStatus::Complete;
self.updated_at = Utc::now();
}
pub fn add_review_note(&mut self, reviewer: &str, note: &str) {
self.review_notes.push(ReviewNote {
note_id: Uuid::new_v4(),
reviewer_id: reviewer.into(),
note: note.into(),
status: ReviewNoteStatus::Open,
created_at: Utc::now(),
resolved_at: None,
});
self.updated_at = Utc::now();
}
pub fn is_complete(&self) -> bool {
matches!(self.status, WorkpaperStatus::Complete)
}
pub fn all_notes_resolved(&self) -> bool {
self.review_notes.iter().all(|n| {
matches!(
n.status,
ReviewNoteStatus::Resolved | ReviewNoteStatus::NotApplicable
)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum WorkpaperSection {
#[default]
Planning,
RiskAssessment,
ControlTesting,
SubstantiveTesting,
Completion,
Reporting,
PermanentFile,
}
impl WorkpaperSection {
pub fn reference_prefix(&self) -> &'static str {
match self {
Self::Planning => "A",
Self::RiskAssessment => "B",
Self::ControlTesting => "C",
Self::SubstantiveTesting => "D",
Self::Completion => "E",
Self::Reporting => "F",
Self::PermanentFile => "P",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Assertion {
Occurrence,
Completeness,
Accuracy,
Cutoff,
Classification,
Existence,
RightsAndObligations,
ValuationAndAllocation,
PresentationAndDisclosure,
}
impl Assertion {
pub fn transaction_assertions() -> Vec<Self> {
vec![
Self::Occurrence,
Self::Completeness,
Self::Accuracy,
Self::Cutoff,
Self::Classification,
]
}
pub fn balance_assertions() -> Vec<Self> {
vec![
Self::Existence,
Self::Completeness,
Self::RightsAndObligations,
Self::ValuationAndAllocation,
]
}
pub fn description(&self) -> &'static str {
match self {
Self::Occurrence => "Transactions and events have occurred and pertain to the entity",
Self::Completeness => {
"All transactions and events that should have been recorded have been recorded"
}
Self::Accuracy => "Amounts and other data have been recorded appropriately",
Self::Cutoff => "Transactions and events have been recorded in the correct period",
Self::Classification => {
"Transactions and events have been recorded in the proper accounts"
}
Self::Existence => "Assets, liabilities, and equity interests exist",
Self::RightsAndObligations => {
"The entity holds rights to assets and liabilities are obligations of the entity"
}
Self::ValuationAndAllocation => {
"Assets, liabilities, and equity interests are included at appropriate amounts"
}
Self::PresentationAndDisclosure => {
"Financial information is appropriately presented and described"
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ProcedureType {
#[default]
InquiryObservation,
Inspection,
Confirmation,
Recalculation,
Reperformance,
AnalyticalProcedures,
TestOfControls,
SubstantiveTest,
Combined,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WorkpaperScope {
pub coverage_percentage: f64,
pub period_start: Option<NaiveDate>,
pub period_end: Option<NaiveDate>,
pub limitations: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SamplingMethod {
StatisticalRandom,
MonetaryUnit,
#[default]
Judgmental,
Haphazard,
Block,
AllItems,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum WorkpaperConclusion {
#[default]
Satisfactory,
SatisfactoryWithExceptions,
Unsatisfactory,
UnableToConclude,
AdditionalProceduresRequired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum WorkpaperStatus {
#[default]
Draft,
PendingReview,
FirstReviewComplete,
PendingSecondReview,
Complete,
Superseded,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewNote {
pub note_id: Uuid,
pub reviewer_id: String,
pub note: String,
pub status: ReviewNoteStatus,
#[serde(with = "crate::serde_timestamp::utc")]
pub created_at: DateTime<Utc>,
#[serde(default, with = "crate::serde_timestamp::utc::option")]
pub resolved_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReviewNoteStatus {
#[default]
Open,
InProgress,
Resolved,
NotApplicable,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_workpaper_creation() {
let wp = Workpaper::new(
Uuid::new_v4(),
"C-100",
"Revenue Recognition Testing",
WorkpaperSection::SubstantiveTesting,
);
assert_eq!(wp.workpaper_ref, "C-100");
assert_eq!(wp.section, WorkpaperSection::SubstantiveTesting);
assert_eq!(wp.status, WorkpaperStatus::Draft);
}
#[test]
fn test_workpaper_with_results() {
let wp = Workpaper::new(
Uuid::new_v4(),
"D-100",
"Accounts Receivable Confirmation",
WorkpaperSection::SubstantiveTesting,
)
.with_scope(
WorkpaperScope::default(),
1000,
50,
SamplingMethod::StatisticalRandom,
)
.with_results(
"Confirmed 50 balances with 2 exceptions",
2,
WorkpaperConclusion::SatisfactoryWithExceptions,
);
assert_eq!(wp.exception_rate, 0.04);
assert_eq!(
wp.conclusion,
WorkpaperConclusion::SatisfactoryWithExceptions
);
}
#[test]
fn test_review_signoff() {
let mut wp = Workpaper::new(
Uuid::new_v4(),
"A-100",
"Planning Memo",
WorkpaperSection::Planning,
);
wp.add_first_review(
"reviewer1",
"John Smith",
NaiveDate::from_ymd_opt(2025, 1, 15).unwrap(),
);
assert_eq!(wp.status, WorkpaperStatus::FirstReviewComplete);
wp.add_second_review(
"manager1",
"Jane Doe",
NaiveDate::from_ymd_opt(2025, 1, 16).unwrap(),
);
assert_eq!(wp.status, WorkpaperStatus::Complete);
assert!(wp.is_complete());
}
#[test]
fn test_assertions() {
let txn_assertions = Assertion::transaction_assertions();
assert_eq!(txn_assertions.len(), 5);
assert!(txn_assertions.contains(&Assertion::Occurrence));
let bal_assertions = Assertion::balance_assertions();
assert_eq!(bal_assertions.len(), 4);
assert!(bal_assertions.contains(&Assertion::Existence));
}
}