use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditAssertion {
Occurrence,
Completeness,
Accuracy,
Cutoff,
Classification,
Existence,
RightsAndObligations,
CompletenessBalance,
ValuationAndAllocation,
PresentationAndDisclosure,
}
impl std::fmt::Display for AuditAssertion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Occurrence => "Occurrence",
Self::Completeness => "Completeness",
Self::Accuracy => "Accuracy",
Self::Cutoff => "Cutoff",
Self::Classification => "Classification",
Self::Existence => "Existence",
Self::RightsAndObligations => "Rights & Obligations",
Self::CompletenessBalance => "Completeness (Balance)",
Self::ValuationAndAllocation => "Valuation & Allocation",
Self::PresentationAndDisclosure => "Presentation & Disclosure",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RiskRating {
Low,
Medium,
High,
}
impl std::fmt::Display for RiskRating {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Low => "Low",
Self::Medium => "Medium",
Self::High => "High",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CraLevel {
Minimal,
Low,
Moderate,
High,
}
impl CraLevel {
pub fn from_ratings(ir: RiskRating, cr: RiskRating) -> Self {
match (ir, cr) {
(RiskRating::Low, RiskRating::Low) => Self::Minimal,
(RiskRating::Low, RiskRating::Medium) | (RiskRating::Medium, RiskRating::Low) => {
Self::Low
}
(RiskRating::Medium, RiskRating::Medium)
| (RiskRating::High, RiskRating::Low)
| (RiskRating::Low, RiskRating::High) => Self::Moderate,
_ => Self::High,
}
}
}
impl std::fmt::Display for CraLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Minimal => "Minimal",
Self::Low => "Low",
Self::Moderate => "Moderate",
Self::High => "High",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProcedureNature {
SubstantiveOnly,
Combined,
ControlsReliance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SamplingExtent {
Reduced,
Standard,
Extended,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProcedureTiming {
Interim,
YearEnd,
Both,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CraPlannedResponse {
pub nature: ProcedureNature,
pub extent: SamplingExtent,
pub timing: ProcedureTiming,
}
impl CraPlannedResponse {
pub fn from_cra_level(level: CraLevel) -> Self {
match level {
CraLevel::Minimal | CraLevel::Low => Self {
nature: ProcedureNature::SubstantiveOnly,
extent: SamplingExtent::Reduced,
timing: ProcedureTiming::YearEnd,
},
CraLevel::Moderate => Self {
nature: ProcedureNature::Combined,
extent: SamplingExtent::Standard,
timing: ProcedureTiming::YearEnd,
},
CraLevel::High => Self {
nature: ProcedureNature::SubstantiveOnly,
extent: SamplingExtent::Extended,
timing: ProcedureTiming::Both,
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombinedRiskAssessment {
pub id: String,
pub entity_code: String,
#[serde(default)]
pub scope_id: Option<String>,
pub account_area: String,
pub assertion: AuditAssertion,
pub inherent_risk: RiskRating,
pub control_risk: RiskRating,
pub combined_risk: CraLevel,
pub significant_risk: bool,
pub risk_factors: Vec<String>,
pub planned_response: CraPlannedResponse,
}
impl CombinedRiskAssessment {
pub fn new(
entity_code: &str,
account_area: &str,
assertion: AuditAssertion,
inherent_risk: RiskRating,
control_risk: RiskRating,
significant_risk: bool,
risk_factors: Vec<String>,
) -> Self {
let combined_risk = CraLevel::from_ratings(inherent_risk, control_risk);
let planned_response = CraPlannedResponse::from_cra_level(combined_risk);
let id = format!(
"CRA-{}-{}-{}",
entity_code,
account_area.replace(' ', "_").to_uppercase(),
format!("{assertion:?}").to_uppercase(),
);
Self {
id,
entity_code: entity_code.to_string(),
scope_id: None,
account_area: account_area.to_string(),
assertion,
inherent_risk,
control_risk,
combined_risk,
significant_risk,
risk_factors,
planned_response,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn cra_matrix_low_low_is_minimal() {
let level = CraLevel::from_ratings(RiskRating::Low, RiskRating::Low);
assert_eq!(level, CraLevel::Minimal);
}
#[test]
fn cra_matrix_high_high_is_high() {
let level = CraLevel::from_ratings(RiskRating::High, RiskRating::High);
assert_eq!(level, CraLevel::High);
}
#[test]
fn cra_matrix_medium_medium_is_moderate() {
let level = CraLevel::from_ratings(RiskRating::Medium, RiskRating::Medium);
assert_eq!(level, CraLevel::Moderate);
}
#[test]
fn cra_matrix_high_low_is_moderate() {
let level = CraLevel::from_ratings(RiskRating::High, RiskRating::Low);
assert_eq!(level, CraLevel::Moderate);
}
#[test]
fn cra_matrix_low_high_is_moderate() {
let level = CraLevel::from_ratings(RiskRating::Low, RiskRating::High);
assert_eq!(level, CraLevel::Moderate);
}
#[test]
fn cra_matrix_medium_high_is_high() {
let level = CraLevel::from_ratings(RiskRating::Medium, RiskRating::High);
assert_eq!(level, CraLevel::High);
}
#[test]
fn planned_response_high_cra_is_extended_both() {
let resp = CraPlannedResponse::from_cra_level(CraLevel::High);
assert_eq!(resp.extent, SamplingExtent::Extended);
assert_eq!(resp.timing, ProcedureTiming::Both);
assert_eq!(resp.nature, ProcedureNature::SubstantiveOnly);
}
#[test]
fn planned_response_moderate_cra_is_combined_standard() {
let resp = CraPlannedResponse::from_cra_level(CraLevel::Moderate);
assert_eq!(resp.nature, ProcedureNature::Combined);
assert_eq!(resp.extent, SamplingExtent::Standard);
}
#[test]
fn cra_new_derives_combined_risk() {
let cra = CombinedRiskAssessment::new(
"C001",
"Revenue",
AuditAssertion::Occurrence,
RiskRating::High,
RiskRating::Medium,
true,
vec!["Presumed fraud risk per ISA 240".into()],
);
assert_eq!(cra.combined_risk, CraLevel::High);
assert!(cra.significant_risk);
}
}