1use chrono::{DateTime, NaiveDate, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::engagement::RiskLevel;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ProfessionalJudgment {
15 pub judgment_id: Uuid,
17 pub judgment_ref: String,
19 pub engagement_id: Uuid,
21 pub judgment_type: JudgmentType,
23 pub subject: String,
25 pub applicable_standards: Vec<String>,
27
28 pub issue_description: String,
31 pub information_considered: Vec<InformationItem>,
33 pub alternatives_evaluated: Vec<AlternativeEvaluation>,
35 pub skepticism_applied: SkepticismDocumentation,
37
38 pub conclusion: String,
41 pub rationale: String,
43 pub residual_risk: String,
45 pub impact_on_audit: Option<String>,
47
48 pub consultation_required: bool,
51 pub consultation: Option<ConsultationRecord>,
53
54 pub preparer_id: String,
57 pub preparer_name: String,
59 pub preparer_date: NaiveDate,
61 pub reviewer_id: Option<String>,
63 pub reviewer_name: Option<String>,
65 pub reviewer_date: Option<NaiveDate>,
67 pub partner_concurrence_required: bool,
69 pub partner_concurrence_id: Option<String>,
71 pub partner_concurrence_date: Option<NaiveDate>,
73
74 pub workpaper_refs: Vec<Uuid>,
77 pub evidence_refs: Vec<Uuid>,
79
80 pub status: JudgmentStatus,
82 pub created_at: DateTime<Utc>,
83 pub updated_at: DateTime<Utc>,
84}
85
86impl ProfessionalJudgment {
87 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 pub fn with_issue(mut self, issue: &str) -> Self {
126 self.issue_description = issue.into();
127 self
128 }
129
130 pub fn add_information(&mut self, item: InformationItem) {
132 self.information_considered.push(item);
133 self.updated_at = Utc::now();
134 }
135
136 pub fn add_alternative(&mut self, alternative: AlternativeEvaluation) {
138 self.alternatives_evaluated.push(alternative);
139 self.updated_at = Utc::now();
140 }
141
142 pub fn with_skepticism(mut self, skepticism: SkepticismDocumentation) -> Self {
144 self.skepticism_applied = skepticism;
145 self
146 }
147
148 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 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 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 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 pub fn add_consultation(&mut self, consultation: ConsultationRecord) {
188 self.consultation = Some(consultation);
189 self.updated_at = Utc::now();
190 }
191
192 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
204#[serde(rename_all = "snake_case")]
205pub enum JudgmentType {
206 MaterialityDetermination,
208 #[default]
210 RiskAssessment,
211 ControlEvaluation,
213 EstimateEvaluation,
215 GoingConcern,
217 MisstatementEvaluation,
219 ReportingDecision,
221 SamplingDesign,
223 RelatedPartyAssessment,
225 SubsequentEvents,
227 FraudRiskAssessment,
229}
230
231impl JudgmentType {
232 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct InformationItem {
275 pub item_id: Uuid,
277 pub description: String,
279 pub source: String,
281 pub reliability: InformationReliability,
283 pub relevance: String,
285 pub weight: InformationWeight,
287}
288
289impl InformationItem {
290 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 pub fn with_weight(mut self, weight: InformationWeight) -> Self {
309 self.weight = weight;
310 self
311 }
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
316#[serde(rename_all = "snake_case")]
317pub enum InformationReliability {
318 High,
320 #[default]
322 Medium,
323 Low,
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
329#[serde(rename_all = "snake_case")]
330pub enum InformationWeight {
331 High,
333 #[default]
335 Moderate,
336 Low,
338 Context,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct AlternativeEvaluation {
345 pub alternative_id: Uuid,
347 pub description: String,
349 pub pros: Vec<String>,
351 pub cons: Vec<String>,
353 pub risk_level: RiskLevel,
355 pub selected: bool,
357 pub rejection_reason: Option<String>,
359}
360
361impl AlternativeEvaluation {
362 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 pub fn select(mut self) -> Self {
377 self.selected = true;
378 self
379 }
380
381 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
391pub struct SkepticismDocumentation {
392 pub contradictory_evidence_considered: Vec<String>,
394 pub management_bias_indicators: Vec<String>,
396 pub alternative_explanations: Vec<String>,
398 pub challenging_questions: Vec<String>,
400 pub corroboration_obtained: String,
402 pub skepticism_assessment: String,
404}
405
406impl SkepticismDocumentation {
407 pub fn new(assessment: &str) -> Self {
409 Self {
410 skepticism_assessment: assessment.into(),
411 ..Default::default()
412 }
413 }
414
415 pub fn with_contradictory_evidence(mut self, evidence: Vec<String>) -> Self {
417 self.contradictory_evidence_considered = evidence;
418 self
419 }
420
421 pub fn with_bias_indicators(mut self, indicators: Vec<String>) -> Self {
423 self.management_bias_indicators = indicators;
424 self
425 }
426
427 pub fn with_alternatives(mut self, alternatives: Vec<String>) -> Self {
429 self.alternative_explanations = alternatives;
430 self
431 }
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct ConsultationRecord {
437 pub consultation_id: Uuid,
439 pub consultant: String,
441 pub consultant_role: String,
443 pub is_external: bool,
445 pub consultation_date: NaiveDate,
447 pub issue_presented: String,
449 pub advice_received: String,
451 pub advice_application: String,
453 pub conclusion: String,
455}
456
457impl ConsultationRecord {
458 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
491#[serde(rename_all = "snake_case")]
492pub enum JudgmentStatus {
493 #[default]
495 Draft,
496 PendingReview,
498 Reviewed,
500 PendingConsultation,
502 PendingPartnerConcurrence,
504 Approved,
506 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 assert!(!judgment.is_approved());
580
581 judgment.add_review(
583 "reviewer1",
584 "Senior Manager",
585 NaiveDate::from_ymd_opt(2025, 1, 15).unwrap(),
586 );
587
588 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}