use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum IsaStandard {
Isa200,
Isa210,
Isa220,
Isa230,
Isa240,
Isa250,
Isa260,
Isa265,
Isa300,
Isa315,
Isa320,
Isa330,
Isa402,
Isa450,
Isa500,
Isa501,
Isa505,
Isa510,
Isa520,
Isa530,
Isa540,
Isa550,
Isa560,
Isa570,
Isa580,
Isa600,
Isa610,
Isa620,
Isa700,
Isa701,
Isa705,
Isa706,
Isa710,
Isa720,
}
impl IsaStandard {
pub fn number(&self) -> &'static str {
match self {
Self::Isa200 => "200",
Self::Isa210 => "210",
Self::Isa220 => "220",
Self::Isa230 => "230",
Self::Isa240 => "240",
Self::Isa250 => "250",
Self::Isa260 => "260",
Self::Isa265 => "265",
Self::Isa300 => "300",
Self::Isa315 => "315",
Self::Isa320 => "320",
Self::Isa330 => "330",
Self::Isa402 => "402",
Self::Isa450 => "450",
Self::Isa500 => "500",
Self::Isa501 => "501",
Self::Isa505 => "505",
Self::Isa510 => "510",
Self::Isa520 => "520",
Self::Isa530 => "530",
Self::Isa540 => "540",
Self::Isa550 => "550",
Self::Isa560 => "560",
Self::Isa570 => "570",
Self::Isa580 => "580",
Self::Isa600 => "600",
Self::Isa610 => "610",
Self::Isa620 => "620",
Self::Isa700 => "700",
Self::Isa701 => "701",
Self::Isa705 => "705",
Self::Isa706 => "706",
Self::Isa710 => "710",
Self::Isa720 => "720",
}
}
pub fn title(&self) -> &'static str {
match self {
Self::Isa200 => "Overall Objectives of the Independent Auditor",
Self::Isa210 => "Agreeing the Terms of Audit Engagements",
Self::Isa220 => "Quality Management for an Audit of Financial Statements",
Self::Isa230 => "Audit Documentation",
Self::Isa240 => "The Auditor's Responsibilities Relating to Fraud",
Self::Isa250 => "Consideration of Laws and Regulations",
Self::Isa260 => "Communication with Those Charged with Governance",
Self::Isa265 => "Communicating Deficiencies in Internal Control",
Self::Isa300 => "Planning an Audit of Financial Statements",
Self::Isa315 => "Identifying and Assessing Risks of Material Misstatement",
Self::Isa320 => "Materiality in Planning and Performing an Audit",
Self::Isa330 => "The Auditor's Responses to Assessed Risks",
Self::Isa402 => "Audit Considerations Relating to Service Organizations",
Self::Isa450 => "Evaluation of Misstatements Identified During the Audit",
Self::Isa500 => "Audit Evidence",
Self::Isa501 => "Audit Evidence - Specific Considerations",
Self::Isa505 => "External Confirmations",
Self::Isa510 => "Initial Audit Engagements - Opening Balances",
Self::Isa520 => "Analytical Procedures",
Self::Isa530 => "Audit Sampling",
Self::Isa540 => "Auditing Accounting Estimates and Related Disclosures",
Self::Isa550 => "Related Parties",
Self::Isa560 => "Subsequent Events",
Self::Isa570 => "Going Concern",
Self::Isa580 => "Written Representations",
Self::Isa600 => "Special Considerations - Audits of Group Financial Statements",
Self::Isa610 => "Using the Work of Internal Auditors",
Self::Isa620 => "Using the Work of an Auditor's Expert",
Self::Isa700 => "Forming an Opinion and Reporting on Financial Statements",
Self::Isa701 => "Communicating Key Audit Matters",
Self::Isa705 => "Modifications to the Opinion",
Self::Isa706 => "Emphasis of Matter and Other Matter Paragraphs",
Self::Isa710 => "Comparative Information",
Self::Isa720 => "The Auditor's Responsibilities Relating to Other Information",
}
}
pub fn series(&self) -> IsaSeries {
match self {
Self::Isa200
| Self::Isa210
| Self::Isa220
| Self::Isa230
| Self::Isa240
| Self::Isa250
| Self::Isa260
| Self::Isa265 => IsaSeries::GeneralPrinciples,
Self::Isa300 | Self::Isa315 | Self::Isa320 | Self::Isa330 => IsaSeries::RiskAssessment,
Self::Isa402 | Self::Isa450 => IsaSeries::InternalControl,
Self::Isa500
| Self::Isa501
| Self::Isa505
| Self::Isa510
| Self::Isa520
| Self::Isa530
| Self::Isa540
| Self::Isa550
| Self::Isa560
| Self::Isa570
| Self::Isa580 => IsaSeries::AuditEvidence,
Self::Isa600 | Self::Isa610 | Self::Isa620 => IsaSeries::UsingWorkOfOthers,
Self::Isa700
| Self::Isa701
| Self::Isa705
| Self::Isa706
| Self::Isa710
| Self::Isa720 => IsaSeries::Reporting,
}
}
pub fn all() -> Vec<Self> {
vec![
Self::Isa200,
Self::Isa210,
Self::Isa220,
Self::Isa230,
Self::Isa240,
Self::Isa250,
Self::Isa260,
Self::Isa265,
Self::Isa300,
Self::Isa315,
Self::Isa320,
Self::Isa330,
Self::Isa402,
Self::Isa450,
Self::Isa500,
Self::Isa501,
Self::Isa505,
Self::Isa510,
Self::Isa520,
Self::Isa530,
Self::Isa540,
Self::Isa550,
Self::Isa560,
Self::Isa570,
Self::Isa580,
Self::Isa600,
Self::Isa610,
Self::Isa620,
Self::Isa700,
Self::Isa701,
Self::Isa705,
Self::Isa706,
Self::Isa710,
Self::Isa720,
]
}
}
impl std::fmt::Display for IsaStandard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ISA {}", self.number())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IsaSeries {
GeneralPrinciples,
RiskAssessment,
InternalControl,
AuditEvidence,
UsingWorkOfOthers,
Reporting,
}
impl std::fmt::Display for IsaSeries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::GeneralPrinciples => write!(f, "General Principles and Responsibilities"),
Self::RiskAssessment => write!(f, "Risk Assessment and Response"),
Self::InternalControl => write!(f, "Internal Control"),
Self::AuditEvidence => write!(f, "Audit Evidence"),
Self::UsingWorkOfOthers => write!(f, "Using Work of Others"),
Self::Reporting => write!(f, "Audit Conclusions and Reporting"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IsaRequirement {
pub standard: IsaStandard,
pub paragraph: String,
pub requirement_type: IsaRequirementType,
pub description: String,
pub is_mandatory: bool,
}
impl IsaRequirement {
pub fn new(
standard: IsaStandard,
paragraph: impl Into<String>,
requirement_type: IsaRequirementType,
description: impl Into<String>,
) -> Self {
let is_mandatory = matches!(requirement_type, IsaRequirementType::Requirement);
Self {
standard,
paragraph: paragraph.into(),
requirement_type,
description: description.into(),
is_mandatory,
}
}
pub fn reference(&self) -> String {
format!("ISA {}.{}", self.standard.number(), self.paragraph)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IsaRequirementType {
Objective,
Requirement,
ApplicationGuidance,
Definition,
}
impl std::fmt::Display for IsaRequirementType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Objective => write!(f, "Objective"),
Self::Requirement => write!(f, "Requirement"),
Self::ApplicationGuidance => write!(f, "Application Guidance"),
Self::Definition => write!(f, "Definition"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IsaProcedureMapping {
pub mapping_id: Uuid,
pub procedure_id: Uuid,
pub procedure_description: String,
pub isa_requirements: Vec<IsaRequirement>,
pub compliance_status: ComplianceStatus,
pub documentation_reference: Option<String>,
pub compliance_notes: String,
}
impl IsaProcedureMapping {
pub fn new(procedure_id: Uuid, procedure_description: impl Into<String>) -> Self {
Self {
mapping_id: Uuid::now_v7(),
procedure_id,
procedure_description: procedure_description.into(),
isa_requirements: Vec::new(),
compliance_status: ComplianceStatus::NotAssessed,
documentation_reference: None,
compliance_notes: String::new(),
}
}
pub fn add_requirement(&mut self, requirement: IsaRequirement) {
self.isa_requirements.push(requirement);
}
pub fn standards_covered(&self) -> Vec<IsaStandard> {
let mut standards: Vec<_> = self.isa_requirements.iter().map(|r| r.standard).collect();
standards.sort_by_key(IsaStandard::number);
standards.dedup();
standards
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ComplianceStatus {
#[default]
NotAssessed,
Compliant,
PartiallyCompliant,
NonCompliant,
NotApplicable,
}
impl std::fmt::Display for ComplianceStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotAssessed => write!(f, "Not Assessed"),
Self::Compliant => write!(f, "Compliant"),
Self::PartiallyCompliant => write!(f, "Partially Compliant"),
Self::NonCompliant => write!(f, "Non-Compliant"),
Self::NotApplicable => write!(f, "Not Applicable"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IsaCoverageSummary {
pub engagement_id: Uuid,
pub standards_addressed: Vec<IsaStandard>,
pub coverage_by_series: Vec<SeriesCoverage>,
pub overall_compliance_percent: f64,
pub gaps: Vec<IsaStandard>,
pub unaddressed_mandatory: Vec<IsaRequirement>,
}
impl IsaCoverageSummary {
pub fn new(engagement_id: Uuid) -> Self {
Self {
engagement_id,
standards_addressed: Vec::new(),
coverage_by_series: Vec::new(),
overall_compliance_percent: 0.0,
gaps: Vec::new(),
unaddressed_mandatory: Vec::new(),
}
}
pub fn calculate_coverage(&mut self, mappings: &[IsaProcedureMapping]) {
self.standards_addressed = mappings
.iter()
.flat_map(IsaProcedureMapping::standards_covered)
.collect();
self.standards_addressed.sort_by_key(IsaStandard::number);
self.standards_addressed.dedup();
self.gaps = IsaStandard::all()
.into_iter()
.filter(|s| !self.standards_addressed.contains(s))
.collect();
let total = IsaStandard::all().len();
let covered = self.standards_addressed.len();
self.overall_compliance_percent = (covered as f64 / total as f64) * 100.0;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SeriesCoverage {
pub series: IsaSeries,
pub total_standards: usize,
pub addressed_standards: usize,
pub coverage_percent: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IsaStandardEntry {
pub number: String,
pub title: String,
pub series: String,
pub display_name: String,
}
impl IsaStandardEntry {
pub fn from_standard(standard: IsaStandard) -> Self {
Self {
number: standard.number().to_string(),
title: standard.title().to_string(),
series: standard.series().to_string(),
display_name: standard.to_string(),
}
}
}
impl IsaStandard {
pub fn standard_entries() -> Vec<IsaStandardEntry> {
Self::all()
.into_iter()
.map(IsaStandardEntry::from_standard)
.collect()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_isa_standard_number() {
assert_eq!(IsaStandard::Isa315.number(), "315");
assert_eq!(IsaStandard::Isa700.number(), "700");
}
#[test]
fn test_isa_standard_title() {
assert_eq!(
IsaStandard::Isa315.title(),
"Identifying and Assessing Risks of Material Misstatement"
);
}
#[test]
fn test_isa_standard_series() {
assert_eq!(IsaStandard::Isa200.series(), IsaSeries::GeneralPrinciples);
assert_eq!(IsaStandard::Isa315.series(), IsaSeries::RiskAssessment);
assert_eq!(IsaStandard::Isa500.series(), IsaSeries::AuditEvidence);
assert_eq!(IsaStandard::Isa700.series(), IsaSeries::Reporting);
}
#[test]
fn test_isa_requirement_reference() {
let req = IsaRequirement::new(
IsaStandard::Isa315,
"25",
IsaRequirementType::Requirement,
"Identify and assess risks of material misstatement",
);
assert_eq!(req.reference(), "ISA 315.25");
assert!(req.is_mandatory);
}
#[test]
fn test_procedure_mapping() {
let mut mapping =
IsaProcedureMapping::new(Uuid::now_v7(), "Test risk assessment procedures");
mapping.add_requirement(IsaRequirement::new(
IsaStandard::Isa315,
"25",
IsaRequirementType::Requirement,
"Risk assessment",
));
mapping.add_requirement(IsaRequirement::new(
IsaStandard::Isa330,
"5",
IsaRequirementType::Requirement,
"Audit responses",
));
let standards = mapping.standards_covered();
assert_eq!(standards.len(), 2);
assert!(standards.contains(&IsaStandard::Isa315));
assert!(standards.contains(&IsaStandard::Isa330));
}
#[test]
fn test_all_standards() {
let all = IsaStandard::all();
assert_eq!(all.len(), 34); }
}