1use chrono::{DateTime, NaiveDate, Utc};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AuditEngagement {
14 pub engagement_id: Uuid,
16 pub engagement_ref: String,
18 pub client_entity_id: String,
20 pub client_name: String,
22 pub engagement_type: EngagementType,
24 pub fiscal_year: u16,
26 pub period_end_date: NaiveDate,
28
29 pub materiality: Decimal,
32 pub performance_materiality: Decimal,
34 pub clearly_trivial: Decimal,
36 pub materiality_basis: String,
38 pub materiality_percentage: f64,
40
41 pub planning_start: NaiveDate,
44 pub planning_end: NaiveDate,
46 pub fieldwork_start: NaiveDate,
48 pub fieldwork_end: NaiveDate,
50 pub completion_start: NaiveDate,
52 pub report_date: NaiveDate,
54
55 pub engagement_partner_id: String,
58 pub engagement_partner_name: String,
60 pub engagement_manager_id: String,
62 pub engagement_manager_name: String,
64 pub team_member_ids: Vec<String>,
66
67 pub status: EngagementStatus,
70 pub current_phase: EngagementPhase,
72
73 pub overall_audit_risk: RiskLevel,
76 pub significant_risk_count: u32,
78 pub fraud_risk_level: RiskLevel,
80
81 pub created_at: DateTime<Utc>,
83 pub updated_at: DateTime<Utc>,
84}
85
86impl AuditEngagement {
87 pub fn new(
89 client_entity_id: &str,
90 client_name: &str,
91 engagement_type: EngagementType,
92 fiscal_year: u16,
93 period_end_date: NaiveDate,
94 ) -> Self {
95 let now = Utc::now();
96 Self {
97 engagement_id: Uuid::new_v4(),
98 engagement_ref: format!("AUD-{}-{:03}", fiscal_year, 1),
99 client_entity_id: client_entity_id.into(),
100 client_name: client_name.into(),
101 engagement_type,
102 fiscal_year,
103 period_end_date,
104 materiality: Decimal::ZERO,
105 performance_materiality: Decimal::ZERO,
106 clearly_trivial: Decimal::ZERO,
107 materiality_basis: String::new(),
108 materiality_percentage: 0.0,
109 planning_start: period_end_date,
110 planning_end: period_end_date,
111 fieldwork_start: period_end_date,
112 fieldwork_end: period_end_date,
113 completion_start: period_end_date,
114 report_date: period_end_date,
115 engagement_partner_id: String::new(),
116 engagement_partner_name: String::new(),
117 engagement_manager_id: String::new(),
118 engagement_manager_name: String::new(),
119 team_member_ids: Vec::new(),
120 status: EngagementStatus::Planning,
121 current_phase: EngagementPhase::Planning,
122 overall_audit_risk: RiskLevel::Medium,
123 significant_risk_count: 0,
124 fraud_risk_level: RiskLevel::Low,
125 created_at: now,
126 updated_at: now,
127 }
128 }
129
130 pub fn with_materiality(
132 mut self,
133 materiality: Decimal,
134 performance_materiality_factor: f64,
135 clearly_trivial_factor: f64,
136 basis: &str,
137 percentage: f64,
138 ) -> Self {
139 self.materiality = materiality;
140 self.performance_materiality =
141 materiality * Decimal::try_from(performance_materiality_factor).unwrap_or_default();
142 self.clearly_trivial =
143 materiality * Decimal::try_from(clearly_trivial_factor).unwrap_or_default();
144 self.materiality_basis = basis.into();
145 self.materiality_percentage = percentage;
146 self
147 }
148
149 pub fn with_team(
151 mut self,
152 partner_id: &str,
153 partner_name: &str,
154 manager_id: &str,
155 manager_name: &str,
156 team_members: Vec<String>,
157 ) -> Self {
158 self.engagement_partner_id = partner_id.into();
159 self.engagement_partner_name = partner_name.into();
160 self.engagement_manager_id = manager_id.into();
161 self.engagement_manager_name = manager_name.into();
162 self.team_member_ids = team_members;
163 self
164 }
165
166 pub fn with_timeline(
168 mut self,
169 planning_start: NaiveDate,
170 planning_end: NaiveDate,
171 fieldwork_start: NaiveDate,
172 fieldwork_end: NaiveDate,
173 completion_start: NaiveDate,
174 report_date: NaiveDate,
175 ) -> Self {
176 self.planning_start = planning_start;
177 self.planning_end = planning_end;
178 self.fieldwork_start = fieldwork_start;
179 self.fieldwork_end = fieldwork_end;
180 self.completion_start = completion_start;
181 self.report_date = report_date;
182 self
183 }
184
185 pub fn advance_phase(&mut self) {
187 self.current_phase = match self.current_phase {
188 EngagementPhase::Planning => EngagementPhase::RiskAssessment,
189 EngagementPhase::RiskAssessment => EngagementPhase::ControlTesting,
190 EngagementPhase::ControlTesting => EngagementPhase::SubstantiveTesting,
191 EngagementPhase::SubstantiveTesting => EngagementPhase::Completion,
192 EngagementPhase::Completion => EngagementPhase::Reporting,
193 EngagementPhase::Reporting => EngagementPhase::Reporting,
194 };
195 self.updated_at = Utc::now();
196 }
197
198 pub fn is_complete(&self) -> bool {
200 matches!(
201 self.status,
202 EngagementStatus::Complete | EngagementStatus::Archived
203 )
204 }
205
206 pub fn days_until_report(&self, as_of: NaiveDate) -> i64 {
208 (self.report_date - as_of).num_days()
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
214#[serde(rename_all = "snake_case")]
215pub enum EngagementType {
216 #[default]
218 AnnualAudit,
219 InterimAudit,
221 Sox404,
223 IntegratedAudit,
225 ReviewEngagement,
227 CompilationEngagement,
229 AgreedUponProcedures,
231 SpecialPurpose,
233}
234
235impl EngagementType {
236 pub fn assurance_level(&self) -> AssuranceLevel {
238 match self {
239 Self::AnnualAudit
240 | Self::InterimAudit
241 | Self::Sox404
242 | Self::IntegratedAudit
243 | Self::SpecialPurpose => AssuranceLevel::Reasonable,
244 Self::ReviewEngagement => AssuranceLevel::Limited,
245 Self::CompilationEngagement | Self::AgreedUponProcedures => AssuranceLevel::None,
246 }
247 }
248
249 pub fn requires_sox_testing(&self) -> bool {
251 matches!(self, Self::Sox404 | Self::IntegratedAudit)
252 }
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258pub enum AssuranceLevel {
259 Reasonable,
261 Limited,
263 None,
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
269#[serde(rename_all = "snake_case")]
270pub enum EngagementStatus {
271 #[default]
273 Planning,
274 InProgress,
276 UnderReview,
278 PendingSignOff,
280 Complete,
282 Archived,
284 OnHold,
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
290#[serde(rename_all = "snake_case")]
291pub enum EngagementPhase {
292 #[default]
294 Planning,
295 RiskAssessment,
297 ControlTesting,
299 SubstantiveTesting,
301 Completion,
303 Reporting,
305}
306
307impl EngagementPhase {
308 pub fn display_name(&self) -> &'static str {
310 match self {
311 Self::Planning => "Planning",
312 Self::RiskAssessment => "Risk Assessment",
313 Self::ControlTesting => "Control Testing",
314 Self::SubstantiveTesting => "Substantive Testing",
315 Self::Completion => "Completion",
316 Self::Reporting => "Reporting",
317 }
318 }
319
320 pub fn isa_reference(&self) -> &'static str {
322 match self {
323 Self::Planning => "ISA 300",
324 Self::RiskAssessment => "ISA 315",
325 Self::ControlTesting => "ISA 330",
326 Self::SubstantiveTesting => "ISA 330, ISA 500",
327 Self::Completion => "ISA 450, ISA 560",
328 Self::Reporting => "ISA 700",
329 }
330 }
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
335#[serde(rename_all = "snake_case")]
336pub enum RiskLevel {
337 Low,
339 #[default]
341 Medium,
342 High,
344 Significant,
346}
347
348impl RiskLevel {
349 pub fn score(&self) -> u8 {
351 match self {
352 Self::Low => 1,
353 Self::Medium => 2,
354 Self::High => 3,
355 Self::Significant => 4,
356 }
357 }
358
359 pub fn from_score(score: u8) -> Self {
361 match score {
362 0..=1 => Self::Low,
363 2 => Self::Medium,
364 3 => Self::High,
365 _ => Self::Significant,
366 }
367 }
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct EngagementTeamMember {
373 pub member_id: String,
375 pub name: String,
377 pub role: TeamMemberRole,
379 pub allocated_hours: f64,
381 pub actual_hours: f64,
383 pub assigned_sections: Vec<String>,
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
389#[serde(rename_all = "snake_case")]
390pub enum TeamMemberRole {
391 EngagementPartner,
392 EngagementQualityReviewer,
393 EngagementManager,
394 Senior,
395 Staff,
396 Specialist,
397 ITAuditor,
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn test_engagement_creation() {
406 let engagement = AuditEngagement::new(
407 "ENTITY001",
408 "Test Company Inc.",
409 EngagementType::AnnualAudit,
410 2025,
411 NaiveDate::from_ymd_opt(2025, 12, 31).unwrap(),
412 );
413
414 assert_eq!(engagement.fiscal_year, 2025);
415 assert_eq!(engagement.engagement_type, EngagementType::AnnualAudit);
416 assert_eq!(engagement.status, EngagementStatus::Planning);
417 }
418
419 #[test]
420 fn test_engagement_with_materiality() {
421 let engagement = AuditEngagement::new(
422 "ENTITY001",
423 "Test Company Inc.",
424 EngagementType::AnnualAudit,
425 2025,
426 NaiveDate::from_ymd_opt(2025, 12, 31).unwrap(),
427 )
428 .with_materiality(
429 Decimal::new(1_000_000, 0),
430 0.75,
431 0.05,
432 "Total Revenue",
433 0.005,
434 );
435
436 assert_eq!(engagement.materiality, Decimal::new(1_000_000, 0));
437 assert_eq!(engagement.performance_materiality, Decimal::new(750_000, 0));
438 assert_eq!(engagement.clearly_trivial, Decimal::new(50_000, 0));
439 }
440
441 #[test]
442 fn test_phase_advancement() {
443 let mut engagement = AuditEngagement::new(
444 "ENTITY001",
445 "Test Company Inc.",
446 EngagementType::AnnualAudit,
447 2025,
448 NaiveDate::from_ymd_opt(2025, 12, 31).unwrap(),
449 );
450
451 assert_eq!(engagement.current_phase, EngagementPhase::Planning);
452 engagement.advance_phase();
453 assert_eq!(engagement.current_phase, EngagementPhase::RiskAssessment);
454 engagement.advance_phase();
455 assert_eq!(engagement.current_phase, EngagementPhase::ControlTesting);
456 }
457
458 #[test]
459 fn test_risk_level_score() {
460 assert_eq!(RiskLevel::Low.score(), 1);
461 assert_eq!(RiskLevel::Significant.score(), 4);
462 assert_eq!(RiskLevel::from_score(3), RiskLevel::High);
463 }
464}