1use chrono::NaiveDate;
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AnalyticalProcedure {
19 pub procedure_id: Uuid,
21
22 pub engagement_id: Uuid,
24
25 pub account_area: String,
27
28 pub procedure_type: AnalyticalProcedureType,
30
31 pub purpose: AnalyticalPurpose,
33
34 pub expectation: AnalyticalExpectation,
36
37 #[serde(with = "rust_decimal::serde::str")]
39 pub actual_value: Decimal,
40
41 #[serde(with = "rust_decimal::serde::str")]
43 pub variance: Decimal,
44
45 #[serde(with = "rust_decimal::serde::str")]
47 pub variance_percent: Decimal,
48
49 #[serde(with = "rust_decimal::serde::str")]
51 pub investigation_threshold: Decimal,
52
53 pub exceeds_threshold: bool,
55
56 pub investigation: Option<VarianceInvestigation>,
58
59 pub conclusion: AnalyticalConclusion,
61
62 pub procedure_date: NaiveDate,
64
65 pub prepared_by: String,
67
68 pub reviewed_by: Option<String>,
70
71 pub workpaper_reference: Option<String>,
73}
74
75impl AnalyticalProcedure {
76 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 pub fn calculate_variance(&mut self) {
106 self.variance = self.actual_value - self.expectation.expected_value;
107
108 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 self.exceeds_threshold = self.variance.abs() > self.investigation_threshold;
118 }
119
120 pub fn requires_investigation(&self) -> bool {
122 self.exceeds_threshold
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
128#[serde(rename_all = "snake_case")]
129pub enum AnalyticalProcedureType {
130 #[default]
132 Trend,
133 Ratio,
135 Reasonableness,
137 Regression,
139 BudgetComparison,
141 IndustryComparison,
143 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
163#[serde(rename_all = "snake_case")]
164pub enum AnalyticalPurpose {
165 #[default]
167 RiskAssessment,
168 Substantive,
170 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
186pub struct AnalyticalExpectation {
187 #[serde(with = "rust_decimal::serde::str")]
189 pub expected_value: Decimal,
190
191 pub expectation_basis: ExpectationBasis,
193
194 pub methodology: String,
196
197 pub data_reliability: ReliabilityLevel,
199
200 pub precision_level: PrecisionLevel,
202
203 pub key_assumptions: Vec<String>,
205
206 pub data_sources: Vec<String>,
208}
209
210impl AnalyticalExpectation {
211 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
227#[serde(rename_all = "snake_case")]
228pub enum ExpectationBasis {
229 #[default]
231 PriorPeriod,
232 Budget,
234 Industry,
236 NonFinancial,
238 StatisticalModel,
240 IndependentCalculation,
242 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
262#[serde(rename_all = "snake_case")]
263pub enum ReliabilityLevel {
264 Low,
266 #[default]
268 Moderate,
269 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
285#[serde(rename_all = "snake_case")]
286pub enum PrecisionLevel {
287 Low,
289 #[default]
291 Moderate,
292 High,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct VarianceInvestigation {
299 pub investigation_id: Uuid,
301
302 pub explanation: String,
304
305 pub explanation_source: String,
307
308 pub corroborated: bool,
310
311 pub corroborating_evidence: Vec<String>,
313
314 pub additional_procedures: Vec<String>,
316
317 pub variance_explained: bool,
319
320 #[serde(default, with = "rust_decimal::serde::str_option")]
322 pub misstatement_amount: Option<Decimal>,
323
324 pub conclusion: InvestigationConclusion,
326}
327
328impl VarianceInvestigation {
329 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
353#[serde(rename_all = "snake_case")]
354pub enum InvestigationConclusion {
355 #[default]
357 NotCompleted,
358 Explained,
360 PotentialMisstatement,
362 MisstatementIdentified,
364 UnableToExplain,
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
370#[serde(rename_all = "snake_case")]
371pub enum AnalyticalConclusion {
372 #[default]
374 NotCompleted,
375 Consistent,
377 InvestigatedAndExplained,
379 PotentialMisstatement,
381 MisstatementIdentified,
383 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
402#[serde(rename_all = "snake_case")]
403pub enum FinancialRatio {
404 GrossMargin,
406 OperatingMargin,
407 NetProfitMargin,
408 ReturnOnAssets,
409 ReturnOnEquity,
410
411 CurrentRatio,
413 QuickRatio,
414 CashRatio,
415
416 InventoryTurnover,
418 ReceivablesTurnover,
419 PayablesTurnover,
420 AssetTurnover,
421 DaysSalesOutstanding,
422 DaysPayablesOutstanding,
423 DaysInventoryOnHand,
424
425 DebtToEquity,
427 DebtToAssets,
428 InterestCoverage,
429
430 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)]
461#[allow(clippy::unwrap_used)]
462mod tests {
463 use super::*;
464 use rust_decimal_macros::dec;
465
466 #[test]
467 fn test_analytical_procedure_creation() {
468 let procedure = AnalyticalProcedure::new(
469 Uuid::now_v7(),
470 "Revenue",
471 AnalyticalProcedureType::Trend,
472 AnalyticalPurpose::Substantive,
473 );
474
475 assert_eq!(procedure.account_area, "Revenue");
476 assert_eq!(procedure.procedure_type, AnalyticalProcedureType::Trend);
477 assert_eq!(procedure.conclusion, AnalyticalConclusion::NotCompleted);
478 }
479
480 #[test]
481 fn test_variance_calculation() {
482 let mut procedure = AnalyticalProcedure::new(
483 Uuid::now_v7(),
484 "Revenue",
485 AnalyticalProcedureType::Trend,
486 AnalyticalPurpose::Substantive,
487 );
488
489 procedure.expectation =
490 AnalyticalExpectation::new(dec!(100000), ExpectationBasis::PriorPeriod);
491 procedure.actual_value = dec!(110000);
492 procedure.investigation_threshold = dec!(5000);
493
494 procedure.calculate_variance();
495
496 assert_eq!(procedure.variance, dec!(10000));
497 assert_eq!(procedure.variance_percent, dec!(10));
498 assert!(procedure.exceeds_threshold);
499 }
500
501 #[test]
502 fn test_variance_within_threshold() {
503 let mut procedure = AnalyticalProcedure::new(
504 Uuid::now_v7(),
505 "Cost of Sales",
506 AnalyticalProcedureType::Reasonableness,
507 AnalyticalPurpose::FinalReview,
508 );
509
510 procedure.expectation =
511 AnalyticalExpectation::new(dec!(50000), ExpectationBasis::IndependentCalculation);
512 procedure.actual_value = dec!(51000);
513 procedure.investigation_threshold = dec!(2500);
514
515 procedure.calculate_variance();
516
517 assert_eq!(procedure.variance, dec!(1000));
518 assert!(!procedure.exceeds_threshold);
519 }
520}