use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReportingLine {
#[default]
AuditCommittee,
Board,
CFO,
CEO,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum IaAssessment {
FullyEffective,
#[default]
LargelyEffective,
PartiallyEffective,
Ineffective,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ObjectivityRating {
#[default]
High,
Moderate,
Low,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CompetenceRating {
High,
#[default]
Moderate,
Low,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RelianceExtent {
NoReliance,
#[default]
LimitedReliance,
SignificantReliance,
FullReliance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum IaReportRating {
#[default]
Satisfactory,
NeedsImprovement,
Unsatisfactory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum IaReportStatus {
#[default]
Draft,
Final,
Retracted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RecommendationPriority {
Critical,
High,
#[default]
Medium,
Low,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ActionPlanStatus {
#[default]
Open,
InProgress,
Implemented,
Overdue,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum IaWorkAssessment {
Reliable,
#[default]
PartiallyReliable,
Unreliable,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InternalAuditFunction {
pub function_id: Uuid,
pub function_ref: String,
pub engagement_id: Uuid,
pub department_name: String,
pub reporting_line: ReportingLine,
pub head_of_ia: String,
pub head_of_ia_qualifications: Vec<String>,
pub staff_count: u32,
pub annual_plan_coverage: f64,
pub quality_assurance: bool,
pub isa_610_assessment: IaAssessment,
pub objectivity_rating: ObjectivityRating,
pub competence_rating: CompetenceRating,
pub systematic_discipline: bool,
pub reliance_extent: RelianceExtent,
pub reliance_areas: Vec<String>,
pub direct_assistance: bool,
#[serde(with = "crate::serde_timestamp::utc")]
pub created_at: DateTime<Utc>,
#[serde(with = "crate::serde_timestamp::utc")]
pub updated_at: DateTime<Utc>,
}
impl InternalAuditFunction {
pub fn new(
engagement_id: Uuid,
department_name: impl Into<String>,
head_of_ia: impl Into<String>,
) -> Self {
let now = Utc::now();
let id = Uuid::new_v4();
let function_ref = format!("IAF-{}", &id.simple().to_string()[..8]);
Self {
function_id: id,
function_ref,
engagement_id,
department_name: department_name.into(),
reporting_line: ReportingLine::AuditCommittee,
head_of_ia: head_of_ia.into(),
head_of_ia_qualifications: Vec::new(),
staff_count: 0,
annual_plan_coverage: 0.0,
quality_assurance: false,
isa_610_assessment: IaAssessment::LargelyEffective,
objectivity_rating: ObjectivityRating::High,
competence_rating: CompetenceRating::Moderate,
systematic_discipline: true,
reliance_extent: RelianceExtent::LimitedReliance,
reliance_areas: Vec::new(),
direct_assistance: false,
created_at: now,
updated_at: now,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IaRecommendation {
pub recommendation_id: Uuid,
pub description: String,
pub priority: RecommendationPriority,
pub management_response: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionPlan {
pub plan_id: Uuid,
pub recommendation_id: Uuid,
pub description: String,
pub responsible_party: String,
pub target_date: NaiveDate,
pub status: ActionPlanStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InternalAuditReport {
pub report_id: Uuid,
pub report_ref: String,
pub engagement_id: Uuid,
pub ia_function_id: Uuid,
pub report_title: String,
pub audit_area: String,
pub report_date: NaiveDate,
pub period_start: NaiveDate,
pub period_end: NaiveDate,
pub scope_description: String,
pub methodology: String,
pub overall_rating: IaReportRating,
pub findings_count: u32,
pub high_risk_findings: u32,
pub recommendations: Vec<IaRecommendation>,
pub management_action_plans: Vec<ActionPlan>,
pub status: IaReportStatus,
pub external_auditor_assessment: Option<IaWorkAssessment>,
#[serde(with = "crate::serde_timestamp::utc")]
pub created_at: DateTime<Utc>,
#[serde(with = "crate::serde_timestamp::utc")]
pub updated_at: DateTime<Utc>,
}
impl InternalAuditReport {
pub fn new(
engagement_id: Uuid,
ia_function_id: Uuid,
report_title: impl Into<String>,
audit_area: impl Into<String>,
report_date: NaiveDate,
period_start: NaiveDate,
period_end: NaiveDate,
) -> Self {
let now = Utc::now();
let id = Uuid::new_v4();
let report_ref = format!("IAR-{}", &id.simple().to_string()[..8]);
Self {
report_id: id,
report_ref,
engagement_id,
ia_function_id,
report_title: report_title.into(),
audit_area: audit_area.into(),
report_date,
period_start,
period_end,
scope_description: String::new(),
methodology: String::new(),
overall_rating: IaReportRating::Satisfactory,
findings_count: 0,
high_risk_findings: 0,
recommendations: Vec::new(),
management_action_plans: Vec::new(),
status: IaReportStatus::Draft,
external_auditor_assessment: None,
created_at: now,
updated_at: now,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn sample_date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).unwrap()
}
#[test]
fn test_new_ia_function() {
let eng = Uuid::new_v4();
let iaf = InternalAuditFunction::new(eng, "Group Internal Audit", "Jane Smith");
assert_eq!(iaf.engagement_id, eng);
assert_eq!(iaf.department_name, "Group Internal Audit");
assert_eq!(iaf.head_of_ia, "Jane Smith");
assert_eq!(iaf.reporting_line, ReportingLine::AuditCommittee);
assert_eq!(iaf.isa_610_assessment, IaAssessment::LargelyEffective);
assert_eq!(iaf.objectivity_rating, ObjectivityRating::High);
assert_eq!(iaf.competence_rating, CompetenceRating::Moderate);
assert_eq!(iaf.reliance_extent, RelianceExtent::LimitedReliance);
assert!(iaf.systematic_discipline);
assert!(!iaf.direct_assistance);
assert!(iaf.function_ref.starts_with("IAF-"));
assert_eq!(iaf.function_ref.len(), 12); }
#[test]
fn test_new_ia_report() {
let eng = Uuid::new_v4();
let func = Uuid::new_v4();
let report = InternalAuditReport::new(
eng,
func,
"Procurement Process Review",
"Procurement",
sample_date(2025, 3, 31),
sample_date(2025, 1, 1),
sample_date(2025, 12, 31),
);
assert_eq!(report.engagement_id, eng);
assert_eq!(report.ia_function_id, func);
assert_eq!(report.report_title, "Procurement Process Review");
assert_eq!(report.audit_area, "Procurement");
assert_eq!(report.overall_rating, IaReportRating::Satisfactory);
assert_eq!(report.status, IaReportStatus::Draft);
assert_eq!(report.findings_count, 0);
assert!(report.recommendations.is_empty());
assert!(report.external_auditor_assessment.is_none());
assert!(report.report_ref.starts_with("IAR-"));
assert_eq!(report.report_ref.len(), 12); }
#[test]
fn test_reporting_line_serde() {
let variants = [
ReportingLine::AuditCommittee,
ReportingLine::Board,
ReportingLine::CFO,
ReportingLine::CEO,
];
for v in variants {
let json = serde_json::to_string(&v).unwrap();
let rt: ReportingLine = serde_json::from_str(&json).unwrap();
assert_eq!(v, rt);
}
assert_eq!(
serde_json::to_string(&ReportingLine::AuditCommittee).unwrap(),
"\"audit_committee\""
);
}
#[test]
fn test_ia_assessment_serde() {
let variants = [
IaAssessment::FullyEffective,
IaAssessment::LargelyEffective,
IaAssessment::PartiallyEffective,
IaAssessment::Ineffective,
];
for v in variants {
let json = serde_json::to_string(&v).unwrap();
let rt: IaAssessment = serde_json::from_str(&json).unwrap();
assert_eq!(v, rt);
}
assert_eq!(
serde_json::to_string(&IaAssessment::FullyEffective).unwrap(),
"\"fully_effective\""
);
}
#[test]
fn test_reliance_extent_serde() {
let variants = [
RelianceExtent::NoReliance,
RelianceExtent::LimitedReliance,
RelianceExtent::SignificantReliance,
RelianceExtent::FullReliance,
];
for v in variants {
let json = serde_json::to_string(&v).unwrap();
let rt: RelianceExtent = serde_json::from_str(&json).unwrap();
assert_eq!(v, rt);
}
assert_eq!(
serde_json::to_string(&RelianceExtent::SignificantReliance).unwrap(),
"\"significant_reliance\""
);
}
#[test]
fn test_ia_report_status_serde() {
let variants = [
IaReportStatus::Draft,
IaReportStatus::Final,
IaReportStatus::Retracted,
];
for v in variants {
let json = serde_json::to_string(&v).unwrap();
let rt: IaReportStatus = serde_json::from_str(&json).unwrap();
assert_eq!(v, rt);
}
assert_eq!(
serde_json::to_string(&IaReportStatus::Final).unwrap(),
"\"final\""
);
}
#[test]
fn test_ia_report_rating_serde() {
let variants = [
IaReportRating::Satisfactory,
IaReportRating::NeedsImprovement,
IaReportRating::Unsatisfactory,
];
for v in variants {
let json = serde_json::to_string(&v).unwrap();
let rt: IaReportRating = serde_json::from_str(&json).unwrap();
assert_eq!(v, rt);
}
assert_eq!(
serde_json::to_string(&IaReportRating::NeedsImprovement).unwrap(),
"\"needs_improvement\""
);
}
#[test]
fn test_recommendation_priority_serde() {
let variants = [
RecommendationPriority::Critical,
RecommendationPriority::High,
RecommendationPriority::Medium,
RecommendationPriority::Low,
];
for v in variants {
let json = serde_json::to_string(&v).unwrap();
let rt: RecommendationPriority = serde_json::from_str(&json).unwrap();
assert_eq!(v, rt);
}
assert_eq!(
serde_json::to_string(&RecommendationPriority::Critical).unwrap(),
"\"critical\""
);
}
#[test]
fn test_action_plan_status_serde() {
let variants = [
ActionPlanStatus::Open,
ActionPlanStatus::InProgress,
ActionPlanStatus::Implemented,
ActionPlanStatus::Overdue,
];
for v in variants {
let json = serde_json::to_string(&v).unwrap();
let rt: ActionPlanStatus = serde_json::from_str(&json).unwrap();
assert_eq!(v, rt);
}
assert_eq!(
serde_json::to_string(&ActionPlanStatus::InProgress).unwrap(),
"\"in_progress\""
);
}
#[test]
fn test_ia_work_assessment_serde() {
let variants = [
IaWorkAssessment::Reliable,
IaWorkAssessment::PartiallyReliable,
IaWorkAssessment::Unreliable,
];
for v in variants {
let json = serde_json::to_string(&v).unwrap();
let rt: IaWorkAssessment = serde_json::from_str(&json).unwrap();
assert_eq!(v, rt);
}
assert_eq!(
serde_json::to_string(&IaWorkAssessment::PartiallyReliable).unwrap(),
"\"partially_reliable\""
);
}
}