Skip to main content

datasynth_standards/audit/
confirmation.rs

1//! External Confirmations (ISA 505).
2//!
3//! Implements external confirmation procedures for obtaining audit evidence:
4//! - Bank confirmations
5//! - Accounts receivable confirmations
6//! - Accounts payable confirmations
7//! - Legal confirmations
8
9use chrono::NaiveDate;
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14/// External confirmation record.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ExternalConfirmation {
17    /// Unique confirmation identifier.
18    pub confirmation_id: Uuid,
19
20    /// Engagement ID.
21    pub engagement_id: Uuid,
22
23    /// Type of confirmation.
24    pub confirmation_type: ConfirmationType,
25
26    /// Form of confirmation.
27    pub confirmation_form: ConfirmationForm,
28
29    /// Name of confirming party.
30    pub confirmee_name: String,
31
32    /// Address of confirming party.
33    pub confirmee_address: String,
34
35    /// Contact information.
36    pub confirmee_contact: String,
37
38    /// Account or item being confirmed.
39    pub item_description: String,
40
41    /// Amount per client records.
42    #[serde(with = "rust_decimal::serde::str")]
43    pub client_amount: Decimal,
44
45    /// Currency.
46    pub currency: String,
47
48    /// Date sent.
49    pub date_sent: NaiveDate,
50
51    /// Follow-up date.
52    pub follow_up_date: Option<NaiveDate>,
53
54    /// Response status.
55    pub response_status: ConfirmationResponseStatus,
56
57    /// Response details (if received).
58    pub response: Option<ConfirmationResponse>,
59
60    /// Reconciliation of differences.
61    pub reconciliation: Option<ConfirmationReconciliation>,
62
63    /// Alternative procedures (if no response).
64    pub alternative_procedures: Option<AlternativeProcedures>,
65
66    /// Conclusion.
67    pub conclusion: ConfirmationConclusion,
68
69    /// Workpaper reference.
70    pub workpaper_reference: Option<String>,
71
72    /// Prepared by.
73    pub prepared_by: String,
74
75    /// Reviewed by.
76    pub reviewed_by: Option<String>,
77}
78
79impl ExternalConfirmation {
80    /// Create a new external confirmation.
81    pub fn new(
82        engagement_id: Uuid,
83        confirmation_type: ConfirmationType,
84        confirmee_name: impl Into<String>,
85        item_description: impl Into<String>,
86        client_amount: Decimal,
87        currency: impl Into<String>,
88    ) -> Self {
89        Self {
90            confirmation_id: Uuid::now_v7(),
91            engagement_id,
92            confirmation_type,
93            confirmation_form: ConfirmationForm::Positive,
94            confirmee_name: confirmee_name.into(),
95            confirmee_address: String::new(),
96            confirmee_contact: String::new(),
97            item_description: item_description.into(),
98            client_amount,
99            currency: currency.into(),
100            date_sent: chrono::Utc::now().date_naive(),
101            follow_up_date: None,
102            response_status: ConfirmationResponseStatus::Pending,
103            response: None,
104            reconciliation: None,
105            alternative_procedures: None,
106            conclusion: ConfirmationConclusion::NotCompleted,
107            workpaper_reference: None,
108            prepared_by: String::new(),
109            reviewed_by: None,
110        }
111    }
112
113    /// Check if confirmation is complete.
114    pub fn is_complete(&self) -> bool {
115        !matches!(self.conclusion, ConfirmationConclusion::NotCompleted)
116    }
117
118    /// Check if alternative procedures are needed.
119    pub fn needs_alternative_procedures(&self) -> bool {
120        matches!(
121            self.response_status,
122            ConfirmationResponseStatus::NoResponse | ConfirmationResponseStatus::Returned
123        )
124    }
125
126    /// Calculate difference between client and confirmed amounts.
127    pub fn difference(&self) -> Option<Decimal> {
128        self.response
129            .as_ref()
130            .map(|r| self.client_amount - r.confirmed_amount)
131    }
132}
133
134/// Type of confirmation.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum ConfirmationType {
138    /// Bank account confirmation.
139    Bank,
140    /// Trade receivable confirmation.
141    AccountsReceivable,
142    /// Trade payable confirmation.
143    AccountsPayable,
144    /// Loan/debt confirmation.
145    Loan,
146    /// Legal confirmation (lawyers' letters).
147    Legal,
148    /// Investment confirmation.
149    Investment,
150    /// Insurance confirmation.
151    Insurance,
152    /// Related party confirmation.
153    RelatedParty,
154    /// Other confirmation.
155    Other,
156}
157
158impl std::fmt::Display for ConfirmationType {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            Self::Bank => write!(f, "Bank Confirmation"),
162            Self::AccountsReceivable => write!(f, "AR Confirmation"),
163            Self::AccountsPayable => write!(f, "AP Confirmation"),
164            Self::Loan => write!(f, "Loan Confirmation"),
165            Self::Legal => write!(f, "Legal Confirmation"),
166            Self::Investment => write!(f, "Investment Confirmation"),
167            Self::Insurance => write!(f, "Insurance Confirmation"),
168            Self::RelatedParty => write!(f, "Related Party Confirmation"),
169            Self::Other => write!(f, "Other Confirmation"),
170        }
171    }
172}
173
174/// Form of confirmation request.
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
176#[serde(rename_all = "snake_case")]
177pub enum ConfirmationForm {
178    /// Positive confirmation - requests response in all cases.
179    #[default]
180    Positive,
181    /// Negative confirmation - requests response only if disagrees.
182    Negative,
183    /// Blank confirmation - confirmee fills in the amount.
184    Blank,
185}
186
187impl std::fmt::Display for ConfirmationForm {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        match self {
190            Self::Positive => write!(f, "Positive"),
191            Self::Negative => write!(f, "Negative"),
192            Self::Blank => write!(f, "Blank"),
193        }
194    }
195}
196
197/// Confirmation response status.
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
199#[serde(rename_all = "snake_case")]
200pub enum ConfirmationResponseStatus {
201    /// Confirmation not yet sent.
202    #[default]
203    NotSent,
204    /// Sent, awaiting response.
205    Pending,
206    /// Response received - agrees.
207    ReceivedAgrees,
208    /// Response received - disagrees.
209    ReceivedDisagrees,
210    /// Response received - partial information.
211    ReceivedPartial,
212    /// No response after follow-up.
213    NoResponse,
214    /// Returned undeliverable.
215    Returned,
216    /// Response received (for blank confirmations).
217    ReceivedBlank,
218}
219
220impl std::fmt::Display for ConfirmationResponseStatus {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        match self {
223            Self::NotSent => write!(f, "Not Sent"),
224            Self::Pending => write!(f, "Pending"),
225            Self::ReceivedAgrees => write!(f, "Received - Agrees"),
226            Self::ReceivedDisagrees => write!(f, "Received - Disagrees"),
227            Self::ReceivedPartial => write!(f, "Received - Partial"),
228            Self::NoResponse => write!(f, "No Response"),
229            Self::Returned => write!(f, "Returned"),
230            Self::ReceivedBlank => write!(f, "Received - Blank"),
231        }
232    }
233}
234
235/// Confirmation response details.
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct ConfirmationResponse {
238    /// Date response received.
239    pub date_received: NaiveDate,
240
241    /// Confirmed amount.
242    #[serde(with = "rust_decimal::serde::str")]
243    pub confirmed_amount: Decimal,
244
245    /// Response agrees with client records.
246    pub agrees: bool,
247
248    /// Comments from confirmee.
249    pub comments: String,
250
251    /// Differences noted by confirmee.
252    pub differences_noted: Vec<ConfirmedDifference>,
253
254    /// Respondent name/title.
255    pub respondent_name: String,
256
257    /// Whether response appears authentic.
258    pub appears_authentic: bool,
259
260    /// Reliability assessment.
261    pub reliability_assessment: ResponseReliability,
262}
263
264impl ConfirmationResponse {
265    /// Create a new confirmation response.
266    pub fn new(date_received: NaiveDate, confirmed_amount: Decimal, agrees: bool) -> Self {
267        Self {
268            date_received,
269            confirmed_amount,
270            agrees,
271            comments: String::new(),
272            differences_noted: Vec::new(),
273            respondent_name: String::new(),
274            appears_authentic: true,
275            reliability_assessment: ResponseReliability::Reliable,
276        }
277    }
278}
279
280/// Reliability assessment of confirmation response.
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
282#[serde(rename_all = "snake_case")]
283pub enum ResponseReliability {
284    /// Response appears reliable.
285    #[default]
286    Reliable,
287    /// Some concerns about reliability.
288    QuestionableReliability,
289    /// Response is unreliable.
290    Unreliable,
291}
292
293/// Difference noted in confirmation.
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct ConfirmedDifference {
296    /// Description of difference.
297    pub description: String,
298
299    /// Amount of difference.
300    #[serde(with = "rust_decimal::serde::str")]
301    pub amount: Decimal,
302
303    /// Type of difference.
304    pub difference_type: DifferenceType,
305}
306
307/// Type of confirmation difference.
308#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
309#[serde(rename_all = "snake_case")]
310pub enum DifferenceType {
311    /// Timing difference (e.g., payment in transit).
312    Timing,
313    /// Actual error in client records.
314    Error,
315    /// Disputed amount.
316    Dispute,
317    /// Cutoff difference.
318    Cutoff,
319    /// Classification difference.
320    Classification,
321    /// Unknown/unexplained.
322    Unknown,
323}
324
325/// Reconciliation of confirmation differences.
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct ConfirmationReconciliation {
328    /// Client balance.
329    #[serde(with = "rust_decimal::serde::str")]
330    pub client_balance: Decimal,
331
332    /// Confirmed balance.
333    #[serde(with = "rust_decimal::serde::str")]
334    pub confirmed_balance: Decimal,
335
336    /// Total difference.
337    #[serde(with = "rust_decimal::serde::str")]
338    pub total_difference: Decimal,
339
340    /// Reconciling items.
341    pub reconciling_items: Vec<ReconcilingItem>,
342
343    /// Unreconciled difference.
344    #[serde(with = "rust_decimal::serde::str")]
345    pub unreconciled_difference: Decimal,
346
347    /// Conclusion on reconciliation.
348    pub conclusion: ReconciliationConclusion,
349}
350
351impl ConfirmationReconciliation {
352    /// Create a new reconciliation.
353    pub fn new(client_balance: Decimal, confirmed_balance: Decimal) -> Self {
354        let total_difference = client_balance - confirmed_balance;
355        Self {
356            client_balance,
357            confirmed_balance,
358            total_difference,
359            reconciling_items: Vec::new(),
360            unreconciled_difference: total_difference,
361            conclusion: ReconciliationConclusion::NotCompleted,
362        }
363    }
364
365    /// Add a reconciling item and update unreconciled difference.
366    pub fn add_reconciling_item(&mut self, item: ReconcilingItem) {
367        self.unreconciled_difference -= item.amount;
368        self.reconciling_items.push(item);
369    }
370}
371
372/// Reconciling item.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct ReconcilingItem {
375    /// Description.
376    pub description: String,
377
378    /// Amount.
379    #[serde(with = "rust_decimal::serde::str")]
380    pub amount: Decimal,
381
382    /// Type of reconciling item.
383    pub item_type: ReconcilingItemType,
384
385    /// Supporting evidence obtained.
386    pub evidence: String,
387}
388
389/// Type of reconciling item.
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
391#[serde(rename_all = "snake_case")]
392pub enum ReconcilingItemType {
393    /// Cash/payment in transit.
394    CashInTransit,
395    /// Deposit in transit.
396    DepositInTransit,
397    /// Outstanding check.
398    OutstandingCheck,
399    /// Bank charges not recorded.
400    BankCharges,
401    /// Interest not recorded.
402    InterestNotRecorded,
403    /// Cutoff adjustment.
404    CutoffAdjustment,
405    /// Error correction.
406    ErrorCorrection,
407    /// Other reconciling item.
408    Other,
409}
410
411/// Reconciliation conclusion.
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
413#[serde(rename_all = "snake_case")]
414pub enum ReconciliationConclusion {
415    /// Reconciliation not completed.
416    #[default]
417    NotCompleted,
418    /// Fully reconciled, no issues.
419    FullyReconciled,
420    /// Reconciled with timing differences only.
421    ReconciledTimingOnly,
422    /// Potential misstatement identified.
423    PotentialMisstatement,
424    /// Misstatement identified.
425    MisstatementIdentified,
426}
427
428/// Alternative procedures when confirmation not received.
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct AlternativeProcedures {
431    /// Reason alternative procedures were needed.
432    pub reason: AlternativeProcedureReason,
433
434    /// Procedures performed.
435    pub procedures: Vec<AlternativeProcedure>,
436
437    /// Evidence obtained.
438    pub evidence_obtained: Vec<String>,
439
440    /// Conclusion.
441    pub conclusion: AlternativeProcedureConclusion,
442}
443
444impl AlternativeProcedures {
445    /// Create new alternative procedures.
446    pub fn new(reason: AlternativeProcedureReason) -> Self {
447        Self {
448            reason,
449            procedures: Vec::new(),
450            evidence_obtained: Vec::new(),
451            conclusion: AlternativeProcedureConclusion::NotCompleted,
452        }
453    }
454}
455
456/// Reason for alternative procedures.
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
458#[serde(rename_all = "snake_case")]
459pub enum AlternativeProcedureReason {
460    /// No response received.
461    NoResponse,
462    /// Response unreliable.
463    UnreliableResponse,
464    /// Confirmation returned undeliverable.
465    Undeliverable,
466    /// Management refused to allow.
467    ManagementRefused,
468}
469
470/// Alternative audit procedure.
471#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct AlternativeProcedure {
473    /// Procedure description.
474    pub description: String,
475
476    /// Type of procedure.
477    pub procedure_type: AlternativeProcedureType,
478
479    /// Result of procedure.
480    pub result: String,
481}
482
483/// Type of alternative procedure.
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
485#[serde(rename_all = "snake_case")]
486pub enum AlternativeProcedureType {
487    /// Examine subsequent cash receipts.
488    SubsequentCashReceipts,
489    /// Examine subsequent cash disbursements.
490    SubsequentCashDisbursements,
491    /// Examine shipping documents.
492    ShippingDocuments,
493    /// Examine receiving reports.
494    ReceivingReports,
495    /// Examine customer purchase orders.
496    PurchaseOrders,
497    /// Examine sales contracts.
498    SalesContracts,
499    /// Examine bank statements.
500    BankStatements,
501    /// Other procedure.
502    Other,
503}
504
505/// Conclusion from alternative procedures.
506#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
507#[serde(rename_all = "snake_case")]
508pub enum AlternativeProcedureConclusion {
509    /// Procedures not completed.
510    #[default]
511    NotCompleted,
512    /// Sufficient evidence obtained.
513    SufficientEvidence,
514    /// Insufficient evidence obtained.
515    InsufficientEvidence,
516    /// Misstatement identified.
517    MisstatementIdentified,
518}
519
520/// Confirmation conclusion.
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
522#[serde(rename_all = "snake_case")]
523pub enum ConfirmationConclusion {
524    /// Confirmation not completed.
525    #[default]
526    NotCompleted,
527    /// Satisfactory response received, balance confirmed.
528    Confirmed,
529    /// Exception noted and resolved.
530    ExceptionResolved,
531    /// Exception noted, potential misstatement.
532    PotentialMisstatement,
533    /// Misstatement identified.
534    MisstatementIdentified,
535    /// Alternative procedures satisfactory.
536    AlternativesSatisfactory,
537    /// Unable to obtain sufficient evidence.
538    InsufficientEvidence,
539}
540
541impl std::fmt::Display for ConfirmationConclusion {
542    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
543        match self {
544            Self::NotCompleted => write!(f, "Not Completed"),
545            Self::Confirmed => write!(f, "Confirmed"),
546            Self::ExceptionResolved => write!(f, "Exception Resolved"),
547            Self::PotentialMisstatement => write!(f, "Potential Misstatement"),
548            Self::MisstatementIdentified => write!(f, "Misstatement Identified"),
549            Self::AlternativesSatisfactory => write!(f, "Alternative Procedures Satisfactory"),
550            Self::InsufficientEvidence => write!(f, "Insufficient Evidence"),
551        }
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558    use rust_decimal_macros::dec;
559
560    #[test]
561    fn test_confirmation_creation() {
562        let confirmation = ExternalConfirmation::new(
563            Uuid::now_v7(),
564            ConfirmationType::AccountsReceivable,
565            "Customer Corp",
566            "Trade receivable balance",
567            dec!(50000),
568            "USD",
569        );
570
571        assert_eq!(confirmation.confirmee_name, "Customer Corp");
572        assert_eq!(confirmation.client_amount, dec!(50000));
573        assert_eq!(
574            confirmation.response_status,
575            ConfirmationResponseStatus::Pending
576        );
577    }
578
579    #[test]
580    fn test_confirmation_difference() {
581        let mut confirmation = ExternalConfirmation::new(
582            Uuid::now_v7(),
583            ConfirmationType::AccountsReceivable,
584            "Customer Corp",
585            "Trade receivable balance",
586            dec!(50000),
587            "USD",
588        );
589
590        confirmation.response = Some(ConfirmationResponse::new(
591            NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
592            dec!(48000),
593            false,
594        ));
595
596        assert_eq!(confirmation.difference(), Some(dec!(2000)));
597    }
598
599    #[test]
600    fn test_reconciliation() {
601        let mut recon = ConfirmationReconciliation::new(dec!(50000), dec!(48000));
602
603        assert_eq!(recon.total_difference, dec!(2000));
604        assert_eq!(recon.unreconciled_difference, dec!(2000));
605
606        recon.add_reconciling_item(ReconcilingItem {
607            description: "Payment in transit".to_string(),
608            amount: dec!(2000),
609            item_type: ReconcilingItemType::CashInTransit,
610            evidence: "Examined subsequent receipt".to_string(),
611        });
612
613        assert_eq!(recon.unreconciled_difference, dec!(0));
614    }
615
616    #[test]
617    fn test_alternative_procedures_needed() {
618        let mut confirmation = ExternalConfirmation::new(
619            Uuid::now_v7(),
620            ConfirmationType::AccountsReceivable,
621            "Customer Corp",
622            "Trade receivable balance",
623            dec!(50000),
624            "USD",
625        );
626
627        confirmation.response_status = ConfirmationResponseStatus::NoResponse;
628        assert!(confirmation.needs_alternative_procedures());
629
630        confirmation.response_status = ConfirmationResponseStatus::ReceivedAgrees;
631        assert!(!confirmation.needs_alternative_procedures());
632    }
633}