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 pub created_at: DateTime<Utc>,
197 pub updated_at: DateTime<Utc>,
198}
199
200impl InternalAuditFunction {
201 pub fn new(
203 engagement_id: Uuid,
204 department_name: impl Into<String>,
205 head_of_ia: impl Into<String>,
206 ) -> Self {
207 let now = Utc::now();
208 let id = Uuid::new_v4();
209 let function_ref = format!("IAF-{}", &id.simple().to_string()[..8]);
210 Self {
211 function_id: id,
212 function_ref,
213 engagement_id,
214 department_name: department_name.into(),
215 reporting_line: ReportingLine::AuditCommittee,
216 head_of_ia: head_of_ia.into(),
217 head_of_ia_qualifications: Vec::new(),
218 staff_count: 0,
219 annual_plan_coverage: 0.0,
220 quality_assurance: false,
221 isa_610_assessment: IaAssessment::LargelyEffective,
222 objectivity_rating: ObjectivityRating::High,
223 competence_rating: CompetenceRating::Moderate,
224 systematic_discipline: true,
225 reliance_extent: RelianceExtent::LimitedReliance,
226 reliance_areas: Vec::new(),
227 direct_assistance: false,
228 created_at: now,
229 updated_at: now,
230 }
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct IaRecommendation {
237 pub recommendation_id: Uuid,
239 pub description: String,
241 pub priority: RecommendationPriority,
243 pub management_response: Option<String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct ActionPlan {
250 pub plan_id: Uuid,
252 pub recommendation_id: Uuid,
254 pub description: String,
256 pub responsible_party: String,
258 pub target_date: NaiveDate,
260 pub status: ActionPlanStatus,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct InternalAuditReport {
267 pub report_id: Uuid,
269 pub report_ref: String,
271 pub engagement_id: Uuid,
273 pub ia_function_id: Uuid,
275
276 pub report_title: String,
279 pub audit_area: String,
281 pub report_date: NaiveDate,
283 pub period_start: NaiveDate,
285 pub period_end: NaiveDate,
287
288 pub scope_description: String,
291 pub methodology: String,
293
294 pub overall_rating: IaReportRating,
297 pub findings_count: u32,
299 pub high_risk_findings: u32,
301 pub recommendations: Vec<IaRecommendation>,
303 pub management_action_plans: Vec<ActionPlan>,
305
306 pub status: IaReportStatus,
309
310 pub external_auditor_assessment: Option<IaWorkAssessment>,
313
314 pub created_at: DateTime<Utc>,
316 pub updated_at: DateTime<Utc>,
317}
318
319impl InternalAuditReport {
320 pub fn new(
322 engagement_id: Uuid,
323 ia_function_id: Uuid,
324 report_title: impl Into<String>,
325 audit_area: impl Into<String>,
326 report_date: NaiveDate,
327 period_start: NaiveDate,
328 period_end: NaiveDate,
329 ) -> Self {
330 let now = Utc::now();
331 let id = Uuid::new_v4();
332 let report_ref = format!("IAR-{}", &id.simple().to_string()[..8]);
333 Self {
334 report_id: id,
335 report_ref,
336 engagement_id,
337 ia_function_id,
338 report_title: report_title.into(),
339 audit_area: audit_area.into(),
340 report_date,
341 period_start,
342 period_end,
343 scope_description: String::new(),
344 methodology: String::new(),
345 overall_rating: IaReportRating::Satisfactory,
346 findings_count: 0,
347 high_risk_findings: 0,
348 recommendations: Vec::new(),
349 management_action_plans: Vec::new(),
350 status: IaReportStatus::Draft,
351 external_auditor_assessment: None,
352 created_at: now,
353 updated_at: now,
354 }
355 }
356}
357
358#[cfg(test)]
359#[allow(clippy::unwrap_used)]
360mod tests {
361 use super::*;
362
363 fn sample_date(year: i32, month: u32, day: u32) -> NaiveDate {
364 NaiveDate::from_ymd_opt(year, month, day).unwrap()
365 }
366
367 #[test]
368 fn test_new_ia_function() {
369 let eng = Uuid::new_v4();
370 let iaf = InternalAuditFunction::new(eng, "Group Internal Audit", "Jane Smith");
371
372 assert_eq!(iaf.engagement_id, eng);
373 assert_eq!(iaf.department_name, "Group Internal Audit");
374 assert_eq!(iaf.head_of_ia, "Jane Smith");
375 assert_eq!(iaf.reporting_line, ReportingLine::AuditCommittee);
376 assert_eq!(iaf.isa_610_assessment, IaAssessment::LargelyEffective);
377 assert_eq!(iaf.objectivity_rating, ObjectivityRating::High);
378 assert_eq!(iaf.competence_rating, CompetenceRating::Moderate);
379 assert_eq!(iaf.reliance_extent, RelianceExtent::LimitedReliance);
380 assert!(iaf.systematic_discipline);
381 assert!(!iaf.direct_assistance);
382 assert!(iaf.function_ref.starts_with("IAF-"));
383 assert_eq!(iaf.function_ref.len(), 12); }
385
386 #[test]
387 fn test_new_ia_report() {
388 let eng = Uuid::new_v4();
389 let func = Uuid::new_v4();
390 let report = InternalAuditReport::new(
391 eng,
392 func,
393 "Procurement Process Review",
394 "Procurement",
395 sample_date(2025, 3, 31),
396 sample_date(2025, 1, 1),
397 sample_date(2025, 12, 31),
398 );
399
400 assert_eq!(report.engagement_id, eng);
401 assert_eq!(report.ia_function_id, func);
402 assert_eq!(report.report_title, "Procurement Process Review");
403 assert_eq!(report.audit_area, "Procurement");
404 assert_eq!(report.overall_rating, IaReportRating::Satisfactory);
405 assert_eq!(report.status, IaReportStatus::Draft);
406 assert_eq!(report.findings_count, 0);
407 assert!(report.recommendations.is_empty());
408 assert!(report.external_auditor_assessment.is_none());
409 assert!(report.report_ref.starts_with("IAR-"));
410 assert_eq!(report.report_ref.len(), 12); }
412
413 #[test]
414 fn test_reporting_line_serde() {
415 let variants = [
416 ReportingLine::AuditCommittee,
417 ReportingLine::Board,
418 ReportingLine::CFO,
419 ReportingLine::CEO,
420 ];
421 for v in variants {
422 let json = serde_json::to_string(&v).unwrap();
423 let rt: ReportingLine = serde_json::from_str(&json).unwrap();
424 assert_eq!(v, rt);
425 }
426 assert_eq!(
427 serde_json::to_string(&ReportingLine::AuditCommittee).unwrap(),
428 "\"audit_committee\""
429 );
430 }
431
432 #[test]
433 fn test_ia_assessment_serde() {
434 let variants = [
435 IaAssessment::FullyEffective,
436 IaAssessment::LargelyEffective,
437 IaAssessment::PartiallyEffective,
438 IaAssessment::Ineffective,
439 ];
440 for v in variants {
441 let json = serde_json::to_string(&v).unwrap();
442 let rt: IaAssessment = serde_json::from_str(&json).unwrap();
443 assert_eq!(v, rt);
444 }
445 assert_eq!(
446 serde_json::to_string(&IaAssessment::FullyEffective).unwrap(),
447 "\"fully_effective\""
448 );
449 }
450
451 #[test]
452 fn test_reliance_extent_serde() {
453 let variants = [
454 RelianceExtent::NoReliance,
455 RelianceExtent::LimitedReliance,
456 RelianceExtent::SignificantReliance,
457 RelianceExtent::FullReliance,
458 ];
459 for v in variants {
460 let json = serde_json::to_string(&v).unwrap();
461 let rt: RelianceExtent = serde_json::from_str(&json).unwrap();
462 assert_eq!(v, rt);
463 }
464 assert_eq!(
465 serde_json::to_string(&RelianceExtent::SignificantReliance).unwrap(),
466 "\"significant_reliance\""
467 );
468 }
469
470 #[test]
471 fn test_ia_report_status_serde() {
472 let variants = [
473 IaReportStatus::Draft,
474 IaReportStatus::Final,
475 IaReportStatus::Retracted,
476 ];
477 for v in variants {
478 let json = serde_json::to_string(&v).unwrap();
479 let rt: IaReportStatus = serde_json::from_str(&json).unwrap();
480 assert_eq!(v, rt);
481 }
482 assert_eq!(
483 serde_json::to_string(&IaReportStatus::Final).unwrap(),
484 "\"final\""
485 );
486 }
487
488 #[test]
489 fn test_ia_report_rating_serde() {
490 let variants = [
491 IaReportRating::Satisfactory,
492 IaReportRating::NeedsImprovement,
493 IaReportRating::Unsatisfactory,
494 ];
495 for v in variants {
496 let json = serde_json::to_string(&v).unwrap();
497 let rt: IaReportRating = serde_json::from_str(&json).unwrap();
498 assert_eq!(v, rt);
499 }
500 assert_eq!(
501 serde_json::to_string(&IaReportRating::NeedsImprovement).unwrap(),
502 "\"needs_improvement\""
503 );
504 }
505
506 #[test]
507 fn test_recommendation_priority_serde() {
508 let variants = [
509 RecommendationPriority::Critical,
510 RecommendationPriority::High,
511 RecommendationPriority::Medium,
512 RecommendationPriority::Low,
513 ];
514 for v in variants {
515 let json = serde_json::to_string(&v).unwrap();
516 let rt: RecommendationPriority = serde_json::from_str(&json).unwrap();
517 assert_eq!(v, rt);
518 }
519 assert_eq!(
520 serde_json::to_string(&RecommendationPriority::Critical).unwrap(),
521 "\"critical\""
522 );
523 }
524
525 #[test]
526 fn test_action_plan_status_serde() {
527 let variants = [
528 ActionPlanStatus::Open,
529 ActionPlanStatus::InProgress,
530 ActionPlanStatus::Implemented,
531 ActionPlanStatus::Overdue,
532 ];
533 for v in variants {
534 let json = serde_json::to_string(&v).unwrap();
535 let rt: ActionPlanStatus = serde_json::from_str(&json).unwrap();
536 assert_eq!(v, rt);
537 }
538 assert_eq!(
539 serde_json::to_string(&ActionPlanStatus::InProgress).unwrap(),
540 "\"in_progress\""
541 );
542 }
543
544 #[test]
545 fn test_ia_work_assessment_serde() {
546 let variants = [
547 IaWorkAssessment::Reliable,
548 IaWorkAssessment::PartiallyReliable,
549 IaWorkAssessment::Unreliable,
550 ];
551 for v in variants {
552 let json = serde_json::to_string(&v).unwrap();
553 let rt: IaWorkAssessment = serde_json::from_str(&json).unwrap();
554 assert_eq!(v, rt);
555 }
556 assert_eq!(
557 serde_json::to_string(&IaWorkAssessment::PartiallyReliable).unwrap(),
558 "\"partially_reliable\""
559 );
560 }
561}