datasynth_core/models/audit/
risk.rs

1//! Risk assessment models per ISA 315 and ISA 330.
2//!
3//! Risk assessment is the foundation of a risk-based audit approach,
4//! identifying risks of material misstatement at both the financial
5//! statement level and assertion level.
6
7use chrono::{DateTime, NaiveDate, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use super::engagement::RiskLevel;
12use super::workpaper::Assertion;
13
14/// Risk assessment for an account or process.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct RiskAssessment {
17    /// Unique risk ID
18    pub risk_id: Uuid,
19    /// External reference
20    pub risk_ref: String,
21    /// Engagement ID
22    pub engagement_id: Uuid,
23    /// Risk category
24    pub risk_category: RiskCategory,
25    /// Account or process being assessed
26    pub account_or_process: String,
27    /// Specific assertion if applicable
28    pub assertion: Option<Assertion>,
29    /// Risk description
30    pub description: String,
31
32    // === Risk Assessment ===
33    /// Inherent risk assessment
34    pub inherent_risk: RiskLevel,
35    /// Control risk assessment
36    pub control_risk: RiskLevel,
37    /// Combined risk of material misstatement
38    pub risk_of_material_misstatement: RiskLevel,
39    /// Is this a significant risk per ISA 315?
40    pub is_significant_risk: bool,
41    /// Rationale for significant risk designation
42    pub significant_risk_rationale: Option<String>,
43
44    // === Fraud Risk ===
45    /// Fraud risk factors identified
46    pub fraud_risk_factors: Vec<FraudRiskFactor>,
47    /// Presumed fraud risk in revenue recognition?
48    pub presumed_revenue_fraud_risk: bool,
49    /// Presumed management override risk?
50    pub presumed_management_override: bool,
51
52    // === Response ===
53    /// Planned audit response
54    pub planned_response: Vec<PlannedResponse>,
55    /// Nature of procedures (substantive, control, combined)
56    pub response_nature: ResponseNature,
57    /// Extent (sample size considerations)
58    pub response_extent: String,
59    /// Timing (interim, year-end, subsequent)
60    pub response_timing: ResponseTiming,
61
62    // === Assessment Details ===
63    /// Assessed by user ID
64    pub assessed_by: String,
65    /// Assessment date
66    pub assessed_date: NaiveDate,
67    /// Review status
68    pub review_status: RiskReviewStatus,
69    /// Reviewer ID
70    pub reviewer_id: Option<String>,
71    /// Review date
72    pub review_date: Option<NaiveDate>,
73
74    // === Cross-References ===
75    /// Related workpaper IDs
76    pub workpaper_refs: Vec<Uuid>,
77    /// Related control IDs
78    pub related_controls: Vec<String>,
79
80    pub created_at: DateTime<Utc>,
81    pub updated_at: DateTime<Utc>,
82}
83
84impl RiskAssessment {
85    /// Create a new risk assessment.
86    pub fn new(
87        engagement_id: Uuid,
88        risk_category: RiskCategory,
89        account_or_process: &str,
90        description: &str,
91    ) -> Self {
92        let now = Utc::now();
93        Self {
94            risk_id: Uuid::new_v4(),
95            risk_ref: format!(
96                "RISK-{}",
97                Uuid::new_v4().simple().to_string()[..8].to_uppercase()
98            ),
99            engagement_id,
100            risk_category,
101            account_or_process: account_or_process.into(),
102            assertion: None,
103            description: description.into(),
104            inherent_risk: RiskLevel::Medium,
105            control_risk: RiskLevel::Medium,
106            risk_of_material_misstatement: RiskLevel::Medium,
107            is_significant_risk: false,
108            significant_risk_rationale: None,
109            fraud_risk_factors: Vec::new(),
110            presumed_revenue_fraud_risk: false,
111            presumed_management_override: true,
112            planned_response: Vec::new(),
113            response_nature: ResponseNature::Combined,
114            response_extent: String::new(),
115            response_timing: ResponseTiming::YearEnd,
116            assessed_by: String::new(),
117            assessed_date: now.date_naive(),
118            review_status: RiskReviewStatus::Draft,
119            reviewer_id: None,
120            review_date: None,
121            workpaper_refs: Vec::new(),
122            related_controls: Vec::new(),
123            created_at: now,
124            updated_at: now,
125        }
126    }
127
128    /// Set the assertion being assessed.
129    pub fn with_assertion(mut self, assertion: Assertion) -> Self {
130        self.assertion = Some(assertion);
131        self
132    }
133
134    /// Set risk levels.
135    pub fn with_risk_levels(mut self, inherent: RiskLevel, control: RiskLevel) -> Self {
136        self.inherent_risk = inherent;
137        self.control_risk = control;
138        self.risk_of_material_misstatement = self.calculate_romm();
139        self
140    }
141
142    /// Mark as significant risk.
143    pub fn mark_significant(mut self, rationale: &str) -> Self {
144        self.is_significant_risk = true;
145        self.significant_risk_rationale = Some(rationale.into());
146        self
147    }
148
149    /// Add a fraud risk factor.
150    pub fn add_fraud_factor(&mut self, factor: FraudRiskFactor) {
151        self.fraud_risk_factors.push(factor);
152        self.updated_at = Utc::now();
153    }
154
155    /// Add a planned response.
156    pub fn add_response(&mut self, response: PlannedResponse) {
157        self.planned_response.push(response);
158        self.updated_at = Utc::now();
159    }
160
161    /// Set who assessed this risk.
162    pub fn with_assessed_by(mut self, user_id: &str, date: NaiveDate) -> Self {
163        self.assessed_by = user_id.into();
164        self.assessed_date = date;
165        self
166    }
167
168    /// Calculate risk of material misstatement from IR and CR.
169    fn calculate_romm(&self) -> RiskLevel {
170        let ir_score = self.inherent_risk.score();
171        let cr_score = self.control_risk.score();
172        let combined = (ir_score + cr_score) / 2;
173        RiskLevel::from_score(combined)
174    }
175
176    /// Get the detection risk needed to achieve acceptable audit risk.
177    pub fn required_detection_risk(&self) -> DetectionRisk {
178        match self.risk_of_material_misstatement {
179            RiskLevel::Low => DetectionRisk::High,
180            RiskLevel::Medium => DetectionRisk::Medium,
181            RiskLevel::High | RiskLevel::Significant => DetectionRisk::Low,
182        }
183    }
184
185    /// Check if this risk requires special audit consideration.
186    pub fn requires_special_consideration(&self) -> bool {
187        self.is_significant_risk
188            || matches!(
189                self.risk_of_material_misstatement,
190                RiskLevel::High | RiskLevel::Significant
191            )
192            || !self.fraud_risk_factors.is_empty()
193    }
194}
195
196/// Risk category per ISA 315.
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
198#[serde(rename_all = "snake_case")]
199pub enum RiskCategory {
200    /// Risk at the financial statement level
201    FinancialStatementLevel,
202    /// Risk at the assertion level
203    #[default]
204    AssertionLevel,
205    /// Fraud risk
206    FraudRisk,
207    /// Going concern risk
208    GoingConcern,
209    /// Related party risk
210    RelatedParty,
211    /// Accounting estimate risk
212    EstimateRisk,
213    /// IT general control risk
214    ItGeneralControl,
215    /// Regulatory compliance risk
216    RegulatoryCompliance,
217}
218
219/// Fraud risk factor per the fraud triangle.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct FraudRiskFactor {
222    /// Factor ID
223    pub factor_id: Uuid,
224    /// Element of fraud triangle
225    pub factor_type: FraudTriangleElement,
226    /// Specific indicator description
227    pub indicator: String,
228    /// Risk score (0-100)
229    pub score: u8,
230    /// Trend direction
231    pub trend: Trend,
232    /// Source of information
233    pub source: String,
234    /// Date identified
235    pub identified_date: NaiveDate,
236}
237
238impl FraudRiskFactor {
239    /// Create a new fraud risk factor.
240    pub fn new(
241        factor_type: FraudTriangleElement,
242        indicator: &str,
243        score: u8,
244        source: &str,
245    ) -> Self {
246        Self {
247            factor_id: Uuid::new_v4(),
248            factor_type,
249            indicator: indicator.into(),
250            score: score.min(100),
251            trend: Trend::Stable,
252            source: source.into(),
253            identified_date: Utc::now().date_naive(),
254        }
255    }
256
257    /// Set the trend.
258    pub fn with_trend(mut self, trend: Trend) -> Self {
259        self.trend = trend;
260        self
261    }
262}
263
264/// Elements of the fraud triangle.
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
266#[serde(rename_all = "snake_case")]
267pub enum FraudTriangleElement {
268    /// Opportunity to commit fraud
269    Opportunity,
270    /// Incentive/pressure to commit fraud
271    Pressure,
272    /// Rationalization/attitude
273    Rationalization,
274}
275
276impl FraudTriangleElement {
277    /// Get a description.
278    pub fn description(&self) -> &'static str {
279        match self {
280            Self::Opportunity => "Circumstances providing opportunity to commit fraud",
281            Self::Pressure => "Incentives or pressures to commit fraud",
282            Self::Rationalization => "Attitude or rationalization to justify fraud",
283        }
284    }
285}
286
287/// Trend direction.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
289#[serde(rename_all = "snake_case")]
290pub enum Trend {
291    /// Increasing
292    Increasing,
293    /// Stable
294    #[default]
295    Stable,
296    /// Decreasing
297    Decreasing,
298}
299
300/// Planned audit response to identified risk.
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct PlannedResponse {
303    /// Response ID
304    pub response_id: Uuid,
305    /// Procedure description
306    pub procedure: String,
307    /// Procedure type
308    pub procedure_type: ResponseProcedureType,
309    /// Assertion addressed
310    pub assertion_addressed: Assertion,
311    /// Assigned to user ID
312    pub assigned_to: String,
313    /// Target completion date
314    pub target_date: NaiveDate,
315    /// Status
316    pub status: ResponseStatus,
317    /// Workpaper reference when complete
318    pub workpaper_ref: Option<Uuid>,
319}
320
321impl PlannedResponse {
322    /// Create a new planned response.
323    pub fn new(
324        procedure: &str,
325        procedure_type: ResponseProcedureType,
326        assertion: Assertion,
327        assigned_to: &str,
328        target_date: NaiveDate,
329    ) -> Self {
330        Self {
331            response_id: Uuid::new_v4(),
332            procedure: procedure.into(),
333            procedure_type,
334            assertion_addressed: assertion,
335            assigned_to: assigned_to.into(),
336            target_date,
337            status: ResponseStatus::Planned,
338            workpaper_ref: None,
339        }
340    }
341}
342
343/// Type of response procedure.
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
345#[serde(rename_all = "snake_case")]
346pub enum ResponseProcedureType {
347    /// Test of controls
348    TestOfControls,
349    /// Substantive analytical procedure
350    AnalyticalProcedure,
351    /// Substantive test of details
352    #[default]
353    TestOfDetails,
354    /// External confirmation
355    Confirmation,
356    /// Physical inspection
357    PhysicalInspection,
358    /// Inquiry
359    Inquiry,
360}
361
362/// Nature of audit response.
363#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
364#[serde(rename_all = "snake_case")]
365pub enum ResponseNature {
366    /// Substantive procedures only
367    SubstantiveOnly,
368    /// Controls reliance with reduced substantive
369    ControlsReliance,
370    /// Combined approach
371    #[default]
372    Combined,
373}
374
375/// Timing of audit response.
376#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
377#[serde(rename_all = "snake_case")]
378pub enum ResponseTiming {
379    /// Interim testing
380    Interim,
381    /// Year-end testing
382    #[default]
383    YearEnd,
384    /// Roll-forward from interim
385    RollForward,
386    /// Subsequent events testing
387    Subsequent,
388}
389
390/// Status of planned response.
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
392#[serde(rename_all = "snake_case")]
393pub enum ResponseStatus {
394    /// Planned but not started
395    #[default]
396    Planned,
397    /// In progress
398    InProgress,
399    /// Complete
400    Complete,
401    /// Deferred
402    Deferred,
403    /// Not required
404    NotRequired,
405}
406
407/// Risk review status.
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
409#[serde(rename_all = "snake_case")]
410pub enum RiskReviewStatus {
411    /// Draft assessment
412    #[default]
413    Draft,
414    /// Pending review
415    PendingReview,
416    /// Reviewed and approved
417    Approved,
418    /// Requires revision
419    RequiresRevision,
420}
421
422/// Detection risk level (inverse of ROMM for achieving acceptable AR).
423#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
424#[serde(rename_all = "snake_case")]
425pub enum DetectionRisk {
426    /// Can accept high detection risk (less testing)
427    High,
428    /// Medium detection risk
429    Medium,
430    /// Low detection risk required (more testing)
431    Low,
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_risk_assessment_creation() {
440        let risk = RiskAssessment::new(
441            Uuid::new_v4(),
442            RiskCategory::AssertionLevel,
443            "Revenue",
444            "Risk of fictitious revenue recognition",
445        )
446        .with_assertion(Assertion::Occurrence)
447        .with_risk_levels(RiskLevel::High, RiskLevel::Medium);
448
449        assert!(risk.inherent_risk == RiskLevel::High);
450        assert!(
451            risk.requires_special_consideration()
452                || risk.risk_of_material_misstatement != RiskLevel::Low
453        );
454    }
455
456    #[test]
457    fn test_significant_risk() {
458        let risk = RiskAssessment::new(
459            Uuid::new_v4(),
460            RiskCategory::FraudRisk,
461            "Revenue",
462            "Fraud risk in revenue recognition",
463        )
464        .mark_significant("Presumed fraud risk per ISA 240");
465
466        assert!(risk.is_significant_risk);
467        assert!(risk.requires_special_consideration());
468    }
469
470    #[test]
471    fn test_fraud_risk_factor() {
472        let factor = FraudRiskFactor::new(
473            FraudTriangleElement::Pressure,
474            "Management bonus tied to revenue targets",
475            75,
476            "Bonus plan review",
477        )
478        .with_trend(Trend::Increasing);
479
480        assert_eq!(factor.factor_type, FraudTriangleElement::Pressure);
481        assert_eq!(factor.score, 75);
482    }
483
484    #[test]
485    fn test_detection_risk() {
486        let risk = RiskAssessment::new(
487            Uuid::new_v4(),
488            RiskCategory::AssertionLevel,
489            "Cash",
490            "Low risk account",
491        )
492        .with_risk_levels(RiskLevel::Low, RiskLevel::Low);
493
494        assert_eq!(risk.required_detection_risk(), DetectionRisk::High);
495    }
496}