use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalConfirmation {
pub confirmation_id: Uuid,
pub engagement_id: Uuid,
pub confirmation_type: ConfirmationType,
pub confirmation_form: ConfirmationForm,
pub confirmee_name: String,
pub confirmee_address: String,
pub confirmee_contact: String,
pub item_description: String,
#[serde(with = "rust_decimal::serde::str")]
pub client_amount: Decimal,
pub currency: String,
pub date_sent: NaiveDate,
pub follow_up_date: Option<NaiveDate>,
pub response_status: ConfirmationResponseStatus,
pub response: Option<ConfirmationResponse>,
pub reconciliation: Option<ConfirmationReconciliation>,
pub alternative_procedures: Option<AlternativeProcedures>,
pub conclusion: ConfirmationConclusion,
pub workpaper_reference: Option<String>,
pub prepared_by: String,
pub reviewed_by: Option<String>,
}
impl ExternalConfirmation {
pub fn new(
engagement_id: Uuid,
confirmation_type: ConfirmationType,
confirmee_name: impl Into<String>,
item_description: impl Into<String>,
client_amount: Decimal,
currency: impl Into<String>,
) -> Self {
Self {
confirmation_id: Uuid::now_v7(),
engagement_id,
confirmation_type,
confirmation_form: ConfirmationForm::Positive,
confirmee_name: confirmee_name.into(),
confirmee_address: String::new(),
confirmee_contact: String::new(),
item_description: item_description.into(),
client_amount,
currency: currency.into(),
date_sent: chrono::Utc::now().date_naive(),
follow_up_date: None,
response_status: ConfirmationResponseStatus::Pending,
response: None,
reconciliation: None,
alternative_procedures: None,
conclusion: ConfirmationConclusion::NotCompleted,
workpaper_reference: None,
prepared_by: String::new(),
reviewed_by: None,
}
}
pub fn is_complete(&self) -> bool {
!matches!(self.conclusion, ConfirmationConclusion::NotCompleted)
}
pub fn needs_alternative_procedures(&self) -> bool {
matches!(
self.response_status,
ConfirmationResponseStatus::NoResponse | ConfirmationResponseStatus::Returned
)
}
pub fn difference(&self) -> Option<Decimal> {
self.response
.as_ref()
.map(|r| self.client_amount - r.confirmed_amount)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConfirmationType {
Bank,
AccountsReceivable,
AccountsPayable,
Loan,
Legal,
Investment,
Insurance,
RelatedParty,
Other,
}
impl std::fmt::Display for ConfirmationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bank => write!(f, "Bank Confirmation"),
Self::AccountsReceivable => write!(f, "AR Confirmation"),
Self::AccountsPayable => write!(f, "AP Confirmation"),
Self::Loan => write!(f, "Loan Confirmation"),
Self::Legal => write!(f, "Legal Confirmation"),
Self::Investment => write!(f, "Investment Confirmation"),
Self::Insurance => write!(f, "Insurance Confirmation"),
Self::RelatedParty => write!(f, "Related Party Confirmation"),
Self::Other => write!(f, "Other Confirmation"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ConfirmationForm {
#[default]
Positive,
Negative,
Blank,
}
impl std::fmt::Display for ConfirmationForm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Positive => write!(f, "Positive"),
Self::Negative => write!(f, "Negative"),
Self::Blank => write!(f, "Blank"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ConfirmationResponseStatus {
#[default]
NotSent,
Pending,
ReceivedAgrees,
ReceivedDisagrees,
ReceivedPartial,
NoResponse,
Returned,
ReceivedBlank,
}
impl std::fmt::Display for ConfirmationResponseStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotSent => write!(f, "Not Sent"),
Self::Pending => write!(f, "Pending"),
Self::ReceivedAgrees => write!(f, "Received - Agrees"),
Self::ReceivedDisagrees => write!(f, "Received - Disagrees"),
Self::ReceivedPartial => write!(f, "Received - Partial"),
Self::NoResponse => write!(f, "No Response"),
Self::Returned => write!(f, "Returned"),
Self::ReceivedBlank => write!(f, "Received - Blank"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfirmationResponse {
pub date_received: NaiveDate,
#[serde(with = "rust_decimal::serde::str")]
pub confirmed_amount: Decimal,
pub agrees: bool,
pub comments: String,
pub differences_noted: Vec<ConfirmedDifference>,
pub respondent_name: String,
pub appears_authentic: bool,
pub reliability_assessment: ResponseReliability,
}
impl ConfirmationResponse {
pub fn new(date_received: NaiveDate, confirmed_amount: Decimal, agrees: bool) -> Self {
Self {
date_received,
confirmed_amount,
agrees,
comments: String::new(),
differences_noted: Vec::new(),
respondent_name: String::new(),
appears_authentic: true,
reliability_assessment: ResponseReliability::Reliable,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ResponseReliability {
#[default]
Reliable,
QuestionableReliability,
Unreliable,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfirmedDifference {
pub description: String,
#[serde(with = "rust_decimal::serde::str")]
pub amount: Decimal,
pub difference_type: DifferenceType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DifferenceType {
Timing,
Error,
Dispute,
Cutoff,
Classification,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfirmationReconciliation {
#[serde(with = "rust_decimal::serde::str")]
pub client_balance: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub confirmed_balance: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub total_difference: Decimal,
pub reconciling_items: Vec<ReconcilingItem>,
#[serde(with = "rust_decimal::serde::str")]
pub unreconciled_difference: Decimal,
pub conclusion: ReconciliationConclusion,
}
impl ConfirmationReconciliation {
pub fn new(client_balance: Decimal, confirmed_balance: Decimal) -> Self {
let total_difference = client_balance - confirmed_balance;
Self {
client_balance,
confirmed_balance,
total_difference,
reconciling_items: Vec::new(),
unreconciled_difference: total_difference,
conclusion: ReconciliationConclusion::NotCompleted,
}
}
pub fn add_reconciling_item(&mut self, item: ReconcilingItem) {
self.unreconciled_difference -= item.amount;
self.reconciling_items.push(item);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReconcilingItem {
pub description: String,
#[serde(with = "rust_decimal::serde::str")]
pub amount: Decimal,
pub item_type: ReconcilingItemType,
pub evidence: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReconcilingItemType {
CashInTransit,
DepositInTransit,
OutstandingCheck,
BankCharges,
InterestNotRecorded,
CutoffAdjustment,
ErrorCorrection,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReconciliationConclusion {
#[default]
NotCompleted,
FullyReconciled,
ReconciledTimingOnly,
PotentialMisstatement,
MisstatementIdentified,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlternativeProcedures {
pub reason: AlternativeProcedureReason,
pub procedures: Vec<AlternativeProcedure>,
pub evidence_obtained: Vec<String>,
pub conclusion: AlternativeProcedureConclusion,
}
impl AlternativeProcedures {
pub fn new(reason: AlternativeProcedureReason) -> Self {
Self {
reason,
procedures: Vec::new(),
evidence_obtained: Vec::new(),
conclusion: AlternativeProcedureConclusion::NotCompleted,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AlternativeProcedureReason {
NoResponse,
UnreliableResponse,
Undeliverable,
ManagementRefused,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlternativeProcedure {
pub description: String,
pub procedure_type: AlternativeProcedureType,
pub result: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AlternativeProcedureType {
SubsequentCashReceipts,
SubsequentCashDisbursements,
ShippingDocuments,
ReceivingReports,
PurchaseOrders,
SalesContracts,
BankStatements,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AlternativeProcedureConclusion {
#[default]
NotCompleted,
SufficientEvidence,
InsufficientEvidence,
MisstatementIdentified,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ConfirmationConclusion {
#[default]
NotCompleted,
Confirmed,
ExceptionResolved,
PotentialMisstatement,
MisstatementIdentified,
AlternativesSatisfactory,
InsufficientEvidence,
}
impl std::fmt::Display for ConfirmationConclusion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotCompleted => write!(f, "Not Completed"),
Self::Confirmed => write!(f, "Confirmed"),
Self::ExceptionResolved => write!(f, "Exception Resolved"),
Self::PotentialMisstatement => write!(f, "Potential Misstatement"),
Self::MisstatementIdentified => write!(f, "Misstatement Identified"),
Self::AlternativesSatisfactory => write!(f, "Alternative Procedures Satisfactory"),
Self::InsufficientEvidence => write!(f, "Insufficient Evidence"),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_confirmation_creation() {
let confirmation = ExternalConfirmation::new(
Uuid::now_v7(),
ConfirmationType::AccountsReceivable,
"Customer Corp",
"Trade receivable balance",
dec!(50000),
"USD",
);
assert_eq!(confirmation.confirmee_name, "Customer Corp");
assert_eq!(confirmation.client_amount, dec!(50000));
assert_eq!(
confirmation.response_status,
ConfirmationResponseStatus::Pending
);
}
#[test]
fn test_confirmation_difference() {
let mut confirmation = ExternalConfirmation::new(
Uuid::now_v7(),
ConfirmationType::AccountsReceivable,
"Customer Corp",
"Trade receivable balance",
dec!(50000),
"USD",
);
confirmation.response = Some(ConfirmationResponse::new(
NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
dec!(48000),
false,
));
assert_eq!(confirmation.difference(), Some(dec!(2000)));
}
#[test]
fn test_reconciliation() {
let mut recon = ConfirmationReconciliation::new(dec!(50000), dec!(48000));
assert_eq!(recon.total_difference, dec!(2000));
assert_eq!(recon.unreconciled_difference, dec!(2000));
recon.add_reconciling_item(ReconcilingItem {
description: "Payment in transit".to_string(),
amount: dec!(2000),
item_type: ReconcilingItemType::CashInTransit,
evidence: "Examined subsequent receipt".to_string(),
});
assert_eq!(recon.unreconciled_difference, dec!(0));
}
#[test]
fn test_alternative_procedures_needed() {
let mut confirmation = ExternalConfirmation::new(
Uuid::now_v7(),
ConfirmationType::AccountsReceivable,
"Customer Corp",
"Trade receivable balance",
dec!(50000),
"USD",
);
confirmation.response_status = ConfirmationResponseStatus::NoResponse;
assert!(confirmation.needs_alternative_procedures());
confirmation.response_status = ConfirmationResponseStatus::ReceivedAgrees;
assert!(!confirmation.needs_alternative_procedures());
}
}