datasynth_core/models/audit/
procedure_step.rs1use chrono::{DateTime, NaiveDate, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::Assertion;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
14#[serde(rename_all = "snake_case")]
15pub enum StepProcedureType {
16 #[default]
18 Inspection,
19 Observation,
21 Inquiry,
23 Confirmation,
25 Recalculation,
27 Reperformance,
29 AnalyticalProcedure,
31 Vouching,
33 Scanning,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
39#[serde(rename_all = "snake_case")]
40pub enum StepStatus {
41 #[default]
43 Planned,
44 InProgress,
46 Complete,
48 Deferred,
50 NotApplicable,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
56#[serde(rename_all = "snake_case")]
57pub enum StepResult {
58 #[default]
60 Pass,
61 Fail,
63 Exception,
65 Inconclusive,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct AuditProcedureStep {
72 pub step_id: Uuid,
74 pub step_ref: String,
76 pub workpaper_id: Uuid,
78 pub engagement_id: Uuid,
80 pub step_number: u32,
82 pub description: String,
84 pub procedure_type: StepProcedureType,
86 pub assertion: Assertion,
88 pub planned_date: Option<NaiveDate>,
90 pub performed_date: Option<NaiveDate>,
92 pub performed_by: Option<String>,
94 pub performed_by_name: Option<String>,
96 pub status: StepStatus,
98 pub result: Option<StepResult>,
100 pub exception_noted: bool,
102 pub exception_description: Option<String>,
104 pub sample_id: Option<Uuid>,
106 pub evidence_ids: Vec<Uuid>,
108 #[serde(with = "crate::serde_timestamp::utc")]
110 pub created_at: DateTime<Utc>,
111 #[serde(with = "crate::serde_timestamp::utc")]
113 pub updated_at: DateTime<Utc>,
114}
115
116impl AuditProcedureStep {
117 pub fn new(
119 workpaper_id: Uuid,
120 engagement_id: Uuid,
121 step_number: u32,
122 description: impl Into<String>,
123 procedure_type: StepProcedureType,
124 assertion: Assertion,
125 ) -> Self {
126 let now = Utc::now();
127 let step_ref = format!("STEP-{}-{:02}", &workpaper_id.to_string()[..8], step_number,);
128 Self {
129 step_id: Uuid::new_v4(),
130 step_ref,
131 workpaper_id,
132 engagement_id,
133 step_number,
134 description: description.into(),
135 procedure_type,
136 assertion,
137 planned_date: None,
138 performed_date: None,
139 performed_by: None,
140 performed_by_name: None,
141 status: StepStatus::Planned,
142 result: None,
143 exception_noted: false,
144 exception_description: None,
145 sample_id: None,
146 evidence_ids: Vec::new(),
147 created_at: now,
148 updated_at: now,
149 }
150 }
151
152 pub fn with_sample(mut self, sample_id: Uuid) -> Self {
154 self.sample_id = Some(sample_id);
155 self
156 }
157
158 pub fn with_evidence(mut self, evidence_ids: Vec<Uuid>) -> Self {
160 self.evidence_ids = evidence_ids;
161 self
162 }
163
164 pub fn perform(&mut self, by: String, by_name: String, date: NaiveDate, result: StepResult) {
169 self.performed_by = Some(by);
170 self.performed_by_name = Some(by_name);
171 self.performed_date = Some(date);
172 self.result = Some(result);
173 self.exception_noted = matches!(result, StepResult::Exception);
174 self.status = StepStatus::Complete;
175 self.updated_at = Utc::now();
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn make_step() -> AuditProcedureStep {
184 AuditProcedureStep::new(
185 Uuid::new_v4(),
186 Uuid::new_v4(),
187 1,
188 "Inspect invoices for proper authorisation",
189 StepProcedureType::Inspection,
190 Assertion::Occurrence,
191 )
192 }
193
194 #[test]
195 fn test_new_step() {
196 let step = make_step();
197 assert_eq!(step.step_number, 1);
198 assert_eq!(step.status, StepStatus::Planned);
199 assert!(step.result.is_none());
200 assert!(!step.exception_noted);
201 assert!(step.step_ref.starts_with("STEP-"));
202 assert!(step.step_ref.ends_with("-01"));
203 }
204
205 #[test]
206 fn test_perform_sets_fields() {
207 let mut step = make_step();
208 let date = NaiveDate::from_ymd_opt(2026, 3, 1).unwrap();
209 step.perform("u123".into(), "Alice Audit".into(), date, StepResult::Pass);
210
211 assert_eq!(step.status, StepStatus::Complete);
212 assert_eq!(step.result, Some(StepResult::Pass));
213 assert_eq!(step.performed_by.as_deref(), Some("u123"));
214 assert_eq!(step.performed_by_name.as_deref(), Some("Alice Audit"));
215 assert_eq!(step.performed_date, Some(date));
216 assert!(!step.exception_noted);
217 }
218
219 #[test]
220 fn test_perform_exception_noted() {
221 let mut step = make_step();
222 let date = NaiveDate::from_ymd_opt(2026, 3, 2).unwrap();
223 step.perform(
224 "u456".into(),
225 "Bob Check".into(),
226 date,
227 StepResult::Exception,
228 );
229
230 assert!(step.exception_noted);
231 assert_eq!(step.result, Some(StepResult::Exception));
232 }
233
234 #[test]
235 fn test_with_sample() {
236 let sample_id = Uuid::new_v4();
237 let step = make_step().with_sample(sample_id);
238 assert_eq!(step.sample_id, Some(sample_id));
239 }
240
241 #[test]
242 fn test_with_evidence() {
243 let ids = vec![Uuid::new_v4(), Uuid::new_v4()];
244 let step = make_step().with_evidence(ids.clone());
245 assert_eq!(step.evidence_ids, ids);
246 }
247
248 #[test]
249 fn test_step_status_serde() {
250 let statuses = [
251 StepStatus::Planned,
252 StepStatus::InProgress,
253 StepStatus::Complete,
254 StepStatus::Deferred,
255 StepStatus::NotApplicable,
256 ];
257 for s in &statuses {
258 let json = serde_json::to_string(s).unwrap();
259 let back: StepStatus = serde_json::from_str(&json).unwrap();
260 assert_eq!(back, *s);
261 }
262 }
263
264 #[test]
265 fn test_step_result_serde() {
266 let results = [
267 StepResult::Pass,
268 StepResult::Fail,
269 StepResult::Exception,
270 StepResult::Inconclusive,
271 ];
272 for r in &results {
273 let json = serde_json::to_string(r).unwrap();
274 let back: StepResult = serde_json::from_str(&json).unwrap();
275 assert_eq!(back, *r);
276 }
277 }
278
279 #[test]
280 fn test_procedure_type_serde() {
281 let types = [
282 StepProcedureType::Inspection,
283 StepProcedureType::Observation,
284 StepProcedureType::Inquiry,
285 StepProcedureType::Confirmation,
286 StepProcedureType::Recalculation,
287 StepProcedureType::Reperformance,
288 StepProcedureType::AnalyticalProcedure,
289 StepProcedureType::Vouching,
290 StepProcedureType::Scanning,
291 ];
292 for t in &types {
293 let json = serde_json::to_string(t).unwrap();
294 let back: StepProcedureType = serde_json::from_str(&json).unwrap();
295 assert_eq!(back, *t);
296 }
297 }
298}