Skip to main content

datasynth_standards/audit/
analytical.rs

1//! Analytical Procedures (ISA 520).
2//!
3//! Implements analytical procedures used throughout the audit:
4//! - Risk assessment procedures
5//! - Substantive analytical procedures
6//! - Final analytical review
7//!
8//! Analytical procedures involve evaluating financial information through
9//! analysis of plausible relationships among both financial and non-financial data.
10
11use chrono::NaiveDate;
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16/// Analytical procedure record.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AnalyticalProcedure {
19    /// Unique procedure identifier.
20    pub procedure_id: Uuid,
21
22    /// Engagement ID.
23    pub engagement_id: Uuid,
24
25    /// Account or area being analyzed.
26    pub account_area: String,
27
28    /// Type of analytical procedure.
29    pub procedure_type: AnalyticalProcedureType,
30
31    /// Purpose of the procedure.
32    pub purpose: AnalyticalPurpose,
33
34    /// Expectation developed by auditor.
35    pub expectation: AnalyticalExpectation,
36
37    /// Actual recorded value.
38    #[serde(with = "rust_decimal::serde::str")]
39    pub actual_value: Decimal,
40
41    /// Variance from expectation.
42    #[serde(with = "rust_decimal::serde::str")]
43    pub variance: Decimal,
44
45    /// Variance as percentage.
46    #[serde(with = "rust_decimal::serde::str")]
47    pub variance_percent: Decimal,
48
49    /// Threshold for investigation.
50    #[serde(with = "rust_decimal::serde::str")]
51    pub investigation_threshold: Decimal,
52
53    /// Whether variance exceeds threshold.
54    pub exceeds_threshold: bool,
55
56    /// Investigation details if variance exceeded threshold.
57    pub investigation: Option<VarianceInvestigation>,
58
59    /// Conclusion from the procedure.
60    pub conclusion: AnalyticalConclusion,
61
62    /// Procedure date.
63    pub procedure_date: NaiveDate,
64
65    /// Preparer ID.
66    pub prepared_by: String,
67
68    /// Reviewer ID.
69    pub reviewed_by: Option<String>,
70
71    /// Workpaper reference.
72    pub workpaper_reference: Option<String>,
73}
74
75impl AnalyticalProcedure {
76    /// Create a new analytical procedure.
77    pub fn new(
78        engagement_id: Uuid,
79        account_area: impl Into<String>,
80        procedure_type: AnalyticalProcedureType,
81        purpose: AnalyticalPurpose,
82    ) -> Self {
83        Self {
84            procedure_id: Uuid::now_v7(),
85            engagement_id,
86            account_area: account_area.into(),
87            procedure_type,
88            purpose,
89            expectation: AnalyticalExpectation::default(),
90            actual_value: Decimal::ZERO,
91            variance: Decimal::ZERO,
92            variance_percent: Decimal::ZERO,
93            investigation_threshold: Decimal::ZERO,
94            exceeds_threshold: false,
95            investigation: None,
96            conclusion: AnalyticalConclusion::NotCompleted,
97            procedure_date: chrono::Utc::now().date_naive(),
98            prepared_by: String::new(),
99            reviewed_by: None,
100            workpaper_reference: None,
101        }
102    }
103
104    /// Calculate variance from expectation.
105    pub fn calculate_variance(&mut self) {
106        self.variance = self.actual_value - self.expectation.expected_value;
107
108        // Calculate percentage variance
109        if self.expectation.expected_value != Decimal::ZERO {
110            self.variance_percent =
111                (self.variance / self.expectation.expected_value) * Decimal::from(100);
112        } else {
113            self.variance_percent = Decimal::ZERO;
114        }
115
116        // Check if exceeds threshold
117        self.exceeds_threshold = self.variance.abs() > self.investigation_threshold;
118    }
119
120    /// Determine if investigation is required.
121    pub fn requires_investigation(&self) -> bool {
122        self.exceeds_threshold
123    }
124}
125
126/// Type of analytical procedure.
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
128#[serde(rename_all = "snake_case")]
129pub enum AnalyticalProcedureType {
130    /// Trend analysis (comparison over time).
131    #[default]
132    Trend,
133    /// Ratio analysis (financial ratios).
134    Ratio,
135    /// Reasonableness test (expectation model).
136    Reasonableness,
137    /// Regression analysis.
138    Regression,
139    /// Comparison to budget or forecast.
140    BudgetComparison,
141    /// Industry comparison.
142    IndustryComparison,
143    /// Non-financial to financial relationship.
144    NonFinancialRelationship,
145}
146
147impl std::fmt::Display for AnalyticalProcedureType {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        match self {
150            Self::Trend => write!(f, "Trend Analysis"),
151            Self::Ratio => write!(f, "Ratio Analysis"),
152            Self::Reasonableness => write!(f, "Reasonableness Test"),
153            Self::Regression => write!(f, "Regression Analysis"),
154            Self::BudgetComparison => write!(f, "Budget Comparison"),
155            Self::IndustryComparison => write!(f, "Industry Comparison"),
156            Self::NonFinancialRelationship => write!(f, "Non-Financial Relationship"),
157        }
158    }
159}
160
161/// Purpose of analytical procedure.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
163#[serde(rename_all = "snake_case")]
164pub enum AnalyticalPurpose {
165    /// Used during risk assessment (ISA 315).
166    #[default]
167    RiskAssessment,
168    /// Used as substantive procedure (ISA 520).
169    Substantive,
170    /// Used during final review (ISA 520).
171    FinalReview,
172}
173
174impl std::fmt::Display for AnalyticalPurpose {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        match self {
177            Self::RiskAssessment => write!(f, "Risk Assessment"),
178            Self::Substantive => write!(f, "Substantive"),
179            Self::FinalReview => write!(f, "Final Review"),
180        }
181    }
182}
183
184/// Auditor's expectation for analytical procedure.
185#[derive(Debug, Clone, Default, Serialize, Deserialize)]
186pub struct AnalyticalExpectation {
187    /// Expected value.
188    #[serde(with = "rust_decimal::serde::str")]
189    pub expected_value: Decimal,
190
191    /// Basis for the expectation.
192    pub expectation_basis: ExpectationBasis,
193
194    /// Description of how expectation was developed.
195    pub methodology: String,
196
197    /// Reliability level of underlying data.
198    pub data_reliability: ReliabilityLevel,
199
200    /// Precision of the expectation.
201    pub precision_level: PrecisionLevel,
202
203    /// Key assumptions made.
204    pub key_assumptions: Vec<String>,
205
206    /// Data sources used.
207    pub data_sources: Vec<String>,
208}
209
210impl AnalyticalExpectation {
211    /// Create a new expectation.
212    pub fn new(expected_value: Decimal, expectation_basis: ExpectationBasis) -> Self {
213        Self {
214            expected_value,
215            expectation_basis,
216            methodology: String::new(),
217            data_reliability: ReliabilityLevel::default(),
218            precision_level: PrecisionLevel::default(),
219            key_assumptions: Vec::new(),
220            data_sources: Vec::new(),
221        }
222    }
223}
224
225/// Basis for developing expectation.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
227#[serde(rename_all = "snake_case")]
228pub enum ExpectationBasis {
229    /// Based on prior period amounts.
230    #[default]
231    PriorPeriod,
232    /// Based on budget or forecast.
233    Budget,
234    /// Based on industry data.
235    Industry,
236    /// Based on non-financial data.
237    NonFinancial,
238    /// Based on statistical model.
239    StatisticalModel,
240    /// Based on auditor's independent calculation.
241    IndependentCalculation,
242    /// Based on interim period results.
243    InterimResults,
244}
245
246impl std::fmt::Display for ExpectationBasis {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        match self {
249            Self::PriorPeriod => write!(f, "Prior Period"),
250            Self::Budget => write!(f, "Budget/Forecast"),
251            Self::Industry => write!(f, "Industry Data"),
252            Self::NonFinancial => write!(f, "Non-Financial Data"),
253            Self::StatisticalModel => write!(f, "Statistical Model"),
254            Self::IndependentCalculation => write!(f, "Independent Calculation"),
255            Self::InterimResults => write!(f, "Interim Results"),
256        }
257    }
258}
259
260/// Reliability level of underlying data.
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
262#[serde(rename_all = "snake_case")]
263pub enum ReliabilityLevel {
264    /// Low reliability (e.g., management estimates).
265    Low,
266    /// Moderate reliability (e.g., unaudited internal data).
267    #[default]
268    Moderate,
269    /// High reliability (e.g., audited data, external sources).
270    High,
271}
272
273impl std::fmt::Display for ReliabilityLevel {
274    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275        match self {
276            Self::Low => write!(f, "Low"),
277            Self::Moderate => write!(f, "Moderate"),
278            Self::High => write!(f, "High"),
279        }
280    }
281}
282
283/// Precision level of the expectation.
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
285#[serde(rename_all = "snake_case")]
286pub enum PrecisionLevel {
287    /// Low precision (general reasonableness).
288    Low,
289    /// Moderate precision.
290    #[default]
291    Moderate,
292    /// High precision (detailed calculation).
293    High,
294}
295
296/// Variance investigation details.
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct VarianceInvestigation {
299    /// Investigation ID.
300    pub investigation_id: Uuid,
301
302    /// Explanation obtained for variance.
303    pub explanation: String,
304
305    /// Source of explanation (e.g., management, documentation).
306    pub explanation_source: String,
307
308    /// Whether explanation was corroborated.
309    pub corroborated: bool,
310
311    /// Corroborating evidence obtained.
312    pub corroborating_evidence: Vec<String>,
313
314    /// Additional procedures performed.
315    pub additional_procedures: Vec<String>,
316
317    /// Whether variance is explained and reasonable.
318    pub variance_explained: bool,
319
320    /// Misstatement identified, if any.
321    #[serde(default, with = "rust_decimal::serde::str_option")]
322    pub misstatement_amount: Option<Decimal>,
323
324    /// Investigation conclusion.
325    pub conclusion: InvestigationConclusion,
326}
327
328impl VarianceInvestigation {
329    /// Create a new variance investigation.
330    pub fn new() -> Self {
331        Self {
332            investigation_id: Uuid::now_v7(),
333            explanation: String::new(),
334            explanation_source: String::new(),
335            corroborated: false,
336            corroborating_evidence: Vec::new(),
337            additional_procedures: Vec::new(),
338            variance_explained: false,
339            misstatement_amount: None,
340            conclusion: InvestigationConclusion::NotCompleted,
341        }
342    }
343}
344
345impl Default for VarianceInvestigation {
346    fn default() -> Self {
347        Self::new()
348    }
349}
350
351/// Investigation conclusion.
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
353#[serde(rename_all = "snake_case")]
354pub enum InvestigationConclusion {
355    /// Investigation not completed.
356    #[default]
357    NotCompleted,
358    /// Variance is explained and reasonable.
359    Explained,
360    /// Variance is explained but may indicate misstatement.
361    PotentialMisstatement,
362    /// Misstatement identified.
363    MisstatementIdentified,
364    /// Unable to explain variance.
365    UnableToExplain,
366}
367
368/// Conclusion from analytical procedure.
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
370#[serde(rename_all = "snake_case")]
371pub enum AnalyticalConclusion {
372    /// Procedure not completed.
373    #[default]
374    NotCompleted,
375    /// Results consistent with expectations, no further work needed.
376    Consistent,
377    /// Results inconsistent, investigation performed, variance explained.
378    InvestigatedAndExplained,
379    /// Results indicate potential misstatement, requires follow-up.
380    PotentialMisstatement,
381    /// Misstatement identified.
382    MisstatementIdentified,
383    /// Unable to form conclusion, alternative procedures needed.
384    Inconclusive,
385}
386
387impl std::fmt::Display for AnalyticalConclusion {
388    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389        match self {
390            Self::NotCompleted => write!(f, "Not Completed"),
391            Self::Consistent => write!(f, "Consistent with Expectations"),
392            Self::InvestigatedAndExplained => write!(f, "Investigated and Explained"),
393            Self::PotentialMisstatement => write!(f, "Potential Misstatement"),
394            Self::MisstatementIdentified => write!(f, "Misstatement Identified"),
395            Self::Inconclusive => write!(f, "Inconclusive"),
396        }
397    }
398}
399
400/// Common financial ratio types.
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
402#[serde(rename_all = "snake_case")]
403pub enum FinancialRatio {
404    // Profitability Ratios
405    GrossMargin,
406    OperatingMargin,
407    NetProfitMargin,
408    ReturnOnAssets,
409    ReturnOnEquity,
410
411    // Liquidity Ratios
412    CurrentRatio,
413    QuickRatio,
414    CashRatio,
415
416    // Activity/Efficiency Ratios
417    InventoryTurnover,
418    ReceivablesTurnover,
419    PayablesTurnover,
420    AssetTurnover,
421    DaysSalesOutstanding,
422    DaysPayablesOutstanding,
423    DaysInventoryOnHand,
424
425    // Leverage Ratios
426    DebtToEquity,
427    DebtToAssets,
428    InterestCoverage,
429
430    // Other
431    Custom,
432}
433
434impl std::fmt::Display for FinancialRatio {
435    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436        match self {
437            Self::GrossMargin => write!(f, "Gross Margin"),
438            Self::OperatingMargin => write!(f, "Operating Margin"),
439            Self::NetProfitMargin => write!(f, "Net Profit Margin"),
440            Self::ReturnOnAssets => write!(f, "Return on Assets"),
441            Self::ReturnOnEquity => write!(f, "Return on Equity"),
442            Self::CurrentRatio => write!(f, "Current Ratio"),
443            Self::QuickRatio => write!(f, "Quick Ratio"),
444            Self::CashRatio => write!(f, "Cash Ratio"),
445            Self::InventoryTurnover => write!(f, "Inventory Turnover"),
446            Self::ReceivablesTurnover => write!(f, "Receivables Turnover"),
447            Self::PayablesTurnover => write!(f, "Payables Turnover"),
448            Self::AssetTurnover => write!(f, "Asset Turnover"),
449            Self::DaysSalesOutstanding => write!(f, "Days Sales Outstanding"),
450            Self::DaysPayablesOutstanding => write!(f, "Days Payables Outstanding"),
451            Self::DaysInventoryOnHand => write!(f, "Days Inventory on Hand"),
452            Self::DebtToEquity => write!(f, "Debt to Equity"),
453            Self::DebtToAssets => write!(f, "Debt to Assets"),
454            Self::InterestCoverage => write!(f, "Interest Coverage"),
455            Self::Custom => write!(f, "Custom Ratio"),
456        }
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463    use rust_decimal_macros::dec;
464
465    #[test]
466    fn test_analytical_procedure_creation() {
467        let procedure = AnalyticalProcedure::new(
468            Uuid::now_v7(),
469            "Revenue",
470            AnalyticalProcedureType::Trend,
471            AnalyticalPurpose::Substantive,
472        );
473
474        assert_eq!(procedure.account_area, "Revenue");
475        assert_eq!(procedure.procedure_type, AnalyticalProcedureType::Trend);
476        assert_eq!(procedure.conclusion, AnalyticalConclusion::NotCompleted);
477    }
478
479    #[test]
480    fn test_variance_calculation() {
481        let mut procedure = AnalyticalProcedure::new(
482            Uuid::now_v7(),
483            "Revenue",
484            AnalyticalProcedureType::Trend,
485            AnalyticalPurpose::Substantive,
486        );
487
488        procedure.expectation =
489            AnalyticalExpectation::new(dec!(100000), ExpectationBasis::PriorPeriod);
490        procedure.actual_value = dec!(110000);
491        procedure.investigation_threshold = dec!(5000);
492
493        procedure.calculate_variance();
494
495        assert_eq!(procedure.variance, dec!(10000));
496        assert_eq!(procedure.variance_percent, dec!(10));
497        assert!(procedure.exceeds_threshold);
498    }
499
500    #[test]
501    fn test_variance_within_threshold() {
502        let mut procedure = AnalyticalProcedure::new(
503            Uuid::now_v7(),
504            "Cost of Sales",
505            AnalyticalProcedureType::Reasonableness,
506            AnalyticalPurpose::FinalReview,
507        );
508
509        procedure.expectation =
510            AnalyticalExpectation::new(dec!(50000), ExpectationBasis::IndependentCalculation);
511        procedure.actual_value = dec!(51000);
512        procedure.investigation_threshold = dec!(2500);
513
514        procedure.calculate_variance();
515
516        assert_eq!(procedure.variance, dec!(1000));
517        assert!(!procedure.exceeds_threshold);
518    }
519}