Skip to main content

datasynth_standards/audit/
opinion.rs

1//! Audit Opinion Models (ISA 700/705/706/701).
2//!
3//! Implements audit opinion formation and reporting:
4//! - ISA 700: Forming an Opinion
5//! - ISA 701: Key Audit Matters
6//! - ISA 705: Modifications to the Opinion
7//! - ISA 706: Emphasis of Matter and Other Matter Paragraphs
8
9use chrono::NaiveDate;
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13/// Audit opinion record.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AuditOpinion {
16    /// Unique opinion identifier.
17    pub opinion_id: Uuid,
18
19    /// Engagement ID.
20    pub engagement_id: Uuid,
21
22    /// Opinion date.
23    pub opinion_date: NaiveDate,
24
25    /// Type of opinion.
26    pub opinion_type: OpinionType,
27
28    /// Key Audit Matters (ISA 701).
29    pub key_audit_matters: Vec<KeyAuditMatter>,
30
31    /// Modification details (if modified opinion).
32    pub modification: Option<OpinionModification>,
33
34    /// Emphasis of Matter paragraphs (ISA 706).
35    pub emphasis_of_matter: Vec<EmphasisOfMatter>,
36
37    /// Other Matter paragraphs (ISA 706).
38    pub other_matter: Vec<OtherMatter>,
39
40    /// Going concern conclusion.
41    pub going_concern_conclusion: GoingConcernConclusion,
42
43    /// Material uncertainty related to going concern.
44    pub material_uncertainty_going_concern: bool,
45
46    /// PCAOB compliance elements (for US issuers).
47    pub pcaob_compliance: Option<PcaobOpinionElements>,
48
49    /// Financial statement period end.
50    pub period_end_date: NaiveDate,
51
52    /// Entity name.
53    pub entity_name: String,
54
55    /// Auditor name/firm.
56    pub auditor_name: String,
57
58    /// Engagement partner.
59    pub engagement_partner: String,
60
61    /// Whether EQCR was performed.
62    pub eqcr_performed: bool,
63}
64
65impl AuditOpinion {
66    /// Create a new audit opinion.
67    pub fn new(
68        engagement_id: Uuid,
69        opinion_date: NaiveDate,
70        opinion_type: OpinionType,
71        entity_name: impl Into<String>,
72        period_end_date: NaiveDate,
73    ) -> Self {
74        Self {
75            opinion_id: Uuid::now_v7(),
76            engagement_id,
77            opinion_date,
78            opinion_type,
79            key_audit_matters: Vec::new(),
80            modification: None,
81            emphasis_of_matter: Vec::new(),
82            other_matter: Vec::new(),
83            going_concern_conclusion: GoingConcernConclusion::default(),
84            material_uncertainty_going_concern: false,
85            pcaob_compliance: None,
86            period_end_date,
87            entity_name: entity_name.into(),
88            auditor_name: String::new(),
89            engagement_partner: String::new(),
90            eqcr_performed: false,
91        }
92    }
93
94    /// Check if opinion is unmodified.
95    pub fn is_unmodified(&self) -> bool {
96        matches!(self.opinion_type, OpinionType::Unmodified)
97    }
98
99    /// Check if opinion is modified.
100    pub fn is_modified(&self) -> bool {
101        !self.is_unmodified()
102    }
103
104    /// Add a Key Audit Matter.
105    pub fn add_kam(&mut self, kam: KeyAuditMatter) {
106        self.key_audit_matters.push(kam);
107    }
108
109    /// Add Emphasis of Matter.
110    pub fn add_eom(&mut self, eom: EmphasisOfMatter) {
111        self.emphasis_of_matter.push(eom);
112    }
113}
114
115/// Type of audit opinion.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
117#[serde(rename_all = "snake_case")]
118pub enum OpinionType {
119    /// Clean opinion - financial statements are fairly presented.
120    #[default]
121    Unmodified,
122    /// Material misstatement but not pervasive.
123    Qualified,
124    /// Material and pervasive misstatement.
125    Adverse,
126    /// Unable to obtain sufficient appropriate audit evidence.
127    Disclaimer,
128}
129
130impl std::fmt::Display for OpinionType {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            Self::Unmodified => write!(f, "Unmodified"),
134            Self::Qualified => write!(f, "Qualified"),
135            Self::Adverse => write!(f, "Adverse"),
136            Self::Disclaimer => write!(f, "Disclaimer"),
137        }
138    }
139}
140
141/// Key Audit Matter (ISA 701).
142///
143/// Matters that were of most significance in the audit and required
144/// significant auditor attention.
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct KeyAuditMatter {
147    /// KAM identifier.
148    pub kam_id: Uuid,
149
150    /// Title/heading of the KAM.
151    pub title: String,
152
153    /// Description of why this matter was significant.
154    pub significance_explanation: String,
155
156    /// How the matter was addressed in the audit.
157    pub audit_response: String,
158
159    /// Related financial statement area.
160    pub financial_statement_area: String,
161
162    /// Risk of material misstatement level.
163    pub romm_level: RiskLevel,
164
165    /// Related findings (if any).
166    pub related_finding_ids: Vec<Uuid>,
167
168    /// Workpaper references.
169    pub workpaper_references: Vec<String>,
170}
171
172impl KeyAuditMatter {
173    /// Create a new Key Audit Matter.
174    pub fn new(
175        title: impl Into<String>,
176        significance_explanation: impl Into<String>,
177        audit_response: impl Into<String>,
178        financial_statement_area: impl Into<String>,
179    ) -> Self {
180        Self {
181            kam_id: Uuid::now_v7(),
182            title: title.into(),
183            significance_explanation: significance_explanation.into(),
184            audit_response: audit_response.into(),
185            financial_statement_area: financial_statement_area.into(),
186            romm_level: RiskLevel::High,
187            related_finding_ids: Vec::new(),
188            workpaper_references: Vec::new(),
189        }
190    }
191}
192
193/// Risk level.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
195#[serde(rename_all = "snake_case")]
196pub enum RiskLevel {
197    Low,
198    #[default]
199    Medium,
200    High,
201    VeryHigh,
202}
203
204/// Opinion modification details (ISA 705).
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct OpinionModification {
207    /// Basis for modification.
208    pub basis: ModificationBasis,
209
210    /// Description of the matter.
211    pub matter_description: String,
212
213    /// Financial statement effects.
214    pub financial_effects: FinancialEffects,
215
216    /// Whether matter is pervasive.
217    pub is_pervasive: bool,
218
219    /// Affected financial statement areas.
220    pub affected_areas: Vec<String>,
221
222    /// Misstatement amount (if quantifiable).
223    pub misstatement_amount: Option<rust_decimal::Decimal>,
224
225    /// Related to prior period.
226    pub relates_to_prior_period: bool,
227
228    /// Related to going concern.
229    pub relates_to_going_concern: bool,
230}
231
232impl OpinionModification {
233    /// Create a new opinion modification.
234    pub fn new(basis: ModificationBasis, matter_description: impl Into<String>) -> Self {
235        Self {
236            basis,
237            matter_description: matter_description.into(),
238            financial_effects: FinancialEffects::default(),
239            is_pervasive: false,
240            affected_areas: Vec::new(),
241            misstatement_amount: None,
242            relates_to_prior_period: false,
243            relates_to_going_concern: false,
244        }
245    }
246}
247
248/// Basis for opinion modification.
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
250#[serde(rename_all = "snake_case")]
251pub enum ModificationBasis {
252    /// Material misstatement in the financial statements.
253    MaterialMisstatement,
254    /// Inability to obtain sufficient appropriate audit evidence.
255    InabilityToObtainEvidence,
256    /// Both misstatement and scope limitation.
257    Both,
258}
259
260impl std::fmt::Display for ModificationBasis {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        match self {
263            Self::MaterialMisstatement => write!(f, "Material Misstatement"),
264            Self::InabilityToObtainEvidence => write!(f, "Inability to Obtain Evidence"),
265            Self::Both => write!(f, "Material Misstatement and Scope Limitation"),
266        }
267    }
268}
269
270/// Financial effects of modification matter.
271#[derive(Debug, Clone, Default, Serialize, Deserialize)]
272pub struct FinancialEffects {
273    /// Effect on assets.
274    pub assets_effect: String,
275    /// Effect on liabilities.
276    pub liabilities_effect: String,
277    /// Effect on equity.
278    pub equity_effect: String,
279    /// Effect on revenue.
280    pub revenue_effect: String,
281    /// Effect on expenses.
282    pub expenses_effect: String,
283    /// Effect on cash flows.
284    pub cash_flows_effect: String,
285    /// Effect on disclosures.
286    pub disclosures_effect: String,
287}
288
289/// Emphasis of Matter paragraph (ISA 706).
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct EmphasisOfMatter {
292    /// EOM identifier.
293    pub eom_id: Uuid,
294
295    /// Matter being emphasized.
296    pub matter: EomMatter,
297
298    /// Description of the matter.
299    pub description: String,
300
301    /// Reference to relevant notes.
302    pub note_reference: String,
303
304    /// Whether matter is appropriately presented and disclosed.
305    pub appropriately_presented: bool,
306}
307
308impl EmphasisOfMatter {
309    /// Create a new Emphasis of Matter.
310    pub fn new(matter: EomMatter, description: impl Into<String>) -> Self {
311        Self {
312            eom_id: Uuid::now_v7(),
313            matter,
314            description: description.into(),
315            note_reference: String::new(),
316            appropriately_presented: true,
317        }
318    }
319}
320
321/// Common Emphasis of Matter topics.
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
323#[serde(rename_all = "snake_case")]
324pub enum EomMatter {
325    /// Significant uncertainty about entity's ability to continue.
326    GoingConcern,
327    /// Major catastrophe affecting operations.
328    MajorCatastrophe,
329    /// Significant related party transactions.
330    RelatedPartyTransactions,
331    /// Significant subsequent event.
332    SubsequentEvent,
333    /// Change in accounting policy.
334    AccountingPolicyChange,
335    /// New accounting standard adoption.
336    NewStandardAdoption,
337    /// Unusually important litigation.
338    Litigation,
339    /// Regulatory action.
340    RegulatoryAction,
341    /// Other significant matter.
342    Other,
343}
344
345impl std::fmt::Display for EomMatter {
346    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347        match self {
348            Self::GoingConcern => write!(f, "Going Concern"),
349            Self::MajorCatastrophe => write!(f, "Major Catastrophe"),
350            Self::RelatedPartyTransactions => write!(f, "Related Party Transactions"),
351            Self::SubsequentEvent => write!(f, "Subsequent Event"),
352            Self::AccountingPolicyChange => write!(f, "Accounting Policy Change"),
353            Self::NewStandardAdoption => write!(f, "New Standard Adoption"),
354            Self::Litigation => write!(f, "Litigation"),
355            Self::RegulatoryAction => write!(f, "Regulatory Action"),
356            Self::Other => write!(f, "Other"),
357        }
358    }
359}
360
361/// Other Matter paragraph (ISA 706).
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct OtherMatter {
364    /// OM identifier.
365    pub om_id: Uuid,
366
367    /// Type of other matter.
368    pub matter_type: OtherMatterType,
369
370    /// Description.
371    pub description: String,
372}
373
374impl OtherMatter {
375    /// Create a new Other Matter.
376    pub fn new(matter_type: OtherMatterType, description: impl Into<String>) -> Self {
377        Self {
378            om_id: Uuid::now_v7(),
379            matter_type,
380            description: description.into(),
381        }
382    }
383}
384
385/// Types of Other Matter paragraphs.
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
387#[serde(rename_all = "snake_case")]
388pub enum OtherMatterType {
389    /// Prior period financial statements audited by another auditor.
390    PredecessorAuditor,
391    /// Prior period not audited.
392    PriorPeriodNotAudited,
393    /// Supplementary information.
394    SupplementaryInformation,
395    /// Other matter relevant to users.
396    Other,
397}
398
399/// Going concern conclusion (ISA 570).
400#[derive(Debug, Clone, Serialize, Deserialize, Default)]
401pub struct GoingConcernConclusion {
402    /// Conclusion on going concern.
403    pub conclusion: GoingConcernAssessment,
404
405    /// Material uncertainty exists.
406    pub material_uncertainty_exists: bool,
407
408    /// If uncertainty exists, is it adequately disclosed.
409    pub adequately_disclosed: bool,
410
411    /// Events/conditions identified.
412    pub events_conditions: Vec<String>,
413
414    /// Management's plans to address.
415    pub management_plans: String,
416
417    /// Auditor's evaluation of management's plans.
418    pub auditor_evaluation: String,
419
420    /// Impact on opinion.
421    pub opinion_impact: Option<OpinionType>,
422}
423
424/// Going concern assessment outcome.
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
426#[serde(rename_all = "snake_case")]
427pub enum GoingConcernAssessment {
428    /// No material uncertainty identified.
429    #[default]
430    NoMaterialUncertainty,
431    /// Material uncertainty exists, adequately disclosed.
432    MaterialUncertaintyAdequatelyDisclosed,
433    /// Material uncertainty exists, not adequately disclosed.
434    MaterialUncertaintyNotAdequatelyDisclosed,
435    /// Going concern basis inappropriate.
436    GoingConcernInappropriate,
437}
438
439impl std::fmt::Display for GoingConcernAssessment {
440    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441        match self {
442            Self::NoMaterialUncertainty => write!(f, "No Material Uncertainty"),
443            Self::MaterialUncertaintyAdequatelyDisclosed => {
444                write!(f, "Material Uncertainty - Adequately Disclosed")
445            }
446            Self::MaterialUncertaintyNotAdequatelyDisclosed => {
447                write!(f, "Material Uncertainty - Not Adequately Disclosed")
448            }
449            Self::GoingConcernInappropriate => write!(f, "Going Concern Basis Inappropriate"),
450        }
451    }
452}
453
454/// PCAOB-specific opinion elements for US issuers.
455#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct PcaobOpinionElements {
457    /// Whether this is an integrated audit (SOX 404).
458    pub is_integrated_audit: bool,
459
460    /// ICFR opinion (for integrated audits).
461    pub icfr_opinion: Option<IcfrOpinion>,
462
463    /// Critical Audit Matters (PCAOB equivalent of KAMs).
464    pub critical_audit_matters: Vec<KeyAuditMatter>,
465
466    /// Auditor tenure disclosure.
467    pub auditor_tenure_years: Option<u32>,
468
469    /// PCAOB registration number.
470    pub pcaob_registration_number: Option<String>,
471}
472
473impl PcaobOpinionElements {
474    /// Create new PCAOB elements.
475    pub fn new(is_integrated_audit: bool) -> Self {
476        Self {
477            is_integrated_audit,
478            icfr_opinion: None,
479            critical_audit_matters: Vec::new(),
480            auditor_tenure_years: None,
481            pcaob_registration_number: None,
482        }
483    }
484}
485
486/// ICFR (Internal Control over Financial Reporting) opinion.
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct IcfrOpinion {
489    /// ICFR opinion type.
490    pub opinion_type: IcfrOpinionType,
491
492    /// Material weaknesses identified.
493    pub material_weaknesses: Vec<MaterialWeakness>,
494
495    /// Significant deficiencies identified.
496    pub significant_deficiencies: Vec<String>,
497
498    /// Scope limitations (if any).
499    pub scope_limitations: Vec<String>,
500}
501
502/// ICFR opinion type.
503#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
504#[serde(rename_all = "snake_case")]
505pub enum IcfrOpinionType {
506    /// ICFR is effective.
507    #[default]
508    Effective,
509    /// Adverse opinion due to material weakness.
510    Adverse,
511    /// Disclaimer due to scope limitation.
512    Disclaimer,
513}
514
515/// Material weakness in ICFR.
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct MaterialWeakness {
518    /// Weakness identifier.
519    pub weakness_id: Uuid,
520
521    /// Description.
522    pub description: String,
523
524    /// Affected controls.
525    pub affected_controls: Vec<String>,
526
527    /// Affected accounts.
528    pub affected_accounts: Vec<String>,
529
530    /// Potential misstatement.
531    pub potential_misstatement: String,
532}
533
534impl MaterialWeakness {
535    /// Create a new material weakness.
536    pub fn new(description: impl Into<String>) -> Self {
537        Self {
538            weakness_id: Uuid::now_v7(),
539            description: description.into(),
540            affected_controls: Vec::new(),
541            affected_accounts: Vec::new(),
542            potential_misstatement: String::new(),
543        }
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    #[test]
552    fn test_audit_opinion_creation() {
553        let opinion = AuditOpinion::new(
554            Uuid::now_v7(),
555            NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
556            OpinionType::Unmodified,
557            "Test Company Inc.",
558            NaiveDate::from_ymd_opt(2023, 12, 31).unwrap(),
559        );
560
561        assert!(opinion.is_unmodified());
562        assert!(!opinion.is_modified());
563        assert!(opinion.key_audit_matters.is_empty());
564    }
565
566    #[test]
567    fn test_modified_opinion() {
568        let mut opinion = AuditOpinion::new(
569            Uuid::now_v7(),
570            NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
571            OpinionType::Qualified,
572            "Test Company Inc.",
573            NaiveDate::from_ymd_opt(2023, 12, 31).unwrap(),
574        );
575
576        opinion.modification = Some(OpinionModification::new(
577            ModificationBasis::MaterialMisstatement,
578            "Inventory was not properly valued",
579        ));
580
581        assert!(opinion.is_modified());
582        assert!(opinion.modification.is_some());
583    }
584
585    #[test]
586    fn test_key_audit_matter() {
587        let kam = KeyAuditMatter::new(
588            "Revenue Recognition",
589            "Complex multi-element arrangements require significant judgment",
590            "We tested controls over revenue recognition and performed substantive testing",
591            "Revenue",
592        );
593
594        assert_eq!(kam.title, "Revenue Recognition");
595        assert_eq!(kam.romm_level, RiskLevel::High);
596    }
597
598    #[test]
599    fn test_going_concern() {
600        let mut gc = GoingConcernConclusion::default();
601        gc.conclusion = GoingConcernAssessment::MaterialUncertaintyAdequatelyDisclosed;
602        gc.material_uncertainty_exists = true;
603        gc.adequately_disclosed = true;
604
605        assert!(gc.material_uncertainty_exists);
606        assert!(gc.adequately_disclosed);
607    }
608
609    #[test]
610    fn test_pcaob_elements() {
611        let mut pcaob = PcaobOpinionElements::new(true);
612        pcaob.auditor_tenure_years = Some(5);
613        pcaob.icfr_opinion = Some(IcfrOpinion {
614            opinion_type: IcfrOpinionType::Effective,
615            material_weaknesses: Vec::new(),
616            significant_deficiencies: Vec::new(),
617            scope_limitations: Vec::new(),
618        });
619
620        assert!(pcaob.is_integrated_audit);
621        assert!(pcaob.icfr_opinion.is_some());
622    }
623}