datasynth_core/models/audit/
judgment.rs

1//! Professional judgment models per ISA 200.
2//!
3//! Professional judgment is essential in applying audit standards and
4//! making informed decisions throughout the audit process.
5
6use chrono::{DateTime, NaiveDate, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::engagement::RiskLevel;
11
12/// Professional judgment documentation.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ProfessionalJudgment {
15    /// Unique judgment ID
16    pub judgment_id: Uuid,
17    /// External reference
18    pub judgment_ref: String,
19    /// Engagement ID
20    pub engagement_id: Uuid,
21    /// Type of judgment
22    pub judgment_type: JudgmentType,
23    /// Subject matter of the judgment
24    pub subject: String,
25    /// Applicable auditing standards
26    pub applicable_standards: Vec<String>,
27
28    // === Structured Documentation ===
29    /// Issue or matter requiring judgment
30    pub issue_description: String,
31    /// Information and factors considered
32    pub information_considered: Vec<InformationItem>,
33    /// Alternatives evaluated
34    pub alternatives_evaluated: Vec<AlternativeEvaluation>,
35    /// Professional skepticism applied
36    pub skepticism_applied: SkepticismDocumentation,
37
38    // === Conclusion ===
39    /// Conclusion reached
40    pub conclusion: String,
41    /// Rationale for conclusion
42    pub rationale: String,
43    /// Residual risk or uncertainty
44    pub residual_risk: String,
45    /// Impact on audit approach
46    pub impact_on_audit: Option<String>,
47
48    // === Consultation ===
49    /// Was consultation required?
50    pub consultation_required: bool,
51    /// Consultation details
52    pub consultation: Option<ConsultationRecord>,
53
54    // === Sign-offs ===
55    /// Preparer user ID
56    pub preparer_id: String,
57    /// Preparer name
58    pub preparer_name: String,
59    /// Date prepared
60    pub preparer_date: NaiveDate,
61    /// Reviewer ID
62    pub reviewer_id: Option<String>,
63    /// Reviewer name
64    pub reviewer_name: Option<String>,
65    /// Review date
66    pub reviewer_date: Option<NaiveDate>,
67    /// Partner concurrence required?
68    pub partner_concurrence_required: bool,
69    /// Partner concurrence ID
70    pub partner_concurrence_id: Option<String>,
71    /// Partner concurrence date
72    pub partner_concurrence_date: Option<NaiveDate>,
73
74    // === Cross-References ===
75    /// Related workpaper IDs
76    pub workpaper_refs: Vec<Uuid>,
77    /// Related evidence IDs
78    pub evidence_refs: Vec<Uuid>,
79
80    // === Status ===
81    pub status: JudgmentStatus,
82    pub created_at: DateTime<Utc>,
83    pub updated_at: DateTime<Utc>,
84}
85
86impl ProfessionalJudgment {
87    /// Create a new professional judgment document.
88    pub fn new(engagement_id: Uuid, judgment_type: JudgmentType, subject: &str) -> Self {
89        let now = Utc::now();
90        Self {
91            judgment_id: Uuid::new_v4(),
92            judgment_ref: format!("JDG-{}-{:03}", now.format("%Y"), 1),
93            engagement_id,
94            judgment_type,
95            subject: subject.into(),
96            applicable_standards: judgment_type.default_standards(),
97            issue_description: String::new(),
98            information_considered: Vec::new(),
99            alternatives_evaluated: Vec::new(),
100            skepticism_applied: SkepticismDocumentation::default(),
101            conclusion: String::new(),
102            rationale: String::new(),
103            residual_risk: String::new(),
104            impact_on_audit: None,
105            consultation_required: judgment_type.typically_requires_consultation(),
106            consultation: None,
107            preparer_id: String::new(),
108            preparer_name: String::new(),
109            preparer_date: now.date_naive(),
110            reviewer_id: None,
111            reviewer_name: None,
112            reviewer_date: None,
113            partner_concurrence_required: judgment_type.requires_partner_concurrence(),
114            partner_concurrence_id: None,
115            partner_concurrence_date: None,
116            workpaper_refs: Vec::new(),
117            evidence_refs: Vec::new(),
118            status: JudgmentStatus::Draft,
119            created_at: now,
120            updated_at: now,
121        }
122    }
123
124    /// Set the issue description.
125    pub fn with_issue(mut self, issue: &str) -> Self {
126        self.issue_description = issue.into();
127        self
128    }
129
130    /// Add information considered.
131    pub fn add_information(&mut self, item: InformationItem) {
132        self.information_considered.push(item);
133        self.updated_at = Utc::now();
134    }
135
136    /// Add an alternative evaluation.
137    pub fn add_alternative(&mut self, alternative: AlternativeEvaluation) {
138        self.alternatives_evaluated.push(alternative);
139        self.updated_at = Utc::now();
140    }
141
142    /// Set skepticism documentation.
143    pub fn with_skepticism(mut self, skepticism: SkepticismDocumentation) -> Self {
144        self.skepticism_applied = skepticism;
145        self
146    }
147
148    /// Set conclusion.
149    pub fn with_conclusion(
150        mut self,
151        conclusion: &str,
152        rationale: &str,
153        residual_risk: &str,
154    ) -> Self {
155        self.conclusion = conclusion.into();
156        self.rationale = rationale.into();
157        self.residual_risk = residual_risk.into();
158        self
159    }
160
161    /// Set preparer.
162    pub fn with_preparer(mut self, id: &str, name: &str, date: NaiveDate) -> Self {
163        self.preparer_id = id.into();
164        self.preparer_name = name.into();
165        self.preparer_date = date;
166        self
167    }
168
169    /// Add reviewer sign-off.
170    pub fn add_review(&mut self, id: &str, name: &str, date: NaiveDate) {
171        self.reviewer_id = Some(id.into());
172        self.reviewer_name = Some(name.into());
173        self.reviewer_date = Some(date);
174        self.status = JudgmentStatus::Reviewed;
175        self.updated_at = Utc::now();
176    }
177
178    /// Add partner concurrence.
179    pub fn add_partner_concurrence(&mut self, id: &str, date: NaiveDate) {
180        self.partner_concurrence_id = Some(id.into());
181        self.partner_concurrence_date = Some(date);
182        self.status = JudgmentStatus::Approved;
183        self.updated_at = Utc::now();
184    }
185
186    /// Add consultation record.
187    pub fn add_consultation(&mut self, consultation: ConsultationRecord) {
188        self.consultation = Some(consultation);
189        self.updated_at = Utc::now();
190    }
191
192    /// Check if judgment is fully approved.
193    pub fn is_approved(&self) -> bool {
194        let reviewer_ok = self.reviewer_id.is_some();
195        let partner_ok =
196            !self.partner_concurrence_required || self.partner_concurrence_id.is_some();
197        let consultation_ok = !self.consultation_required || self.consultation.is_some();
198        reviewer_ok && partner_ok && consultation_ok
199    }
200}
201
202/// Type of professional judgment.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
204#[serde(rename_all = "snake_case")]
205pub enum JudgmentType {
206    /// Materiality determination
207    MaterialityDetermination,
208    /// Risk assessment judgment
209    #[default]
210    RiskAssessment,
211    /// Control evaluation
212    ControlEvaluation,
213    /// Accounting estimate evaluation
214    EstimateEvaluation,
215    /// Going concern assessment
216    GoingConcern,
217    /// Misstatement evaluation
218    MisstatementEvaluation,
219    /// Audit report modification decision
220    ReportingDecision,
221    /// Sampling design judgment
222    SamplingDesign,
223    /// Related party judgment
224    RelatedPartyAssessment,
225    /// Subsequent events evaluation
226    SubsequentEvents,
227    /// Fraud risk assessment
228    FraudRiskAssessment,
229}
230
231impl JudgmentType {
232    /// Get default applicable standards.
233    pub fn default_standards(&self) -> Vec<String> {
234        match self {
235            Self::MaterialityDetermination => vec!["ISA 320".into(), "ISA 450".into()],
236            Self::RiskAssessment => vec!["ISA 315".into()],
237            Self::ControlEvaluation => vec!["ISA 330".into(), "ISA 265".into()],
238            Self::EstimateEvaluation => vec!["ISA 540".into()],
239            Self::GoingConcern => vec!["ISA 570".into()],
240            Self::MisstatementEvaluation => vec!["ISA 450".into()],
241            Self::ReportingDecision => vec!["ISA 700".into(), "ISA 705".into(), "ISA 706".into()],
242            Self::SamplingDesign => vec!["ISA 530".into()],
243            Self::RelatedPartyAssessment => vec!["ISA 550".into()],
244            Self::SubsequentEvents => vec!["ISA 560".into()],
245            Self::FraudRiskAssessment => vec!["ISA 240".into()],
246        }
247    }
248
249    /// Check if this type typically requires consultation.
250    pub fn typically_requires_consultation(&self) -> bool {
251        matches!(
252            self,
253            Self::GoingConcern
254                | Self::ReportingDecision
255                | Self::FraudRiskAssessment
256                | Self::EstimateEvaluation
257        )
258    }
259
260    /// Check if this type requires partner concurrence.
261    pub fn requires_partner_concurrence(&self) -> bool {
262        matches!(
263            self,
264            Self::MaterialityDetermination
265                | Self::GoingConcern
266                | Self::ReportingDecision
267                | Self::FraudRiskAssessment
268        )
269    }
270}
271
272/// Information item considered in judgment.
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct InformationItem {
275    /// Item ID
276    pub item_id: Uuid,
277    /// Description of information
278    pub description: String,
279    /// Source of information
280    pub source: String,
281    /// Reliability assessment
282    pub reliability: InformationReliability,
283    /// Relevance to the judgment
284    pub relevance: String,
285    /// Weight given in analysis
286    pub weight: InformationWeight,
287}
288
289impl InformationItem {
290    /// Create a new information item.
291    pub fn new(
292        description: &str,
293        source: &str,
294        reliability: InformationReliability,
295        relevance: &str,
296    ) -> Self {
297        Self {
298            item_id: Uuid::new_v4(),
299            description: description.into(),
300            source: source.into(),
301            reliability,
302            relevance: relevance.into(),
303            weight: InformationWeight::Moderate,
304        }
305    }
306
307    /// Set the weight.
308    pub fn with_weight(mut self, weight: InformationWeight) -> Self {
309        self.weight = weight;
310        self
311    }
312}
313
314/// Reliability of information.
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
316#[serde(rename_all = "snake_case")]
317pub enum InformationReliability {
318    /// High reliability
319    High,
320    /// Medium reliability
321    #[default]
322    Medium,
323    /// Low reliability
324    Low,
325}
326
327/// Weight given to information.
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
329#[serde(rename_all = "snake_case")]
330pub enum InformationWeight {
331    /// High weight - primary factor
332    High,
333    /// Moderate weight
334    #[default]
335    Moderate,
336    /// Low weight - secondary factor
337    Low,
338    /// Not weighted - for context only
339    Context,
340}
341
342/// Alternative evaluation in judgment process.
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct AlternativeEvaluation {
345    /// Alternative ID
346    pub alternative_id: Uuid,
347    /// Description of alternative
348    pub description: String,
349    /// Pros/advantages
350    pub pros: Vec<String>,
351    /// Cons/disadvantages
352    pub cons: Vec<String>,
353    /// Risk level if chosen
354    pub risk_level: RiskLevel,
355    /// Was this alternative selected?
356    pub selected: bool,
357    /// Reason if not selected
358    pub rejection_reason: Option<String>,
359}
360
361impl AlternativeEvaluation {
362    /// Create a new alternative evaluation.
363    pub fn new(description: &str, pros: Vec<String>, cons: Vec<String>) -> Self {
364        Self {
365            alternative_id: Uuid::new_v4(),
366            description: description.into(),
367            pros,
368            cons,
369            risk_level: RiskLevel::Medium,
370            selected: false,
371            rejection_reason: None,
372        }
373    }
374
375    /// Mark as selected.
376    pub fn select(mut self) -> Self {
377        self.selected = true;
378        self
379    }
380
381    /// Mark as rejected with reason.
382    pub fn reject(mut self, reason: &str) -> Self {
383        self.selected = false;
384        self.rejection_reason = Some(reason.into());
385        self
386    }
387}
388
389/// Documentation of professional skepticism.
390#[derive(Debug, Clone, Serialize, Deserialize, Default)]
391pub struct SkepticismDocumentation {
392    /// Contradictory evidence considered
393    pub contradictory_evidence_considered: Vec<String>,
394    /// Management bias indicators evaluated
395    pub management_bias_indicators: Vec<String>,
396    /// Alternative explanations explored
397    pub alternative_explanations: Vec<String>,
398    /// Challenging questions asked
399    pub challenging_questions: Vec<String>,
400    /// Corroboration obtained
401    pub corroboration_obtained: String,
402    /// Overall skepticism assessment
403    pub skepticism_assessment: String,
404}
405
406impl SkepticismDocumentation {
407    /// Create skepticism documentation.
408    pub fn new(assessment: &str) -> Self {
409        Self {
410            skepticism_assessment: assessment.into(),
411            ..Default::default()
412        }
413    }
414
415    /// Add contradictory evidence.
416    pub fn with_contradictory_evidence(mut self, evidence: Vec<String>) -> Self {
417        self.contradictory_evidence_considered = evidence;
418        self
419    }
420
421    /// Add management bias indicators.
422    pub fn with_bias_indicators(mut self, indicators: Vec<String>) -> Self {
423        self.management_bias_indicators = indicators;
424        self
425    }
426
427    /// Add alternative explanations.
428    pub fn with_alternatives(mut self, alternatives: Vec<String>) -> Self {
429        self.alternative_explanations = alternatives;
430        self
431    }
432}
433
434/// Consultation record.
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct ConsultationRecord {
437    /// Consultation ID
438    pub consultation_id: Uuid,
439    /// Consultant (internal or external)
440    pub consultant: String,
441    /// Consultant title/role
442    pub consultant_role: String,
443    /// Is external consultant?
444    pub is_external: bool,
445    /// Date of consultation
446    pub consultation_date: NaiveDate,
447    /// Issue presented
448    pub issue_presented: String,
449    /// Advice received
450    pub advice_received: String,
451    /// How advice was applied
452    pub advice_application: String,
453    /// Consultation conclusion
454    pub conclusion: String,
455}
456
457impl ConsultationRecord {
458    /// Create a new consultation record.
459    pub fn new(consultant: &str, role: &str, is_external: bool, date: NaiveDate) -> Self {
460        Self {
461            consultation_id: Uuid::new_v4(),
462            consultant: consultant.into(),
463            consultant_role: role.into(),
464            is_external,
465            consultation_date: date,
466            issue_presented: String::new(),
467            advice_received: String::new(),
468            advice_application: String::new(),
469            conclusion: String::new(),
470        }
471    }
472
473    /// Set the issue and advice.
474    pub fn with_content(
475        mut self,
476        issue: &str,
477        advice: &str,
478        application: &str,
479        conclusion: &str,
480    ) -> Self {
481        self.issue_presented = issue.into();
482        self.advice_received = advice.into();
483        self.advice_application = application.into();
484        self.conclusion = conclusion.into();
485        self
486    }
487}
488
489/// Judgment status.
490#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
491#[serde(rename_all = "snake_case")]
492pub enum JudgmentStatus {
493    /// Draft
494    #[default]
495    Draft,
496    /// Pending review
497    PendingReview,
498    /// Reviewed
499    Reviewed,
500    /// Pending consultation
501    PendingConsultation,
502    /// Pending partner concurrence
503    PendingPartnerConcurrence,
504    /// Approved
505    Approved,
506    /// Superseded
507    Superseded,
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    #[test]
515    fn test_judgment_creation() {
516        let judgment = ProfessionalJudgment::new(
517            Uuid::new_v4(),
518            JudgmentType::MaterialityDetermination,
519            "Overall audit materiality",
520        )
521        .with_issue("Determination of materiality for the 2025 audit")
522        .with_conclusion(
523            "Materiality set at $1M based on 0.5% of revenue",
524            "Revenue is stable metric and primary KPI for stakeholders",
525            "Risk of material misstatement below $1M not individually evaluated",
526        );
527
528        assert_eq!(
529            judgment.judgment_type,
530            JudgmentType::MaterialityDetermination
531        );
532        assert!(judgment.partner_concurrence_required);
533    }
534
535    #[test]
536    fn test_information_item() {
537        let item = InformationItem::new(
538            "Prior year financial statements",
539            "Audited FS",
540            InformationReliability::High,
541            "Baseline for trend analysis",
542        )
543        .with_weight(InformationWeight::High);
544
545        assert_eq!(item.reliability, InformationReliability::High);
546        assert_eq!(item.weight, InformationWeight::High);
547    }
548
549    #[test]
550    fn test_alternative_evaluation() {
551        let selected = AlternativeEvaluation::new(
552            "Use revenue as materiality base",
553            vec!["Stable metric".into(), "Primary KPI".into()],
554            vec!["May not capture asset-focused risks".into()],
555        )
556        .select();
557
558        let rejected = AlternativeEvaluation::new(
559            "Use total assets as materiality base",
560            vec!["Captures balance sheet risks".into()],
561            vec!["Assets less stable".into()],
562        )
563        .reject("Revenue more relevant to stakeholders");
564
565        assert!(selected.selected);
566        assert!(!rejected.selected);
567        assert!(rejected.rejection_reason.is_some());
568    }
569
570    #[test]
571    fn test_judgment_approval() {
572        let mut judgment = ProfessionalJudgment::new(
573            Uuid::new_v4(),
574            JudgmentType::RiskAssessment,
575            "Overall risk assessment",
576        );
577
578        // Not approved initially
579        assert!(!judgment.is_approved());
580
581        // Add reviewer
582        judgment.add_review(
583            "reviewer1",
584            "Senior Manager",
585            NaiveDate::from_ymd_opt(2025, 1, 15).unwrap(),
586        );
587
588        // Risk assessment doesn't require partner concurrence
589        assert!(judgment.is_approved());
590    }
591
592    #[test]
593    fn test_judgment_types() {
594        assert!(JudgmentType::GoingConcern.requires_partner_concurrence());
595        assert!(JudgmentType::GoingConcern.typically_requires_consultation());
596        assert!(!JudgmentType::SamplingDesign.requires_partner_concurrence());
597    }
598}