datasynth_core/models/audit/
engagement.rs

1//! Audit engagement models per ISA 210 and ISA 220.
2//!
3//! An audit engagement represents the entire audit project including
4//! planning, fieldwork, and reporting phases.
5
6use chrono::{DateTime, NaiveDate, Utc};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11/// Audit engagement representing an audit project.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AuditEngagement {
14    /// Unique engagement ID
15    pub engagement_id: Uuid,
16    /// External engagement reference (e.g., "AUD-2025-001")
17    pub engagement_ref: String,
18    /// Client entity being audited
19    pub client_entity_id: String,
20    /// Client name
21    pub client_name: String,
22    /// Type of engagement
23    pub engagement_type: EngagementType,
24    /// Fiscal year being audited
25    pub fiscal_year: u16,
26    /// Fiscal period end date
27    pub period_end_date: NaiveDate,
28
29    // === Materiality ===
30    /// Overall materiality
31    pub materiality: Decimal,
32    /// Performance materiality (typically 50-75% of materiality)
33    pub performance_materiality: Decimal,
34    /// Clearly trivial threshold (typically 3-5% of materiality)
35    pub clearly_trivial: Decimal,
36    /// Materiality basis (e.g., "Total Revenue", "Total Assets")
37    pub materiality_basis: String,
38    /// Materiality percentage applied
39    pub materiality_percentage: f64,
40
41    // === Timeline ===
42    /// Planning phase start date
43    pub planning_start: NaiveDate,
44    /// Planning phase end date
45    pub planning_end: NaiveDate,
46    /// Fieldwork start date
47    pub fieldwork_start: NaiveDate,
48    /// Fieldwork end date
49    pub fieldwork_end: NaiveDate,
50    /// Completion phase start date
51    pub completion_start: NaiveDate,
52    /// Expected report date
53    pub report_date: NaiveDate,
54
55    // === Team ===
56    /// Engagement partner ID
57    pub engagement_partner_id: String,
58    /// Engagement partner name
59    pub engagement_partner_name: String,
60    /// Engagement manager ID
61    pub engagement_manager_id: String,
62    /// Engagement manager name
63    pub engagement_manager_name: String,
64    /// All team member IDs
65    pub team_member_ids: Vec<String>,
66
67    // === Status ===
68    /// Current engagement status
69    pub status: EngagementStatus,
70    /// Current phase
71    pub current_phase: EngagementPhase,
72
73    // === Risk Assessment Summary ===
74    /// Overall audit risk assessment
75    pub overall_audit_risk: RiskLevel,
76    /// Number of significant risks identified
77    pub significant_risk_count: u32,
78    /// Fraud risk assessment level
79    pub fraud_risk_level: RiskLevel,
80
81    // === Timestamps ===
82    pub created_at: DateTime<Utc>,
83    pub updated_at: DateTime<Utc>,
84}
85
86impl AuditEngagement {
87    /// Create a new audit engagement.
88    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    /// Set materiality values.
131    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    /// Set the engagement team.
150    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    /// Set engagement timeline.
167    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    /// Advance to the next phase.
186    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    /// Check if the engagement is complete.
199    pub fn is_complete(&self) -> bool {
200        matches!(
201            self.status,
202            EngagementStatus::Complete | EngagementStatus::Archived
203        )
204    }
205
206    /// Calculate days until report date.
207    pub fn days_until_report(&self, as_of: NaiveDate) -> i64 {
208        (self.report_date - as_of).num_days()
209    }
210}
211
212/// Type of audit engagement.
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
214#[serde(rename_all = "snake_case")]
215pub enum EngagementType {
216    /// Annual financial statement audit
217    #[default]
218    AnnualAudit,
219    /// Interim audit procedures
220    InterimAudit,
221    /// SOX 404 internal control audit
222    Sox404,
223    /// Integrated audit (financial statements + SOX)
224    IntegratedAudit,
225    /// Review engagement (limited assurance)
226    ReviewEngagement,
227    /// Compilation engagement (no assurance)
228    CompilationEngagement,
229    /// Agreed-upon procedures
230    AgreedUponProcedures,
231    /// Special purpose audit
232    SpecialPurpose,
233}
234
235impl EngagementType {
236    /// Get the assurance level for this engagement type.
237    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    /// Check if this requires SOX compliance testing.
250    pub fn requires_sox_testing(&self) -> bool {
251        matches!(self, Self::Sox404 | Self::IntegratedAudit)
252    }
253}
254
255/// Level of assurance provided.
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258pub enum AssuranceLevel {
259    /// Reasonable assurance (audit)
260    Reasonable,
261    /// Limited assurance (review)
262    Limited,
263    /// No assurance (compilation, AUP)
264    None,
265}
266
267/// Engagement status.
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
269#[serde(rename_all = "snake_case")]
270pub enum EngagementStatus {
271    /// Planning in progress
272    #[default]
273    Planning,
274    /// Active fieldwork
275    InProgress,
276    /// Under review
277    UnderReview,
278    /// Pending partner sign-off
279    PendingSignOff,
280    /// Complete
281    Complete,
282    /// Archived
283    Archived,
284    /// On hold
285    OnHold,
286}
287
288/// Current engagement phase.
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
290#[serde(rename_all = "snake_case")]
291pub enum EngagementPhase {
292    /// Planning and understanding the entity
293    #[default]
294    Planning,
295    /// Risk assessment procedures
296    RiskAssessment,
297    /// Testing of controls
298    ControlTesting,
299    /// Substantive testing
300    SubstantiveTesting,
301    /// Completion procedures
302    Completion,
303    /// Report issuance
304    Reporting,
305}
306
307impl EngagementPhase {
308    /// Get the phase name for display.
309    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    /// Get the ISA reference for this phase.
321    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/// Risk level classification.
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
335#[serde(rename_all = "snake_case")]
336pub enum RiskLevel {
337    /// Low risk
338    Low,
339    /// Medium risk
340    #[default]
341    Medium,
342    /// High risk
343    High,
344    /// Significant risk (per ISA 315)
345    Significant,
346}
347
348impl RiskLevel {
349    /// Get numeric score for calculations.
350    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    /// Create from numeric score.
360    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/// Engagement team member role.
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct EngagementTeamMember {
373    /// Team member ID
374    pub member_id: String,
375    /// Team member name
376    pub name: String,
377    /// Role on the engagement
378    pub role: TeamMemberRole,
379    /// Allocated hours
380    pub allocated_hours: f64,
381    /// Actual hours worked
382    pub actual_hours: f64,
383    /// Sections assigned
384    pub assigned_sections: Vec<String>,
385}
386
387/// Role of a team member.
388#[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}