use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyticalProcedure {
pub procedure_id: Uuid,
pub engagement_id: Uuid,
pub account_area: String,
pub procedure_type: AnalyticalProcedureType,
pub purpose: AnalyticalPurpose,
pub expectation: AnalyticalExpectation,
#[serde(with = "datasynth_core::serde_decimal")]
pub actual_value: Decimal,
#[serde(with = "datasynth_core::serde_decimal")]
pub variance: Decimal,
#[serde(with = "datasynth_core::serde_decimal")]
pub variance_percent: Decimal,
#[serde(with = "datasynth_core::serde_decimal")]
pub investigation_threshold: Decimal,
pub exceeds_threshold: bool,
pub investigation: Option<VarianceInvestigation>,
pub conclusion: AnalyticalConclusion,
pub procedure_date: NaiveDate,
pub prepared_by: String,
pub reviewed_by: Option<String>,
pub workpaper_reference: Option<String>,
}
impl AnalyticalProcedure {
pub fn new(
engagement_id: Uuid,
account_area: impl Into<String>,
procedure_type: AnalyticalProcedureType,
purpose: AnalyticalPurpose,
) -> Self {
Self {
procedure_id: Uuid::now_v7(),
engagement_id,
account_area: account_area.into(),
procedure_type,
purpose,
expectation: AnalyticalExpectation::default(),
actual_value: Decimal::ZERO,
variance: Decimal::ZERO,
variance_percent: Decimal::ZERO,
investigation_threshold: Decimal::ZERO,
exceeds_threshold: false,
investigation: None,
conclusion: AnalyticalConclusion::NotCompleted,
procedure_date: chrono::Utc::now().date_naive(),
prepared_by: String::new(),
reviewed_by: None,
workpaper_reference: None,
}
}
pub fn calculate_variance(&mut self) {
self.variance = self.actual_value - self.expectation.expected_value;
if self.expectation.expected_value != Decimal::ZERO {
self.variance_percent =
(self.variance / self.expectation.expected_value) * Decimal::from(100);
} else {
self.variance_percent = Decimal::ZERO;
}
self.exceeds_threshold = self.variance.abs() > self.investigation_threshold;
}
pub fn requires_investigation(&self) -> bool {
self.exceeds_threshold
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AnalyticalProcedureType {
#[default]
Trend,
Ratio,
Reasonableness,
Regression,
BudgetComparison,
IndustryComparison,
NonFinancialRelationship,
}
impl std::fmt::Display for AnalyticalProcedureType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Trend => write!(f, "Trend Analysis"),
Self::Ratio => write!(f, "Ratio Analysis"),
Self::Reasonableness => write!(f, "Reasonableness Test"),
Self::Regression => write!(f, "Regression Analysis"),
Self::BudgetComparison => write!(f, "Budget Comparison"),
Self::IndustryComparison => write!(f, "Industry Comparison"),
Self::NonFinancialRelationship => write!(f, "Non-Financial Relationship"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AnalyticalPurpose {
#[default]
RiskAssessment,
Substantive,
FinalReview,
}
impl std::fmt::Display for AnalyticalPurpose {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RiskAssessment => write!(f, "Risk Assessment"),
Self::Substantive => write!(f, "Substantive"),
Self::FinalReview => write!(f, "Final Review"),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnalyticalExpectation {
#[serde(with = "datasynth_core::serde_decimal")]
pub expected_value: Decimal,
pub expectation_basis: ExpectationBasis,
pub methodology: String,
pub data_reliability: ReliabilityLevel,
pub precision_level: PrecisionLevel,
pub key_assumptions: Vec<String>,
pub data_sources: Vec<String>,
}
impl AnalyticalExpectation {
pub fn new(expected_value: Decimal, expectation_basis: ExpectationBasis) -> Self {
Self {
expected_value,
expectation_basis,
methodology: String::new(),
data_reliability: ReliabilityLevel::default(),
precision_level: PrecisionLevel::default(),
key_assumptions: Vec::new(),
data_sources: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ExpectationBasis {
#[default]
PriorPeriod,
Budget,
Industry,
NonFinancial,
StatisticalModel,
IndependentCalculation,
InterimResults,
}
impl std::fmt::Display for ExpectationBasis {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PriorPeriod => write!(f, "Prior Period"),
Self::Budget => write!(f, "Budget/Forecast"),
Self::Industry => write!(f, "Industry Data"),
Self::NonFinancial => write!(f, "Non-Financial Data"),
Self::StatisticalModel => write!(f, "Statistical Model"),
Self::IndependentCalculation => write!(f, "Independent Calculation"),
Self::InterimResults => write!(f, "Interim Results"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReliabilityLevel {
Low,
#[default]
Moderate,
High,
}
impl std::fmt::Display for ReliabilityLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Low => write!(f, "Low"),
Self::Moderate => write!(f, "Moderate"),
Self::High => write!(f, "High"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PrecisionLevel {
Low,
#[default]
Moderate,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VarianceInvestigation {
pub investigation_id: Uuid,
pub explanation: String,
pub explanation_source: String,
pub corroborated: bool,
pub corroborating_evidence: Vec<String>,
pub additional_procedures: Vec<String>,
pub variance_explained: bool,
#[serde(default, with = "datasynth_core::serde_decimal::option")]
pub misstatement_amount: Option<Decimal>,
pub conclusion: InvestigationConclusion,
}
impl VarianceInvestigation {
pub fn new() -> Self {
Self {
investigation_id: Uuid::now_v7(),
explanation: String::new(),
explanation_source: String::new(),
corroborated: false,
corroborating_evidence: Vec::new(),
additional_procedures: Vec::new(),
variance_explained: false,
misstatement_amount: None,
conclusion: InvestigationConclusion::NotCompleted,
}
}
}
impl Default for VarianceInvestigation {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum InvestigationConclusion {
#[default]
NotCompleted,
Explained,
PotentialMisstatement,
MisstatementIdentified,
UnableToExplain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AnalyticalConclusion {
#[default]
NotCompleted,
Consistent,
InvestigatedAndExplained,
PotentialMisstatement,
MisstatementIdentified,
Inconclusive,
}
impl std::fmt::Display for AnalyticalConclusion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotCompleted => write!(f, "Not Completed"),
Self::Consistent => write!(f, "Consistent with Expectations"),
Self::InvestigatedAndExplained => write!(f, "Investigated and Explained"),
Self::PotentialMisstatement => write!(f, "Potential Misstatement"),
Self::MisstatementIdentified => write!(f, "Misstatement Identified"),
Self::Inconclusive => write!(f, "Inconclusive"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FinancialRatio {
GrossMargin,
OperatingMargin,
NetProfitMargin,
ReturnOnAssets,
ReturnOnEquity,
CurrentRatio,
QuickRatio,
CashRatio,
InventoryTurnover,
ReceivablesTurnover,
PayablesTurnover,
AssetTurnover,
DaysSalesOutstanding,
DaysPayablesOutstanding,
DaysInventoryOnHand,
DebtToEquity,
DebtToAssets,
InterestCoverage,
Custom,
}
impl std::fmt::Display for FinancialRatio {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::GrossMargin => write!(f, "Gross Margin"),
Self::OperatingMargin => write!(f, "Operating Margin"),
Self::NetProfitMargin => write!(f, "Net Profit Margin"),
Self::ReturnOnAssets => write!(f, "Return on Assets"),
Self::ReturnOnEquity => write!(f, "Return on Equity"),
Self::CurrentRatio => write!(f, "Current Ratio"),
Self::QuickRatio => write!(f, "Quick Ratio"),
Self::CashRatio => write!(f, "Cash Ratio"),
Self::InventoryTurnover => write!(f, "Inventory Turnover"),
Self::ReceivablesTurnover => write!(f, "Receivables Turnover"),
Self::PayablesTurnover => write!(f, "Payables Turnover"),
Self::AssetTurnover => write!(f, "Asset Turnover"),
Self::DaysSalesOutstanding => write!(f, "Days Sales Outstanding"),
Self::DaysPayablesOutstanding => write!(f, "Days Payables Outstanding"),
Self::DaysInventoryOnHand => write!(f, "Days Inventory on Hand"),
Self::DebtToEquity => write!(f, "Debt to Equity"),
Self::DebtToAssets => write!(f, "Debt to Assets"),
Self::InterestCoverage => write!(f, "Interest Coverage"),
Self::Custom => write!(f, "Custom Ratio"),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_analytical_procedure_creation() {
let procedure = AnalyticalProcedure::new(
Uuid::now_v7(),
"Revenue",
AnalyticalProcedureType::Trend,
AnalyticalPurpose::Substantive,
);
assert_eq!(procedure.account_area, "Revenue");
assert_eq!(procedure.procedure_type, AnalyticalProcedureType::Trend);
assert_eq!(procedure.conclusion, AnalyticalConclusion::NotCompleted);
}
#[test]
fn test_variance_calculation() {
let mut procedure = AnalyticalProcedure::new(
Uuid::now_v7(),
"Revenue",
AnalyticalProcedureType::Trend,
AnalyticalPurpose::Substantive,
);
procedure.expectation =
AnalyticalExpectation::new(dec!(100000), ExpectationBasis::PriorPeriod);
procedure.actual_value = dec!(110000);
procedure.investigation_threshold = dec!(5000);
procedure.calculate_variance();
assert_eq!(procedure.variance, dec!(10000));
assert_eq!(procedure.variance_percent, dec!(10));
assert!(procedure.exceeds_threshold);
}
#[test]
fn test_variance_within_threshold() {
let mut procedure = AnalyticalProcedure::new(
Uuid::now_v7(),
"Cost of Sales",
AnalyticalProcedureType::Reasonableness,
AnalyticalPurpose::FinalReview,
);
procedure.expectation =
AnalyticalExpectation::new(dec!(50000), ExpectationBasis::IndependentCalculation);
procedure.actual_value = dec!(51000);
procedure.investigation_threshold = dec!(2500);
procedure.calculate_variance();
assert_eq!(procedure.variance, dec!(1000));
assert!(!procedure.exceeds_threshold);
}
}