1use chrono::{DateTime, NaiveDate, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use super::engagement::RiskLevel;
12use super::workpaper::Assertion;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct RiskAssessment {
17 pub risk_id: Uuid,
19 pub risk_ref: String,
21 pub engagement_id: Uuid,
23 pub risk_category: RiskCategory,
25 pub account_or_process: String,
27 pub assertion: Option<Assertion>,
29 pub description: String,
31
32 pub inherent_risk: RiskLevel,
35 pub control_risk: RiskLevel,
37 pub risk_of_material_misstatement: RiskLevel,
39 pub is_significant_risk: bool,
41 pub significant_risk_rationale: Option<String>,
43
44 pub fraud_risk_factors: Vec<FraudRiskFactor>,
47 pub presumed_revenue_fraud_risk: bool,
49 pub presumed_management_override: bool,
51
52 pub planned_response: Vec<PlannedResponse>,
55 pub response_nature: ResponseNature,
57 pub response_extent: String,
59 pub response_timing: ResponseTiming,
61
62 pub assessed_by: String,
65 pub assessed_date: NaiveDate,
67 pub review_status: RiskReviewStatus,
69 pub reviewer_id: Option<String>,
71 pub review_date: Option<NaiveDate>,
73
74 pub workpaper_refs: Vec<Uuid>,
77 pub related_controls: Vec<String>,
79
80 pub created_at: DateTime<Utc>,
81 pub updated_at: DateTime<Utc>,
82}
83
84impl RiskAssessment {
85 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 pub fn with_assertion(mut self, assertion: Assertion) -> Self {
130 self.assertion = Some(assertion);
131 self
132 }
133
134 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 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 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 pub fn add_response(&mut self, response: PlannedResponse) {
157 self.planned_response.push(response);
158 self.updated_at = Utc::now();
159 }
160
161 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
198#[serde(rename_all = "snake_case")]
199pub enum RiskCategory {
200 FinancialStatementLevel,
202 #[default]
204 AssertionLevel,
205 FraudRisk,
207 GoingConcern,
209 RelatedParty,
211 EstimateRisk,
213 ItGeneralControl,
215 RegulatoryCompliance,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct FraudRiskFactor {
222 pub factor_id: Uuid,
224 pub factor_type: FraudTriangleElement,
226 pub indicator: String,
228 pub score: u8,
230 pub trend: Trend,
232 pub source: String,
234 pub identified_date: NaiveDate,
236}
237
238impl FraudRiskFactor {
239 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 pub fn with_trend(mut self, trend: Trend) -> Self {
259 self.trend = trend;
260 self
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
266#[serde(rename_all = "snake_case")]
267pub enum FraudTriangleElement {
268 Opportunity,
270 Pressure,
272 Rationalization,
274}
275
276impl FraudTriangleElement {
277 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
289#[serde(rename_all = "snake_case")]
290pub enum Trend {
291 Increasing,
293 #[default]
295 Stable,
296 Decreasing,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct PlannedResponse {
303 pub response_id: Uuid,
305 pub procedure: String,
307 pub procedure_type: ResponseProcedureType,
309 pub assertion_addressed: Assertion,
311 pub assigned_to: String,
313 pub target_date: NaiveDate,
315 pub status: ResponseStatus,
317 pub workpaper_ref: Option<Uuid>,
319}
320
321impl PlannedResponse {
322 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
345#[serde(rename_all = "snake_case")]
346pub enum ResponseProcedureType {
347 TestOfControls,
349 AnalyticalProcedure,
351 #[default]
353 TestOfDetails,
354 Confirmation,
356 PhysicalInspection,
358 Inquiry,
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
364#[serde(rename_all = "snake_case")]
365pub enum ResponseNature {
366 SubstantiveOnly,
368 ControlsReliance,
370 #[default]
372 Combined,
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
377#[serde(rename_all = "snake_case")]
378pub enum ResponseTiming {
379 Interim,
381 #[default]
383 YearEnd,
384 RollForward,
386 Subsequent,
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
392#[serde(rename_all = "snake_case")]
393pub enum ResponseStatus {
394 #[default]
396 Planned,
397 InProgress,
399 Complete,
401 Deferred,
403 NotRequired,
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
409#[serde(rename_all = "snake_case")]
410pub enum RiskReviewStatus {
411 #[default]
413 Draft,
414 PendingReview,
416 Approved,
418 RequiresRevision,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
424#[serde(rename_all = "snake_case")]
425pub enum DetectionRisk {
426 High,
428 Medium,
430 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}