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)]
363mod tests {
364    use super::*;
365
366    fn sample_date(year: i32, month: u32, day: u32) -> NaiveDate {
367        NaiveDate::from_ymd_opt(year, month, day).unwrap()
368    }
369
370    #[test]
371    fn test_new_ia_function() {
372        let eng = Uuid::new_v4();
373        let iaf = InternalAuditFunction::new(eng, "Group Internal Audit", "Jane Smith");
374
375        assert_eq!(iaf.engagement_id, eng);
376        assert_eq!(iaf.department_name, "Group Internal Audit");
377        assert_eq!(iaf.head_of_ia, "Jane Smith");
378        assert_eq!(iaf.reporting_line, ReportingLine::AuditCommittee);
379        assert_eq!(iaf.isa_610_assessment, IaAssessment::LargelyEffective);
380        assert_eq!(iaf.objectivity_rating, ObjectivityRating::High);
381        assert_eq!(iaf.competence_rating, CompetenceRating::Moderate);
382        assert_eq!(iaf.reliance_extent, RelianceExtent::LimitedReliance);
383        assert!(iaf.systematic_discipline);
384        assert!(!iaf.direct_assistance);
385        assert!(iaf.function_ref.starts_with("IAF-"));
386        assert_eq!(iaf.function_ref.len(), 12); // "IAF-" + 8 hex chars
387    }
388
389    #[test]
390    fn test_new_ia_report() {
391        let eng = Uuid::new_v4();
392        let func = Uuid::new_v4();
393        let report = InternalAuditReport::new(
394            eng,
395            func,
396            "Procurement Process Review",
397            "Procurement",
398            sample_date(2025, 3, 31),
399            sample_date(2025, 1, 1),
400            sample_date(2025, 12, 31),
401        );
402
403        assert_eq!(report.engagement_id, eng);
404        assert_eq!(report.ia_function_id, func);
405        assert_eq!(report.report_title, "Procurement Process Review");
406        assert_eq!(report.audit_area, "Procurement");
407        assert_eq!(report.overall_rating, IaReportRating::Satisfactory);
408        assert_eq!(report.status, IaReportStatus::Draft);
409        assert_eq!(report.findings_count, 0);
410        assert!(report.recommendations.is_empty());
411        assert!(report.external_auditor_assessment.is_none());
412        assert!(report.report_ref.starts_with("IAR-"));
413        assert_eq!(report.report_ref.len(), 12); // "IAR-" + 8 hex chars
414    }
415
416    #[test]
417    fn test_reporting_line_serde() {
418        let variants = [
419            ReportingLine::AuditCommittee,
420            ReportingLine::Board,
421            ReportingLine::CFO,
422            ReportingLine::CEO,
423        ];
424        for v in variants {
425            let json = serde_json::to_string(&v).unwrap();
426            let rt: ReportingLine = serde_json::from_str(&json).unwrap();
427            assert_eq!(v, rt);
428        }
429        assert_eq!(
430            serde_json::to_string(&ReportingLine::AuditCommittee).unwrap(),
431            "\"audit_committee\""
432        );
433    }
434
435    #[test]
436    fn test_ia_assessment_serde() {
437        let variants = [
438            IaAssessment::FullyEffective,
439            IaAssessment::LargelyEffective,
440            IaAssessment::PartiallyEffective,
441            IaAssessment::Ineffective,
442        ];
443        for v in variants {
444            let json = serde_json::to_string(&v).unwrap();
445            let rt: IaAssessment = serde_json::from_str(&json).unwrap();
446            assert_eq!(v, rt);
447        }
448        assert_eq!(
449            serde_json::to_string(&IaAssessment::FullyEffective).unwrap(),
450            "\"fully_effective\""
451        );
452    }
453
454    #[test]
455    fn test_reliance_extent_serde() {
456        let variants = [
457            RelianceExtent::NoReliance,
458            RelianceExtent::LimitedReliance,
459            RelianceExtent::SignificantReliance,
460            RelianceExtent::FullReliance,
461        ];
462        for v in variants {
463            let json = serde_json::to_string(&v).unwrap();
464            let rt: RelianceExtent = serde_json::from_str(&json).unwrap();
465            assert_eq!(v, rt);
466        }
467        assert_eq!(
468            serde_json::to_string(&RelianceExtent::SignificantReliance).unwrap(),
469            "\"significant_reliance\""
470        );
471    }
472
473    #[test]
474    fn test_ia_report_status_serde() {
475        let variants = [
476            IaReportStatus::Draft,
477            IaReportStatus::Final,
478            IaReportStatus::Retracted,
479        ];
480        for v in variants {
481            let json = serde_json::to_string(&v).unwrap();
482            let rt: IaReportStatus = serde_json::from_str(&json).unwrap();
483            assert_eq!(v, rt);
484        }
485        assert_eq!(
486            serde_json::to_string(&IaReportStatus::Final).unwrap(),
487            "\"final\""
488        );
489    }
490
491    #[test]
492    fn test_ia_report_rating_serde() {
493        let variants = [
494            IaReportRating::Satisfactory,
495            IaReportRating::NeedsImprovement,
496            IaReportRating::Unsatisfactory,
497        ];
498        for v in variants {
499            let json = serde_json::to_string(&v).unwrap();
500            let rt: IaReportRating = serde_json::from_str(&json).unwrap();
501            assert_eq!(v, rt);
502        }
503        assert_eq!(
504            serde_json::to_string(&IaReportRating::NeedsImprovement).unwrap(),
505            "\"needs_improvement\""
506        );
507    }
508
509    #[test]
510    fn test_recommendation_priority_serde() {
511        let variants = [
512            RecommendationPriority::Critical,
513            RecommendationPriority::High,
514            RecommendationPriority::Medium,
515            RecommendationPriority::Low,
516        ];
517        for v in variants {
518            let json = serde_json::to_string(&v).unwrap();
519            let rt: RecommendationPriority = serde_json::from_str(&json).unwrap();
520            assert_eq!(v, rt);
521        }
522        assert_eq!(
523            serde_json::to_string(&RecommendationPriority::Critical).unwrap(),
524            "\"critical\""
525        );
526    }
527
528    #[test]
529    fn test_action_plan_status_serde() {
530        let variants = [
531            ActionPlanStatus::Open,
532            ActionPlanStatus::InProgress,
533            ActionPlanStatus::Implemented,
534            ActionPlanStatus::Overdue,
535        ];
536        for v in variants {
537            let json = serde_json::to_string(&v).unwrap();
538            let rt: ActionPlanStatus = serde_json::from_str(&json).unwrap();
539            assert_eq!(v, rt);
540        }
541        assert_eq!(
542            serde_json::to_string(&ActionPlanStatus::InProgress).unwrap(),
543            "\"in_progress\""
544        );
545    }
546
547    #[test]
548    fn test_ia_work_assessment_serde() {
549        let variants = [
550            IaWorkAssessment::Reliable,
551            IaWorkAssessment::PartiallyReliable,
552            IaWorkAssessment::Unreliable,
553        ];
554        for v in variants {
555            let json = serde_json::to_string(&v).unwrap();
556            let rt: IaWorkAssessment = serde_json::from_str(&json).unwrap();
557            assert_eq!(v, rt);
558        }
559        assert_eq!(
560            serde_json::to_string(&IaWorkAssessment::PartiallyReliable).unwrap(),
561            "\"partially_reliable\""
562        );
563    }
564}