1use chrono::{DateTime, NaiveDate, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum ReportingLine {
15 #[default]
17 AuditCommittee,
18 Board,
20 CFO,
22 CEO,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
28#[serde(rename_all = "snake_case")]
29pub enum IaAssessment {
30 FullyEffective,
32 #[default]
34 LargelyEffective,
35 PartiallyEffective,
37 Ineffective,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
43#[serde(rename_all = "snake_case")]
44pub enum ObjectivityRating {
45 #[default]
47 High,
48 Moderate,
50 Low,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
56#[serde(rename_all = "snake_case")]
57pub enum CompetenceRating {
58 High,
60 #[default]
62 Moderate,
63 Low,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
69#[serde(rename_all = "snake_case")]
70pub enum RelianceExtent {
71 NoReliance,
73 #[default]
75 LimitedReliance,
76 SignificantReliance,
78 FullReliance,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
84#[serde(rename_all = "snake_case")]
85pub enum IaReportRating {
86 #[default]
88 Satisfactory,
89 NeedsImprovement,
91 Unsatisfactory,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
97#[serde(rename_all = "snake_case")]
98pub enum IaReportStatus {
99 #[default]
101 Draft,
102 Final,
104 Retracted,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
110#[serde(rename_all = "snake_case")]
111pub enum RecommendationPriority {
112 Critical,
114 High,
116 #[default]
118 Medium,
119 Low,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
125#[serde(rename_all = "snake_case")]
126pub enum ActionPlanStatus {
127 #[default]
129 Open,
130 InProgress,
132 Implemented,
134 Overdue,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
140#[serde(rename_all = "snake_case")]
141pub enum IaWorkAssessment {
142 Reliable,
144 #[default]
146 PartiallyReliable,
147 Unreliable,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct InternalAuditFunction {
154 pub function_id: Uuid,
156 pub function_ref: String,
158 pub engagement_id: Uuid,
160
161 pub department_name: String,
164 pub reporting_line: ReportingLine,
166 pub head_of_ia: String,
168 pub head_of_ia_qualifications: Vec<String>,
170 pub staff_count: u32,
172 pub annual_plan_coverage: f64,
174 pub quality_assurance: bool,
176
177 pub isa_610_assessment: IaAssessment,
180 pub objectivity_rating: ObjectivityRating,
182 pub competence_rating: CompetenceRating,
184 pub systematic_discipline: bool,
186
187 pub reliance_extent: RelianceExtent,
190 pub reliance_areas: Vec<String>,
192 pub direct_assistance: bool,
194
195 #[serde(with = "crate::serde_timestamp::utc")]
197 pub created_at: DateTime<Utc>,
198 #[serde(with = "crate::serde_timestamp::utc")]
199 pub updated_at: DateTime<Utc>,
200}
201
202impl InternalAuditFunction {
203 pub fn new(
205 engagement_id: Uuid,
206 department_name: impl Into<String>,
207 head_of_ia: impl Into<String>,
208 ) -> Self {
209 let now = Utc::now();
210 let id = Uuid::new_v4();
211 let function_ref = format!("IAF-{}", &id.simple().to_string()[..8]);
212 Self {
213 function_id: id,
214 function_ref,
215 engagement_id,
216 department_name: department_name.into(),
217 reporting_line: ReportingLine::AuditCommittee,
218 head_of_ia: head_of_ia.into(),
219 head_of_ia_qualifications: Vec::new(),
220 staff_count: 0,
221 annual_plan_coverage: 0.0,
222 quality_assurance: false,
223 isa_610_assessment: IaAssessment::LargelyEffective,
224 objectivity_rating: ObjectivityRating::High,
225 competence_rating: CompetenceRating::Moderate,
226 systematic_discipline: true,
227 reliance_extent: RelianceExtent::LimitedReliance,
228 reliance_areas: Vec::new(),
229 direct_assistance: false,
230 created_at: now,
231 updated_at: now,
232 }
233 }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct IaRecommendation {
239 pub recommendation_id: Uuid,
241 pub description: String,
243 pub priority: RecommendationPriority,
245 pub management_response: Option<String>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct ActionPlan {
252 pub plan_id: Uuid,
254 pub recommendation_id: Uuid,
256 pub description: String,
258 pub responsible_party: String,
260 pub target_date: NaiveDate,
262 pub status: ActionPlanStatus,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct InternalAuditReport {
269 pub report_id: Uuid,
271 pub report_ref: String,
273 pub engagement_id: Uuid,
275 pub ia_function_id: Uuid,
277
278 pub report_title: String,
281 pub audit_area: String,
283 pub report_date: NaiveDate,
285 pub period_start: NaiveDate,
287 pub period_end: NaiveDate,
289
290 pub scope_description: String,
293 pub methodology: String,
295
296 pub overall_rating: IaReportRating,
299 pub findings_count: u32,
301 pub high_risk_findings: u32,
303 pub recommendations: Vec<IaRecommendation>,
305 pub management_action_plans: Vec<ActionPlan>,
307
308 pub status: IaReportStatus,
311
312 pub external_auditor_assessment: Option<IaWorkAssessment>,
315
316 #[serde(with = "crate::serde_timestamp::utc")]
318 pub created_at: DateTime<Utc>,
319 #[serde(with = "crate::serde_timestamp::utc")]
320 pub updated_at: DateTime<Utc>,
321}
322
323impl InternalAuditReport {
324 pub fn new(
326 engagement_id: Uuid,
327 ia_function_id: Uuid,
328 report_title: impl Into<String>,
329 audit_area: impl Into<String>,
330 report_date: NaiveDate,
331 period_start: NaiveDate,
332 period_end: NaiveDate,
333 ) -> Self {
334 let now = Utc::now();
335 let id = Uuid::new_v4();
336 let report_ref = format!("IAR-{}", &id.simple().to_string()[..8]);
337 Self {
338 report_id: id,
339 report_ref,
340 engagement_id,
341 ia_function_id,
342 report_title: report_title.into(),
343 audit_area: audit_area.into(),
344 report_date,
345 period_start,
346 period_end,
347 scope_description: String::new(),
348 methodology: String::new(),
349 overall_rating: IaReportRating::Satisfactory,
350 findings_count: 0,
351 high_risk_findings: 0,
352 recommendations: Vec::new(),
353 management_action_plans: Vec::new(),
354 status: IaReportStatus::Draft,
355 external_auditor_assessment: None,
356 created_at: now,
357 updated_at: now,
358 }
359 }
360}
361
362#[cfg(test)]
363#[allow(clippy::unwrap_used)]
364mod tests {
365 use super::*;
366
367 fn sample_date(year: i32, month: u32, day: u32) -> NaiveDate {
368 NaiveDate::from_ymd_opt(year, month, day).unwrap()
369 }
370
371 #[test]
372 fn test_new_ia_function() {
373 let eng = Uuid::new_v4();
374 let iaf = InternalAuditFunction::new(eng, "Group Internal Audit", "Jane Smith");
375
376 assert_eq!(iaf.engagement_id, eng);
377 assert_eq!(iaf.department_name, "Group Internal Audit");
378 assert_eq!(iaf.head_of_ia, "Jane Smith");
379 assert_eq!(iaf.reporting_line, ReportingLine::AuditCommittee);
380 assert_eq!(iaf.isa_610_assessment, IaAssessment::LargelyEffective);
381 assert_eq!(iaf.objectivity_rating, ObjectivityRating::High);
382 assert_eq!(iaf.competence_rating, CompetenceRating::Moderate);
383 assert_eq!(iaf.reliance_extent, RelianceExtent::LimitedReliance);
384 assert!(iaf.systematic_discipline);
385 assert!(!iaf.direct_assistance);
386 assert!(iaf.function_ref.starts_with("IAF-"));
387 assert_eq!(iaf.function_ref.len(), 12); }
389
390 #[test]
391 fn test_new_ia_report() {
392 let eng = Uuid::new_v4();
393 let func = Uuid::new_v4();
394 let report = InternalAuditReport::new(
395 eng,
396 func,
397 "Procurement Process Review",
398 "Procurement",
399 sample_date(2025, 3, 31),
400 sample_date(2025, 1, 1),
401 sample_date(2025, 12, 31),
402 );
403
404 assert_eq!(report.engagement_id, eng);
405 assert_eq!(report.ia_function_id, func);
406 assert_eq!(report.report_title, "Procurement Process Review");
407 assert_eq!(report.audit_area, "Procurement");
408 assert_eq!(report.overall_rating, IaReportRating::Satisfactory);
409 assert_eq!(report.status, IaReportStatus::Draft);
410 assert_eq!(report.findings_count, 0);
411 assert!(report.recommendations.is_empty());
412 assert!(report.external_auditor_assessment.is_none());
413 assert!(report.report_ref.starts_with("IAR-"));
414 assert_eq!(report.report_ref.len(), 12); }
416
417 #[test]
418 fn test_reporting_line_serde() {
419 let variants = [
420 ReportingLine::AuditCommittee,
421 ReportingLine::Board,
422 ReportingLine::CFO,
423 ReportingLine::CEO,
424 ];
425 for v in variants {
426 let json = serde_json::to_string(&v).unwrap();
427 let rt: ReportingLine = serde_json::from_str(&json).unwrap();
428 assert_eq!(v, rt);
429 }
430 assert_eq!(
431 serde_json::to_string(&ReportingLine::AuditCommittee).unwrap(),
432 "\"audit_committee\""
433 );
434 }
435
436 #[test]
437 fn test_ia_assessment_serde() {
438 let variants = [
439 IaAssessment::FullyEffective,
440 IaAssessment::LargelyEffective,
441 IaAssessment::PartiallyEffective,
442 IaAssessment::Ineffective,
443 ];
444 for v in variants {
445 let json = serde_json::to_string(&v).unwrap();
446 let rt: IaAssessment = serde_json::from_str(&json).unwrap();
447 assert_eq!(v, rt);
448 }
449 assert_eq!(
450 serde_json::to_string(&IaAssessment::FullyEffective).unwrap(),
451 "\"fully_effective\""
452 );
453 }
454
455 #[test]
456 fn test_reliance_extent_serde() {
457 let variants = [
458 RelianceExtent::NoReliance,
459 RelianceExtent::LimitedReliance,
460 RelianceExtent::SignificantReliance,
461 RelianceExtent::FullReliance,
462 ];
463 for v in variants {
464 let json = serde_json::to_string(&v).unwrap();
465 let rt: RelianceExtent = serde_json::from_str(&json).unwrap();
466 assert_eq!(v, rt);
467 }
468 assert_eq!(
469 serde_json::to_string(&RelianceExtent::SignificantReliance).unwrap(),
470 "\"significant_reliance\""
471 );
472 }
473
474 #[test]
475 fn test_ia_report_status_serde() {
476 let variants = [
477 IaReportStatus::Draft,
478 IaReportStatus::Final,
479 IaReportStatus::Retracted,
480 ];
481 for v in variants {
482 let json = serde_json::to_string(&v).unwrap();
483 let rt: IaReportStatus = serde_json::from_str(&json).unwrap();
484 assert_eq!(v, rt);
485 }
486 assert_eq!(
487 serde_json::to_string(&IaReportStatus::Final).unwrap(),
488 "\"final\""
489 );
490 }
491
492 #[test]
493 fn test_ia_report_rating_serde() {
494 let variants = [
495 IaReportRating::Satisfactory,
496 IaReportRating::NeedsImprovement,
497 IaReportRating::Unsatisfactory,
498 ];
499 for v in variants {
500 let json = serde_json::to_string(&v).unwrap();
501 let rt: IaReportRating = serde_json::from_str(&json).unwrap();
502 assert_eq!(v, rt);
503 }
504 assert_eq!(
505 serde_json::to_string(&IaReportRating::NeedsImprovement).unwrap(),
506 "\"needs_improvement\""
507 );
508 }
509
510 #[test]
511 fn test_recommendation_priority_serde() {
512 let variants = [
513 RecommendationPriority::Critical,
514 RecommendationPriority::High,
515 RecommendationPriority::Medium,
516 RecommendationPriority::Low,
517 ];
518 for v in variants {
519 let json = serde_json::to_string(&v).unwrap();
520 let rt: RecommendationPriority = serde_json::from_str(&json).unwrap();
521 assert_eq!(v, rt);
522 }
523 assert_eq!(
524 serde_json::to_string(&RecommendationPriority::Critical).unwrap(),
525 "\"critical\""
526 );
527 }
528
529 #[test]
530 fn test_action_plan_status_serde() {
531 let variants = [
532 ActionPlanStatus::Open,
533 ActionPlanStatus::InProgress,
534 ActionPlanStatus::Implemented,
535 ActionPlanStatus::Overdue,
536 ];
537 for v in variants {
538 let json = serde_json::to_string(&v).unwrap();
539 let rt: ActionPlanStatus = serde_json::from_str(&json).unwrap();
540 assert_eq!(v, rt);
541 }
542 assert_eq!(
543 serde_json::to_string(&ActionPlanStatus::InProgress).unwrap(),
544 "\"in_progress\""
545 );
546 }
547
548 #[test]
549 fn test_ia_work_assessment_serde() {
550 let variants = [
551 IaWorkAssessment::Reliable,
552 IaWorkAssessment::PartiallyReliable,
553 IaWorkAssessment::Unreliable,
554 ];
555 for v in variants {
556 let json = serde_json::to_string(&v).unwrap();
557 let rt: IaWorkAssessment = serde_json::from_str(&json).unwrap();
558 assert_eq!(v, rt);
559 }
560 assert_eq!(
561 serde_json::to_string(&IaWorkAssessment::PartiallyReliable).unwrap(),
562 "\"partially_reliable\""
563 );
564 }
565}