Skip to main content

datasynth_core/models/audit/
analytical_relationships.rs

1//! Analytical relationship models for audit support data — ISA 520.
2//!
3//! Analytical procedures (ISA 520) require the auditor to develop expectations
4//! about plausible relationships between financial and non-financial data.
5//! This module captures those relationships in a structured form so that they
6//! can be output as training data and used by AI-assisted audit tools.
7//!
8//! Standard relationships computed per entity per period:
9//! - DSO (Days Sales Outstanding)
10//! - DPO (Days Payable Outstanding)
11//! - Inventory Turnover
12//! - Gross Margin
13//! - Payroll to Revenue
14//! - Depreciation to Gross Fixed Assets
15//! - Revenue Growth (period-on-period)
16//! - Operating Expense Ratio
17
18use rust_decimal::Decimal;
19use serde::{Deserialize, Serialize};
20
21// ---------------------------------------------------------------------------
22// Enums
23// ---------------------------------------------------------------------------
24
25/// The mathematical / statistical nature of the analytical relationship.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum RelationshipType {
29    /// A ratio between two financial line items (e.g. DSO = AR / Revenue × 365).
30    Ratio,
31    /// A period-on-period trend (e.g. revenue growth rate).
32    Trend,
33    /// A correlation between two time-series variables (e.g. revenue vs AR).
34    Correlation,
35    /// A reasonableness check — does the value fall within an expected range?
36    Reasonableness,
37}
38
39impl std::fmt::Display for RelationshipType {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        let s = match self {
42            Self::Ratio => "Ratio",
43            Self::Trend => "Trend",
44            Self::Correlation => "Correlation",
45            Self::Reasonableness => "Reasonableness",
46        };
47        write!(f, "{s}")
48    }
49}
50
51/// Reliability of the underlying data used to compute the relationship.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum DataReliability {
55    /// Data comes from a routine, fully-automated, previously-audited source.
56    High,
57    /// Data is semi-automated or subject to internal review but not audit.
58    Medium,
59    /// Data is manually compiled or unverified.
60    Low,
61}
62
63impl std::fmt::Display for DataReliability {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        let s = match self {
66            Self::High => "High",
67            Self::Medium => "Medium",
68            Self::Low => "Low",
69        };
70        write!(f, "{s}")
71    }
72}
73
74// ---------------------------------------------------------------------------
75// Supporting sub-structures
76// ---------------------------------------------------------------------------
77
78/// A single period's computed value for an analytical relationship.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct PeriodDataPoint {
81    /// Human-readable period label (e.g. "FY2024-Q3", "FY2023").
82    pub period: String,
83    /// Computed value for the relationship in this period.
84    #[serde(with = "rust_decimal::serde::str")]
85    pub value: Decimal,
86    /// Whether this is the current (under-audit) period.
87    pub is_current: bool,
88}
89
90/// A non-financial or operational metric that supports the analytical relationship.
91///
92/// Examples: headcount for payroll ratios, units shipped for revenue reasonableness.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct SupportingMetric {
95    /// Name of the metric (e.g. "Employee headcount", "Units shipped").
96    pub metric_name: String,
97    /// Metric value for the current period.
98    #[serde(with = "rust_decimal::serde::str")]
99    pub value: Decimal,
100    /// System or process from which the metric was sourced.
101    pub source: String,
102}
103
104// ---------------------------------------------------------------------------
105// Main struct
106// ---------------------------------------------------------------------------
107
108/// An analytical relationship computed from actual journal entry data.
109///
110/// Each relationship captures the formula, historical trend, expected range,
111/// and any variance explanation — providing the auditor with structured
112/// evidence to support or challenge the recorded amounts.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct AnalyticalRelationship {
115    /// Unique identifier for this relationship record.
116    pub id: String,
117    /// Entity / company code this relationship relates to.
118    pub entity_code: String,
119    /// Human-readable name (e.g. "Days Sales Outstanding", "Gross Margin").
120    pub relationship_name: String,
121    /// The account area or financial statement section (e.g. "Receivables", "Revenue").
122    pub account_area: String,
123    /// Mathematical / statistical type of the relationship.
124    pub relationship_type: RelationshipType,
125    /// Plain-English formula showing how the value is calculated.
126    /// Example: `"AR / Revenue * 365 = DSO"`
127    pub formula: String,
128    /// Computed values for the current period and 2–3 prior comparison periods.
129    pub periods: Vec<PeriodDataPoint>,
130    /// Expected range `(lower_bound, upper_bound)` based on industry norms.
131    /// The value is expressed in the same units as the ratio (e.g. days, %, ×).
132    pub expected_range: (String, String),
133    /// Explanation of why the current value is outside the expected range,
134    /// or `None` if it falls within range.
135    pub variance_explanation: Option<String>,
136    /// Non-financial supporting metrics used to corroborate the relationship.
137    pub supporting_metrics: Vec<SupportingMetric>,
138    /// Reliability of the data underlying this relationship.
139    pub reliability: DataReliability,
140    /// Whether the current period value is within the expected range.
141    pub within_expected_range: bool,
142}
143
144impl AnalyticalRelationship {
145    /// Return the current period data point (the one being audited).
146    pub fn current_period(&self) -> Option<&PeriodDataPoint> {
147        self.periods.iter().find(|p| p.is_current)
148    }
149}