Skip to main content

datasynth_core/models/audit/
internal_audit.rs

1//! Internal audit function and report models per ISA 610.
2//!
3//! ISA 610 governs the use of the work of internal auditors. The external auditor
4//! must evaluate the internal audit function's objectivity, competence, and whether
5//! a systematic and disciplined approach has been applied.
6
7use chrono::{DateTime, NaiveDate, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11/// Reporting line of the internal audit function.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum ReportingLine {
15    /// Reports to the Audit Committee
16    #[default]
17    AuditCommittee,
18    /// Reports to the Board of Directors
19    Board,
20    /// Reports to the Chief Financial Officer
21    CFO,
22    /// Reports to the Chief Executive Officer
23    CEO,
24}
25
26/// ISA 610 assessment of the overall effectiveness of the internal audit function.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
28#[serde(rename_all = "snake_case")]
29pub enum IaAssessment {
30    /// Internal audit is fully effective across all dimensions
31    FullyEffective,
32    /// Internal audit is largely effective with minor gaps
33    #[default]
34    LargelyEffective,
35    /// Internal audit is partially effective with notable gaps
36    PartiallyEffective,
37    /// Internal audit is not effective
38    Ineffective,
39}
40
41/// Objectivity rating of internal audit personnel.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
43#[serde(rename_all = "snake_case")]
44pub enum ObjectivityRating {
45    /// High objectivity — strong independence safeguards in place
46    #[default]
47    High,
48    /// Moderate objectivity — some independence concerns exist
49    Moderate,
50    /// Low objectivity — significant independence concerns exist
51    Low,
52}
53
54/// Competence rating of the internal audit function.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
56#[serde(rename_all = "snake_case")]
57pub enum CompetenceRating {
58    /// High competence — well-qualified staff with relevant expertise
59    High,
60    /// Moderate competence — adequate but some skill gaps exist
61    #[default]
62    Moderate,
63    /// Low competence — significant skill gaps exist
64    Low,
65}
66
67/// Extent to which the external auditor relies on the internal audit function's work.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
69#[serde(rename_all = "snake_case")]
70pub enum RelianceExtent {
71    /// No reliance — external auditor performs all work independently
72    NoReliance,
73    /// Limited reliance — minor use of internal audit work
74    #[default]
75    LimitedReliance,
76    /// Significant reliance — substantial use of internal audit work
77    SignificantReliance,
78    /// Full reliance — maximum use of internal audit work permitted by ISA 610
79    FullReliance,
80}
81
82/// Overall rating assigned by the internal auditor to the audited area.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
84#[serde(rename_all = "snake_case")]
85pub enum IaReportRating {
86    /// Area is operating satisfactorily
87    #[default]
88    Satisfactory,
89    /// Area needs improvement in certain respects
90    NeedsImprovement,
91    /// Area is unsatisfactory with material control weaknesses
92    Unsatisfactory,
93}
94
95/// Lifecycle status of an internal audit report.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
97#[serde(rename_all = "snake_case")]
98pub enum IaReportStatus {
99    /// Report is in draft form, pending management review
100    #[default]
101    Draft,
102    /// Report has been finalised and issued
103    Final,
104    /// Report has been retracted
105    Retracted,
106}
107
108/// Priority level assigned to an internal audit recommendation.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
110#[serde(rename_all = "snake_case")]
111pub enum RecommendationPriority {
112    /// Critical — immediate action required to address a significant control failure
113    Critical,
114    /// High — urgent action required
115    High,
116    /// Medium — action required within a reasonable timeframe
117    #[default]
118    Medium,
119    /// Low — action desirable but not urgent
120    Low,
121}
122
123/// Status of a management action plan in response to a recommendation.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
125#[serde(rename_all = "snake_case")]
126pub enum ActionPlanStatus {
127    /// Action plan has not yet been started
128    #[default]
129    Open,
130    /// Action plan is in progress
131    InProgress,
132    /// Action plan has been fully implemented
133    Implemented,
134    /// Action plan has passed its target date without implementation
135    Overdue,
136}
137
138/// External auditor's assessment of the reliability of a specific piece of internal audit work.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
140#[serde(rename_all = "snake_case")]
141pub enum IaWorkAssessment {
142    /// Work is reliable and can be used without significant modification
143    Reliable,
144    /// Work is partially reliable — some additional procedures required
145    #[default]
146    PartiallyReliable,
147    /// Work is not reliable — cannot be used by the external auditor
148    Unreliable,
149}
150
151/// The internal audit function of the entity being audited (ISA 610).
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct InternalAuditFunction {
154    /// Unique function ID
155    pub function_id: Uuid,
156    /// Human-readable reference (format: "IAF-{first 8 hex chars of function_id}")
157    pub function_ref: String,
158    /// Engagement this assessment relates to
159    pub engagement_id: Uuid,
160
161    // === Organisation ===
162    /// Name of the internal audit department
163    pub department_name: String,
164    /// Reporting line of the head of internal audit
165    pub reporting_line: ReportingLine,
166    /// Name of the head of internal audit
167    pub head_of_ia: String,
168    /// Professional qualifications held by the head of internal audit
169    pub head_of_ia_qualifications: Vec<String>,
170    /// Number of internal audit staff (FTE)
171    pub staff_count: u32,
172    /// Percentage of total risk universe covered by the annual audit plan
173    pub annual_plan_coverage: f64,
174    /// Whether a formal quality assurance and improvement programme exists
175    pub quality_assurance: bool,
176
177    // === ISA 610 Assessment ===
178    /// Overall assessment of the internal audit function per ISA 610
179    pub isa_610_assessment: IaAssessment,
180    /// Assessment of objectivity
181    pub objectivity_rating: ObjectivityRating,
182    /// Assessment of technical competence
183    pub competence_rating: CompetenceRating,
184    /// Whether a systematic and disciplined approach is applied
185    pub systematic_discipline: bool,
186
187    // === Reliance Decision ===
188    /// Extent to which the external auditor plans to rely on internal audit work
189    pub reliance_extent: RelianceExtent,
190    /// Specific audit areas where reliance will be placed
191    pub reliance_areas: Vec<String>,
192    /// Whether direct assistance from internal audit staff will be used
193    pub direct_assistance: bool,
194
195    // === Timestamps ===
196    pub created_at: DateTime<Utc>,
197    pub updated_at: DateTime<Utc>,
198}
199
200impl InternalAuditFunction {
201    /// Create a new internal audit function record with sensible defaults.
202    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/// A recommendation raised in an internal audit report.
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct IaRecommendation {
237    /// Unique recommendation ID
238    pub recommendation_id: Uuid,
239    /// Description of the recommendation
240    pub description: String,
241    /// Priority level
242    pub priority: RecommendationPriority,
243    /// Management's response to the recommendation
244    pub management_response: Option<String>,
245}
246
247/// Management's action plan in response to an internal audit recommendation.
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct ActionPlan {
250    /// Unique plan ID
251    pub plan_id: Uuid,
252    /// The recommendation this plan addresses
253    pub recommendation_id: Uuid,
254    /// Description of the planned action
255    pub description: String,
256    /// Party responsible for implementing the action
257    pub responsible_party: String,
258    /// Target implementation date
259    pub target_date: NaiveDate,
260    /// Current status of the action plan
261    pub status: ActionPlanStatus,
262}
263
264/// An internal audit report for a specific audit area (ISA 610).
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct InternalAuditReport {
267    /// Unique report ID
268    pub report_id: Uuid,
269    /// Human-readable reference (format: "IAR-{first 8 hex chars of report_id}")
270    pub report_ref: String,
271    /// Engagement this report is associated with
272    pub engagement_id: Uuid,
273    /// The internal audit function that produced this report
274    pub ia_function_id: Uuid,
275
276    // === Report Header ===
277    /// Report title
278    pub report_title: String,
279    /// Audit area covered by the report
280    pub audit_area: String,
281    /// Date the report was issued
282    pub report_date: NaiveDate,
283    /// Start of the period covered by the audit
284    pub period_start: NaiveDate,
285    /// End of the period covered by the audit
286    pub period_end: NaiveDate,
287
288    // === Scope & Methodology ===
289    /// Description of the audit scope
290    pub scope_description: String,
291    /// Methodology applied during the audit
292    pub methodology: String,
293
294    // === Findings & Ratings ===
295    /// Overall rating of the audited area
296    pub overall_rating: IaReportRating,
297    /// Total number of findings raised
298    pub findings_count: u32,
299    /// Number of high-risk findings
300    pub high_risk_findings: u32,
301    /// Recommendations raised in the report
302    pub recommendations: Vec<IaRecommendation>,
303    /// Management action plans in response to the recommendations
304    pub management_action_plans: Vec<ActionPlan>,
305
306    // === Status ===
307    /// Current lifecycle status of the report
308    pub status: IaReportStatus,
309
310    // === External Auditor's Assessment ===
311    /// External auditor's assessment of the reliability of this report's work
312    pub external_auditor_assessment: Option<IaWorkAssessment>,
313
314    // === Timestamps ===
315    pub created_at: DateTime<Utc>,
316    pub updated_at: DateTime<Utc>,
317}
318
319impl InternalAuditReport {
320    /// Create a new internal audit report.
321    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); // "IAF-" + 8 hex chars
384    }
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); // "IAR-" + 8 hex chars
411    }
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}