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    #[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    /// Create a new internal audit function record with sensible defaults.
204    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/// A recommendation raised in an internal audit report.
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct IaRecommendation {
239    /// Unique recommendation ID
240    pub recommendation_id: Uuid,
241    /// Description of the recommendation
242    pub description: String,
243    /// Priority level
244    pub priority: RecommendationPriority,
245    /// Management's response to the recommendation
246    pub management_response: Option<String>,
247}
248
249/// Management's action plan in response to an internal audit recommendation.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct ActionPlan {
252    /// Unique plan ID
253    pub plan_id: Uuid,
254    /// The recommendation this plan addresses
255    pub recommendation_id: Uuid,
256    /// Description of the planned action
257    pub description: String,
258    /// Party responsible for implementing the action
259    pub responsible_party: String,
260    /// Target implementation date
261    pub target_date: NaiveDate,
262    /// Current status of the action plan
263    pub status: ActionPlanStatus,
264}
265
266/// An internal audit report for a specific audit area (ISA 610).
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct InternalAuditReport {
269    /// Unique report ID
270    pub report_id: Uuid,
271    /// Human-readable reference (format: "IAR-{first 8 hex chars of report_id}")
272    pub report_ref: String,
273    /// Engagement this report is associated with
274    pub engagement_id: Uuid,
275    /// The internal audit function that produced this report
276    pub ia_function_id: Uuid,
277
278    // === Report Header ===
279    /// Report title
280    pub report_title: String,
281    /// Audit area covered by the report
282    pub audit_area: String,
283    /// Date the report was issued
284    pub report_date: NaiveDate,
285    /// Start of the period covered by the audit
286    pub period_start: NaiveDate,
287    /// End of the period covered by the audit
288    pub period_end: NaiveDate,
289
290    // === Scope & Methodology ===
291    /// Description of the audit scope
292    pub scope_description: String,
293    /// Methodology applied during the audit
294    pub methodology: String,
295
296    // === Findings & Ratings ===
297    /// Overall rating of the audited area
298    pub overall_rating: IaReportRating,
299    /// Total number of findings raised
300    pub findings_count: u32,
301    /// Number of high-risk findings
302    pub high_risk_findings: u32,
303    /// Recommendations raised in the report
304    pub recommendations: Vec<IaRecommendation>,
305    /// Management action plans in response to the recommendations
306    pub management_action_plans: Vec<ActionPlan>,
307
308    // === Status ===
309    /// Current lifecycle status of the report
310    pub status: IaReportStatus,
311
312    // === External Auditor's Assessment ===
313    /// External auditor's assessment of the reliability of this report's work
314    pub external_auditor_assessment: Option<IaWorkAssessment>,
315
316    // === Timestamps ===
317    #[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    /// Create a new internal audit report.
325    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); // "IAF-" + 8 hex chars
388    }
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); // "IAR-" + 8 hex chars
415    }
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}