1use chrono::NaiveDate;
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AuditOpinion {
16 pub opinion_id: Uuid,
18
19 pub engagement_id: Uuid,
21
22 pub opinion_date: NaiveDate,
24
25 pub opinion_type: OpinionType,
27
28 pub key_audit_matters: Vec<KeyAuditMatter>,
30
31 pub modification: Option<OpinionModification>,
33
34 pub emphasis_of_matter: Vec<EmphasisOfMatter>,
36
37 pub other_matter: Vec<OtherMatter>,
39
40 pub going_concern_conclusion: GoingConcernConclusion,
42
43 pub material_uncertainty_going_concern: bool,
45
46 pub pcaob_compliance: Option<PcaobOpinionElements>,
48
49 pub period_end_date: NaiveDate,
51
52 pub entity_name: String,
54
55 pub auditor_name: String,
57
58 pub engagement_partner: String,
60
61 pub eqcr_performed: bool,
63}
64
65impl AuditOpinion {
66 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 pub fn is_unmodified(&self) -> bool {
96 matches!(self.opinion_type, OpinionType::Unmodified)
97 }
98
99 pub fn is_modified(&self) -> bool {
101 !self.is_unmodified()
102 }
103
104 pub fn add_kam(&mut self, kam: KeyAuditMatter) {
106 self.key_audit_matters.push(kam);
107 }
108
109 pub fn add_eom(&mut self, eom: EmphasisOfMatter) {
111 self.emphasis_of_matter.push(eom);
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
117#[serde(rename_all = "snake_case")]
118pub enum OpinionType {
119 #[default]
121 Unmodified,
122 Qualified,
124 Adverse,
126 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#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct KeyAuditMatter {
147 pub kam_id: Uuid,
149
150 pub title: String,
152
153 pub significance_explanation: String,
155
156 pub audit_response: String,
158
159 pub financial_statement_area: String,
161
162 pub romm_level: RiskLevel,
164
165 pub related_finding_ids: Vec<Uuid>,
167
168 pub workpaper_references: Vec<String>,
170}
171
172impl KeyAuditMatter {
173 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct OpinionModification {
207 pub basis: ModificationBasis,
209
210 pub matter_description: String,
212
213 pub financial_effects: FinancialEffects,
215
216 pub is_pervasive: bool,
218
219 pub affected_areas: Vec<String>,
221
222 pub misstatement_amount: Option<rust_decimal::Decimal>,
224
225 pub relates_to_prior_period: bool,
227
228 pub relates_to_going_concern: bool,
230}
231
232impl OpinionModification {
233 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
250#[serde(rename_all = "snake_case")]
251pub enum ModificationBasis {
252 MaterialMisstatement,
254 InabilityToObtainEvidence,
256 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
272pub struct FinancialEffects {
273 pub assets_effect: String,
275 pub liabilities_effect: String,
277 pub equity_effect: String,
279 pub revenue_effect: String,
281 pub expenses_effect: String,
283 pub cash_flows_effect: String,
285 pub disclosures_effect: String,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct EmphasisOfMatter {
292 pub eom_id: Uuid,
294
295 pub matter: EomMatter,
297
298 pub description: String,
300
301 pub note_reference: String,
303
304 pub appropriately_presented: bool,
306}
307
308impl EmphasisOfMatter {
309 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
323#[serde(rename_all = "snake_case")]
324pub enum EomMatter {
325 GoingConcern,
327 MajorCatastrophe,
329 RelatedPartyTransactions,
331 SubsequentEvent,
333 AccountingPolicyChange,
335 NewStandardAdoption,
337 Litigation,
339 RegulatoryAction,
341 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#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct OtherMatter {
364 pub om_id: Uuid,
366
367 pub matter_type: OtherMatterType,
369
370 pub description: String,
372}
373
374impl OtherMatter {
375 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
387#[serde(rename_all = "snake_case")]
388pub enum OtherMatterType {
389 PredecessorAuditor,
391 PriorPeriodNotAudited,
393 SupplementaryInformation,
395 Other,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize, Default)]
401pub struct GoingConcernConclusion {
402 pub conclusion: GoingConcernAssessment,
404
405 pub material_uncertainty_exists: bool,
407
408 pub adequately_disclosed: bool,
410
411 pub events_conditions: Vec<String>,
413
414 pub management_plans: String,
416
417 pub auditor_evaluation: String,
419
420 pub opinion_impact: Option<OpinionType>,
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
426#[serde(rename_all = "snake_case")]
427pub enum GoingConcernAssessment {
428 #[default]
430 NoMaterialUncertainty,
431 MaterialUncertaintyAdequatelyDisclosed,
433 MaterialUncertaintyNotAdequatelyDisclosed,
435 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#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct PcaobOpinionElements {
457 pub is_integrated_audit: bool,
459
460 pub icfr_opinion: Option<IcfrOpinion>,
462
463 pub critical_audit_matters: Vec<KeyAuditMatter>,
465
466 pub auditor_tenure_years: Option<u32>,
468
469 pub pcaob_registration_number: Option<String>,
471}
472
473impl PcaobOpinionElements {
474 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#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct IcfrOpinion {
489 pub opinion_type: IcfrOpinionType,
491
492 pub material_weaknesses: Vec<MaterialWeakness>,
494
495 pub significant_deficiencies: Vec<String>,
497
498 pub scope_limitations: Vec<String>,
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
504#[serde(rename_all = "snake_case")]
505pub enum IcfrOpinionType {
506 #[default]
508 Effective,
509 Adverse,
511 Disclaimer,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct MaterialWeakness {
518 pub weakness_id: Uuid,
520
521 pub description: String,
523
524 pub affected_controls: Vec<String>,
526
527 pub affected_accounts: Vec<String>,
529
530 pub potential_misstatement: String,
532}
533
534impl MaterialWeakness {
535 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}