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)]
180#[allow(clippy::unwrap_used)]
181mod tests {
182 use super::*;
183
184 fn make_step() -> AuditProcedureStep {
185 AuditProcedureStep::new(
186 Uuid::new_v4(),
187 Uuid::new_v4(),
188 1,
189 "Inspect invoices for proper authorisation",
190 StepProcedureType::Inspection,
191 Assertion::Occurrence,
192 )
193 }
194
195 #[test]
196 fn test_new_step() {
197 let step = make_step();
198 assert_eq!(step.step_number, 1);
199 assert_eq!(step.status, StepStatus::Planned);
200 assert!(step.result.is_none());
201 assert!(!step.exception_noted);
202 assert!(step.step_ref.starts_with("STEP-"));
203 assert!(step.step_ref.ends_with("-01"));
204 }
205
206 #[test]
207 fn test_perform_sets_fields() {
208 let mut step = make_step();
209 let date = NaiveDate::from_ymd_opt(2026, 3, 1).unwrap();
210 step.perform("u123".into(), "Alice Audit".into(), date, StepResult::Pass);
211
212 assert_eq!(step.status, StepStatus::Complete);
213 assert_eq!(step.result, Some(StepResult::Pass));
214 assert_eq!(step.performed_by.as_deref(), Some("u123"));
215 assert_eq!(step.performed_by_name.as_deref(), Some("Alice Audit"));
216 assert_eq!(step.performed_date, Some(date));
217 assert!(!step.exception_noted);
218 }
219
220 #[test]
221 fn test_perform_exception_noted() {
222 let mut step = make_step();
223 let date = NaiveDate::from_ymd_opt(2026, 3, 2).unwrap();
224 step.perform(
225 "u456".into(),
226 "Bob Check".into(),
227 date,
228 StepResult::Exception,
229 );
230
231 assert!(step.exception_noted);
232 assert_eq!(step.result, Some(StepResult::Exception));
233 }
234
235 #[test]
236 fn test_with_sample() {
237 let sample_id = Uuid::new_v4();
238 let step = make_step().with_sample(sample_id);
239 assert_eq!(step.sample_id, Some(sample_id));
240 }
241
242 #[test]
243 fn test_with_evidence() {
244 let ids = vec![Uuid::new_v4(), Uuid::new_v4()];
245 let step = make_step().with_evidence(ids.clone());
246 assert_eq!(step.evidence_ids, ids);
247 }
248
249 #[test]
250 fn test_step_status_serde() {
251 let statuses = [
252 StepStatus::Planned,
253 StepStatus::InProgress,
254 StepStatus::Complete,
255 StepStatus::Deferred,
256 StepStatus::NotApplicable,
257 ];
258 for s in &statuses {
259 let json = serde_json::to_string(s).unwrap();
260 let back: StepStatus = serde_json::from_str(&json).unwrap();
261 assert_eq!(back, *s);
262 }
263 }
264
265 #[test]
266 fn test_step_result_serde() {
267 let results = [
268 StepResult::Pass,
269 StepResult::Fail,
270 StepResult::Exception,
271 StepResult::Inconclusive,
272 ];
273 for r in &results {
274 let json = serde_json::to_string(r).unwrap();
275 let back: StepResult = serde_json::from_str(&json).unwrap();
276 assert_eq!(back, *r);
277 }
278 }
279
280 #[test]
281 fn test_procedure_type_serde() {
282 let types = [
283 StepProcedureType::Inspection,
284 StepProcedureType::Observation,
285 StepProcedureType::Inquiry,
286 StepProcedureType::Confirmation,
287 StepProcedureType::Recalculation,
288 StepProcedureType::Reperformance,
289 StepProcedureType::AnalyticalProcedure,
290 StepProcedureType::Vouching,
291 StepProcedureType::Scanning,
292 ];
293 for t in &types {
294 let json = serde_json::to_string(t).unwrap();
295 let back: StepProcedureType = serde_json::from_str(&json).unwrap();
296 assert_eq!(back, *t);
297 }
298 }
299}