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 pub created_at: DateTime<Utc>,
110 pub updated_at: DateTime<Utc>,
112}
113
114impl AuditProcedureStep {
115 pub fn new(
117 workpaper_id: Uuid,
118 engagement_id: Uuid,
119 step_number: u32,
120 description: impl Into<String>,
121 procedure_type: StepProcedureType,
122 assertion: Assertion,
123 ) -> Self {
124 let now = Utc::now();
125 let step_ref = format!("STEP-{}-{:02}", &workpaper_id.to_string()[..8], step_number,);
126 Self {
127 step_id: Uuid::new_v4(),
128 step_ref,
129 workpaper_id,
130 engagement_id,
131 step_number,
132 description: description.into(),
133 procedure_type,
134 assertion,
135 planned_date: None,
136 performed_date: None,
137 performed_by: None,
138 performed_by_name: None,
139 status: StepStatus::Planned,
140 result: None,
141 exception_noted: false,
142 exception_description: None,
143 sample_id: None,
144 evidence_ids: Vec::new(),
145 created_at: now,
146 updated_at: now,
147 }
148 }
149
150 pub fn with_sample(mut self, sample_id: Uuid) -> Self {
152 self.sample_id = Some(sample_id);
153 self
154 }
155
156 pub fn with_evidence(mut self, evidence_ids: Vec<Uuid>) -> Self {
158 self.evidence_ids = evidence_ids;
159 self
160 }
161
162 pub fn perform(&mut self, by: String, by_name: String, date: NaiveDate, result: StepResult) {
167 self.performed_by = Some(by);
168 self.performed_by_name = Some(by_name);
169 self.performed_date = Some(date);
170 self.result = Some(result);
171 self.exception_noted = matches!(result, StepResult::Exception);
172 self.status = StepStatus::Complete;
173 self.updated_at = Utc::now();
174 }
175}
176
177#[cfg(test)]
178#[allow(clippy::unwrap_used)]
179mod tests {
180 use super::*;
181
182 fn make_step() -> AuditProcedureStep {
183 AuditProcedureStep::new(
184 Uuid::new_v4(),
185 Uuid::new_v4(),
186 1,
187 "Inspect invoices for proper authorisation",
188 StepProcedureType::Inspection,
189 Assertion::Occurrence,
190 )
191 }
192
193 #[test]
194 fn test_new_step() {
195 let step = make_step();
196 assert_eq!(step.step_number, 1);
197 assert_eq!(step.status, StepStatus::Planned);
198 assert!(step.result.is_none());
199 assert!(!step.exception_noted);
200 assert!(step.step_ref.starts_with("STEP-"));
201 assert!(step.step_ref.ends_with("-01"));
202 }
203
204 #[test]
205 fn test_perform_sets_fields() {
206 let mut step = make_step();
207 let date = NaiveDate::from_ymd_opt(2026, 3, 1).unwrap();
208 step.perform("u123".into(), "Alice Audit".into(), date, StepResult::Pass);
209
210 assert_eq!(step.status, StepStatus::Complete);
211 assert_eq!(step.result, Some(StepResult::Pass));
212 assert_eq!(step.performed_by.as_deref(), Some("u123"));
213 assert_eq!(step.performed_by_name.as_deref(), Some("Alice Audit"));
214 assert_eq!(step.performed_date, Some(date));
215 assert!(!step.exception_noted);
216 }
217
218 #[test]
219 fn test_perform_exception_noted() {
220 let mut step = make_step();
221 let date = NaiveDate::from_ymd_opt(2026, 3, 2).unwrap();
222 step.perform(
223 "u456".into(),
224 "Bob Check".into(),
225 date,
226 StepResult::Exception,
227 );
228
229 assert!(step.exception_noted);
230 assert_eq!(step.result, Some(StepResult::Exception));
231 }
232
233 #[test]
234 fn test_with_sample() {
235 let sample_id = Uuid::new_v4();
236 let step = make_step().with_sample(sample_id);
237 assert_eq!(step.sample_id, Some(sample_id));
238 }
239
240 #[test]
241 fn test_with_evidence() {
242 let ids = vec![Uuid::new_v4(), Uuid::new_v4()];
243 let step = make_step().with_evidence(ids.clone());
244 assert_eq!(step.evidence_ids, ids);
245 }
246
247 #[test]
248 fn test_step_status_serde() {
249 let statuses = [
250 StepStatus::Planned,
251 StepStatus::InProgress,
252 StepStatus::Complete,
253 StepStatus::Deferred,
254 StepStatus::NotApplicable,
255 ];
256 for s in &statuses {
257 let json = serde_json::to_string(s).unwrap();
258 let back: StepStatus = serde_json::from_str(&json).unwrap();
259 assert_eq!(back, *s);
260 }
261 }
262
263 #[test]
264 fn test_step_result_serde() {
265 let results = [
266 StepResult::Pass,
267 StepResult::Fail,
268 StepResult::Exception,
269 StepResult::Inconclusive,
270 ];
271 for r in &results {
272 let json = serde_json::to_string(r).unwrap();
273 let back: StepResult = serde_json::from_str(&json).unwrap();
274 assert_eq!(back, *r);
275 }
276 }
277
278 #[test]
279 fn test_procedure_type_serde() {
280 let types = [
281 StepProcedureType::Inspection,
282 StepProcedureType::Observation,
283 StepProcedureType::Inquiry,
284 StepProcedureType::Confirmation,
285 StepProcedureType::Recalculation,
286 StepProcedureType::Reperformance,
287 StepProcedureType::AnalyticalProcedure,
288 StepProcedureType::Vouching,
289 StepProcedureType::Scanning,
290 ];
291 for t in &types {
292 let json = serde_json::to_string(t).unwrap();
293 let back: StepProcedureType = serde_json::from_str(&json).unwrap();
294 assert_eq!(back, *t);
295 }
296 }
297}