datasynth_core/models/audit/going_concern.rs
1//! Going concern assessment models — ISA 570 / ASC 205-40.
2//!
3//! Going concern is a fundamental assumption underlying the preparation of
4//! financial statements. Auditors are required (ISA 570) to evaluate whether
5//! there is a material uncertainty about the entity's ability to continue as a
6//! going concern over the next twelve months.
7
8use chrono::NaiveDate;
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12// ---------------------------------------------------------------------------
13// Enums
14// ---------------------------------------------------------------------------
15
16/// Type of going concern indicator identified during the assessment.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum GoingConcernIndicatorType {
20 /// Entity has sustained operating losses over multiple periods.
21 RecurringOperatingLosses,
22 /// Net cash outflows from operations over one or more periods.
23 NegativeOperatingCashFlow,
24 /// Current liabilities exceed current assets (negative working capital).
25 WorkingCapitalDeficiency,
26 /// One or more financial covenants have been breached.
27 DebtCovenantBreach,
28 /// Departure of a major customer materially impacting revenue.
29 LossOfKeyCustomer,
30 /// Regulatory action threatening the entity's licence to operate.
31 RegulatoryAction,
32 /// Material litigation exposure that could threaten solvency.
33 LitigationExposure,
34 /// Entity has been unable to refinance or obtain new credit facilities.
35 InabilityToObtainFinancing,
36}
37
38impl std::fmt::Display for GoingConcernIndicatorType {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 let s = match self {
41 Self::RecurringOperatingLosses => "Recurring Operating Losses",
42 Self::NegativeOperatingCashFlow => "Negative Operating Cash Flow",
43 Self::WorkingCapitalDeficiency => "Working Capital Deficiency",
44 Self::DebtCovenantBreach => "Debt Covenant Breach",
45 Self::LossOfKeyCustomer => "Loss of Key Customer",
46 Self::RegulatoryAction => "Regulatory Action",
47 Self::LitigationExposure => "Litigation Exposure",
48 Self::InabilityToObtainFinancing => "Inability to Obtain Financing",
49 };
50 write!(f, "{s}")
51 }
52}
53
54/// Severity classification of a going concern indicator.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum GoingConcernSeverity {
58 /// Indicator present but manageable; unlikely to threaten continuity alone.
59 Low,
60 /// Indicator creates meaningful doubt; mitigating plans are required.
61 Medium,
62 /// Indicator poses a serious threat to the entity's continuity.
63 High,
64}
65
66/// Auditor's overall conclusion on going concern.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum GoingConcernConclusion {
70 /// No material uncertainty exists; going concern basis is appropriate.
71 #[default]
72 NoMaterialUncertainty,
73 /// A material uncertainty exists and is adequately disclosed (ISA 570.22).
74 MaterialUncertaintyExists,
75 /// Significant doubt that the entity is a going concern (ASC 205-40).
76 GoingConcernDoubt,
77}
78
79// ---------------------------------------------------------------------------
80// Indicator
81// ---------------------------------------------------------------------------
82
83/// A single going concern indicator identified during the assessment.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct GoingConcernIndicator {
86 /// Nature of the indicator.
87 pub indicator_type: GoingConcernIndicatorType,
88 /// Assessed severity.
89 pub severity: GoingConcernSeverity,
90 /// Narrative description of the specific circumstances.
91 pub description: String,
92 /// Quantitative measure associated with the indicator (e.g. net loss amount).
93 #[serde(
94 with = "rust_decimal::serde::str_option",
95 skip_serializing_if = "Option::is_none",
96 default
97 )]
98 pub quantitative_measure: Option<Decimal>,
99 /// Threshold at which the indicator becomes critical (e.g. covenant limit).
100 #[serde(
101 with = "rust_decimal::serde::str_option",
102 skip_serializing_if = "Option::is_none",
103 default
104 )]
105 pub threshold: Option<Decimal>,
106}
107
108// ---------------------------------------------------------------------------
109// Assessment
110// ---------------------------------------------------------------------------
111
112/// Going concern assessment prepared for a single entity and reporting period.
113///
114/// One assessment is generated per entity per period. The auditor's
115/// conclusion is driven by the number and severity of indicators identified.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct GoingConcernAssessment {
118 /// Entity code of the assessed entity.
119 pub entity_code: String,
120 /// Date on which the assessment was finalised.
121 pub assessment_date: NaiveDate,
122 /// Human-readable period descriptor (e.g. "FY2024").
123 pub assessment_period: String,
124 /// Indicators identified during the assessment (may be empty).
125 pub indicators: Vec<GoingConcernIndicator>,
126 /// Management's plans to address the identified indicators.
127 pub management_plans: Vec<String>,
128 /// Auditor's overall conclusion.
129 pub auditor_conclusion: GoingConcernConclusion,
130 /// Whether a material uncertainty paragraph is required in the audit report.
131 pub material_uncertainty_exists: bool,
132}
133
134impl GoingConcernAssessment {
135 /// Derive the conclusion and `material_uncertainty_exists` flag from the
136 /// number of indicators present.
137 ///
138 /// | Indicator count | Conclusion | Material uncertainty |
139 /// |-----------------|-----------------------------|----------------------|
140 /// | 0 | `NoMaterialUncertainty` | false |
141 /// | 1–2 | `MaterialUncertaintyExists` | true |
142 /// | 3+ | `GoingConcernDoubt` | true |
143 pub fn conclude_from_indicators(mut self) -> Self {
144 let n = self.indicators.len();
145 self.auditor_conclusion = match n {
146 0 => GoingConcernConclusion::NoMaterialUncertainty,
147 1..=2 => GoingConcernConclusion::MaterialUncertaintyExists,
148 _ => GoingConcernConclusion::GoingConcernDoubt,
149 };
150 self.material_uncertainty_exists = n > 0;
151 self
152 }
153}