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}