datasynth_core/models/audit/risk_assessment_cra.rs
1//! Combined Risk Assessment (CRA) models per ISA 315.
2//!
3//! The CRA is the cornerstone of a risk-based audit. For each account area and
4//! financial statement assertion the auditor combines inherent risk (IR) and
5//! control risk (CR) into a single CRA level that drives the nature, extent,
6//! and timing of planned audit procedures.
7//!
8//! References:
9//! - ISA 315 (Revised 2019) — Identifying and Assessing Risks of Material Misstatement
10//! - ISA 315.28 — Significant risks require special audit consideration
11//! - ISA 240 — Revenue occurrence is always presumed a significant fraud risk
12
13use serde::{Deserialize, Serialize};
14
15// ---------------------------------------------------------------------------
16// Core enums
17// ---------------------------------------------------------------------------
18
19/// Financial statement assertion being assessed (ISA 315.A129).
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum AuditAssertion {
23 // ---- Transaction-level assertions (ISA 315.A129a) ----
24 /// Recorded transactions and events have occurred and pertain to the entity.
25 Occurrence,
26 /// All transactions and events that should have been recorded have been.
27 Completeness,
28 /// Amounts and other data have been recorded appropriately.
29 Accuracy,
30 /// Transactions have been recorded in the correct accounting period.
31 Cutoff,
32 /// Transactions have been recorded in the proper accounts.
33 Classification,
34 // ---- Account-balance assertions (ISA 315.A129b) ----
35 /// Assets, liabilities and equity interests exist at the period end.
36 Existence,
37 /// The entity holds or controls the rights to assets; liabilities are obligations.
38 RightsAndObligations,
39 /// All assets, liabilities and equity interests that should have been recorded are.
40 CompletenessBalance,
41 /// Assets, liabilities and equity interests are included at appropriate amounts.
42 ValuationAndAllocation,
43 // ---- Presentation assertions (ISA 315.A129c) ----
44 /// All disclosures are appropriately described, disclosed and presented.
45 PresentationAndDisclosure,
46}
47
48impl std::fmt::Display for AuditAssertion {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 let s = match self {
51 Self::Occurrence => "Occurrence",
52 Self::Completeness => "Completeness",
53 Self::Accuracy => "Accuracy",
54 Self::Cutoff => "Cutoff",
55 Self::Classification => "Classification",
56 Self::Existence => "Existence",
57 Self::RightsAndObligations => "Rights & Obligations",
58 Self::CompletenessBalance => "Completeness (Balance)",
59 Self::ValuationAndAllocation => "Valuation & Allocation",
60 Self::PresentationAndDisclosure => "Presentation & Disclosure",
61 };
62 write!(f, "{s}")
63 }
64}
65
66/// Individual risk rating — used separately for inherent risk and control risk.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum RiskRating {
70 /// Well below the acceptable threshold.
71 Low,
72 /// Moderate — some risk present but not pervasive.
73 Medium,
74 /// Elevated — significant susceptibility to misstatement.
75 High,
76}
77
78impl std::fmt::Display for RiskRating {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 let s = match self {
81 Self::Low => "Low",
82 Self::Medium => "Medium",
83 Self::High => "High",
84 };
85 write!(f, "{s}")
86 }
87}
88
89/// Combined Risk Assessment level — derived from the IR × CR matrix.
90///
91/// | IR \ CR | Low | Medium | High |
92/// |----------|----------|----------|-------|
93/// | Low | Minimal | Low | Moderate |
94/// | Medium | Low | Moderate | High |
95/// | High | Moderate | High | High |
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum CraLevel {
99 /// Low IR + Low CR.
100 Minimal,
101 /// Low IR + Medium CR, or Medium IR + Low CR.
102 Low,
103 /// Medium IR + Medium CR, or High IR + Low CR.
104 Moderate,
105 /// High IR + Medium CR, or High IR + High CR, or Medium IR + High CR.
106 High,
107}
108
109impl CraLevel {
110 /// Compute the CRA level from individual IR and CR ratings.
111 ///
112 /// | IR \ CR | Low | Medium | High |
113 /// |----------|---------|----------|----------|
114 /// | Low | Minimal | Low | Moderate |
115 /// | Medium | Low | Moderate | High |
116 /// | High | Moderate| High | High |
117 pub fn from_ratings(ir: RiskRating, cr: RiskRating) -> Self {
118 match (ir, cr) {
119 (RiskRating::Low, RiskRating::Low) => Self::Minimal,
120 (RiskRating::Low, RiskRating::Medium) | (RiskRating::Medium, RiskRating::Low) => {
121 Self::Low
122 }
123 (RiskRating::Medium, RiskRating::Medium)
124 | (RiskRating::High, RiskRating::Low)
125 | (RiskRating::Low, RiskRating::High) => Self::Moderate,
126 _ => Self::High,
127 }
128 }
129}
130
131impl std::fmt::Display for CraLevel {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 let s = match self {
134 Self::Minimal => "Minimal",
135 Self::Low => "Low",
136 Self::Moderate => "Moderate",
137 Self::High => "High",
138 };
139 write!(f, "{s}")
140 }
141}
142
143// ---------------------------------------------------------------------------
144// Planned response
145// ---------------------------------------------------------------------------
146
147/// Nature of planned audit procedures in response to the CRA.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150pub enum ProcedureNature {
151 /// Substantive procedures only — no reliance on controls.
152 SubstantiveOnly,
153 /// Combined approach — tests of controls plus reduced substantive procedures.
154 Combined,
155 /// Controls reliance — extensive controls testing with minimal substantive.
156 ControlsReliance,
157}
158
159/// Planned extent of substantive testing (maps to relative sample sizes).
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
161#[serde(rename_all = "snake_case")]
162pub enum SamplingExtent {
163 /// Below-standard sample sizes applicable to low-risk areas.
164 Reduced,
165 /// Standard sample sizes.
166 Standard,
167 /// Above-standard sample sizes for high-risk or significant-risk areas.
168 Extended,
169}
170
171/// Timing of planned procedures relative to the period end.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
173#[serde(rename_all = "snake_case")]
174pub enum ProcedureTiming {
175 /// Procedures performed at an interim date only.
176 Interim,
177 /// Procedures performed at or after the period end only.
178 YearEnd,
179 /// Procedures at both interim and year-end (roll-forward required).
180 Both,
181}
182
183/// Planned response design driven by the CRA level.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct CraPlannedResponse {
186 /// Nature: substantive only, combined, or controls reliance.
187 pub nature: ProcedureNature,
188 /// Extent: reduced, standard, or extended.
189 pub extent: SamplingExtent,
190 /// Timing: interim, year-end, or both.
191 pub timing: ProcedureTiming,
192}
193
194impl CraPlannedResponse {
195 /// Derive a planned response from the CRA level per ISA 330 guidance.
196 ///
197 /// | CRA level | Nature | Extent | Timing |
198 /// |-----------|------------------|----------|----------|
199 /// | Minimal | SubstantiveOnly | Reduced | YearEnd |
200 /// | Low | SubstantiveOnly | Reduced | YearEnd |
201 /// | Moderate | Combined | Standard | YearEnd |
202 /// | High | SubstantiveOnly | Extended | Both |
203 pub fn from_cra_level(level: CraLevel) -> Self {
204 match level {
205 CraLevel::Minimal | CraLevel::Low => Self {
206 nature: ProcedureNature::SubstantiveOnly,
207 extent: SamplingExtent::Reduced,
208 timing: ProcedureTiming::YearEnd,
209 },
210 CraLevel::Moderate => Self {
211 nature: ProcedureNature::Combined,
212 extent: SamplingExtent::Standard,
213 timing: ProcedureTiming::YearEnd,
214 },
215 CraLevel::High => Self {
216 nature: ProcedureNature::SubstantiveOnly,
217 extent: SamplingExtent::Extended,
218 timing: ProcedureTiming::Both,
219 },
220 }
221 }
222}
223
224// ---------------------------------------------------------------------------
225// Main struct
226// ---------------------------------------------------------------------------
227
228/// Combined Risk Assessment for a single account area / assertion pair.
229///
230/// One `CombinedRiskAssessment` is generated for each (account area, assertion)
231/// combination that the auditor scopes into the engagement. The CRA drives the
232/// design of audit procedures (ISA 330) and the allocation of materiality.
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct CombinedRiskAssessment {
235 /// Unique identifier (deterministic slug).
236 pub id: String,
237 /// Entity / company code the assessment belongs to.
238 pub entity_code: String,
239 /// Audit scope this assessment belongs to (FK → AuditScope.id).
240 #[serde(default)]
241 pub scope_id: Option<String>,
242 /// Account area (e.g. "Revenue", "Trade Receivables", "Inventory").
243 pub account_area: String,
244 /// The specific assertion being assessed.
245 pub assertion: AuditAssertion,
246 /// Inherent risk rating.
247 pub inherent_risk: RiskRating,
248 /// Control risk rating (effectiveness of related internal controls).
249 pub control_risk: RiskRating,
250 /// Combined risk level derived from the IR × CR matrix.
251 pub combined_risk: CraLevel,
252 /// Whether this is a significant risk per ISA 315.28 requiring special consideration.
253 pub significant_risk: bool,
254 /// Descriptive risk factors supporting the assessment.
255 pub risk_factors: Vec<String>,
256 /// Planned audit response designed for this CRA.
257 pub planned_response: CraPlannedResponse,
258}
259
260impl CombinedRiskAssessment {
261 /// Build a `CombinedRiskAssessment` and derive the CRA level and response.
262 pub fn new(
263 entity_code: &str,
264 account_area: &str,
265 assertion: AuditAssertion,
266 inherent_risk: RiskRating,
267 control_risk: RiskRating,
268 significant_risk: bool,
269 risk_factors: Vec<String>,
270 ) -> Self {
271 let combined_risk = CraLevel::from_ratings(inherent_risk, control_risk);
272 let planned_response = CraPlannedResponse::from_cra_level(combined_risk);
273 let id = format!(
274 "CRA-{}-{}-{}",
275 entity_code,
276 account_area.replace(' ', "_").to_uppercase(),
277 format!("{assertion:?}").to_uppercase(),
278 );
279
280 Self {
281 id,
282 entity_code: entity_code.to_string(),
283 scope_id: None,
284 account_area: account_area.to_string(),
285 assertion,
286 inherent_risk,
287 control_risk,
288 combined_risk,
289 significant_risk,
290 risk_factors,
291 planned_response,
292 }
293 }
294}
295
296// ---------------------------------------------------------------------------
297// Tests
298// ---------------------------------------------------------------------------
299
300#[cfg(test)]
301#[allow(clippy::unwrap_used)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn cra_matrix_low_low_is_minimal() {
307 let level = CraLevel::from_ratings(RiskRating::Low, RiskRating::Low);
308 assert_eq!(level, CraLevel::Minimal);
309 }
310
311 #[test]
312 fn cra_matrix_high_high_is_high() {
313 let level = CraLevel::from_ratings(RiskRating::High, RiskRating::High);
314 assert_eq!(level, CraLevel::High);
315 }
316
317 #[test]
318 fn cra_matrix_medium_medium_is_moderate() {
319 let level = CraLevel::from_ratings(RiskRating::Medium, RiskRating::Medium);
320 assert_eq!(level, CraLevel::Moderate);
321 }
322
323 #[test]
324 fn cra_matrix_high_low_is_moderate() {
325 let level = CraLevel::from_ratings(RiskRating::High, RiskRating::Low);
326 assert_eq!(level, CraLevel::Moderate);
327 }
328
329 #[test]
330 fn cra_matrix_low_high_is_moderate() {
331 // Low IR caps the combined level even when controls provide no assurance.
332 let level = CraLevel::from_ratings(RiskRating::Low, RiskRating::High);
333 assert_eq!(level, CraLevel::Moderate);
334 }
335
336 #[test]
337 fn cra_matrix_medium_high_is_high() {
338 let level = CraLevel::from_ratings(RiskRating::Medium, RiskRating::High);
339 assert_eq!(level, CraLevel::High);
340 }
341
342 #[test]
343 fn planned_response_high_cra_is_extended_both() {
344 let resp = CraPlannedResponse::from_cra_level(CraLevel::High);
345 assert_eq!(resp.extent, SamplingExtent::Extended);
346 assert_eq!(resp.timing, ProcedureTiming::Both);
347 assert_eq!(resp.nature, ProcedureNature::SubstantiveOnly);
348 }
349
350 #[test]
351 fn planned_response_moderate_cra_is_combined_standard() {
352 let resp = CraPlannedResponse::from_cra_level(CraLevel::Moderate);
353 assert_eq!(resp.nature, ProcedureNature::Combined);
354 assert_eq!(resp.extent, SamplingExtent::Standard);
355 }
356
357 #[test]
358 fn cra_new_derives_combined_risk() {
359 let cra = CombinedRiskAssessment::new(
360 "C001",
361 "Revenue",
362 AuditAssertion::Occurrence,
363 RiskRating::High,
364 RiskRating::Medium,
365 true,
366 vec!["Presumed fraud risk per ISA 240".into()],
367 );
368 assert_eq!(cra.combined_risk, CraLevel::High);
369 assert!(cra.significant_risk);
370 }
371}