1use chrono::NaiveDate;
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ExternalConfirmation {
17 pub confirmation_id: Uuid,
19
20 pub engagement_id: Uuid,
22
23 pub confirmation_type: ConfirmationType,
25
26 pub confirmation_form: ConfirmationForm,
28
29 pub confirmee_name: String,
31
32 pub confirmee_address: String,
34
35 pub confirmee_contact: String,
37
38 pub item_description: String,
40
41 #[serde(with = "rust_decimal::serde::str")]
43 pub client_amount: Decimal,
44
45 pub currency: String,
47
48 pub date_sent: NaiveDate,
50
51 pub follow_up_date: Option<NaiveDate>,
53
54 pub response_status: ConfirmationResponseStatus,
56
57 pub response: Option<ConfirmationResponse>,
59
60 pub reconciliation: Option<ConfirmationReconciliation>,
62
63 pub alternative_procedures: Option<AlternativeProcedures>,
65
66 pub conclusion: ConfirmationConclusion,
68
69 pub workpaper_reference: Option<String>,
71
72 pub prepared_by: String,
74
75 pub reviewed_by: Option<String>,
77}
78
79impl ExternalConfirmation {
80 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 pub fn is_complete(&self) -> bool {
115 !matches!(self.conclusion, ConfirmationConclusion::NotCompleted)
116 }
117
118 pub fn needs_alternative_procedures(&self) -> bool {
120 matches!(
121 self.response_status,
122 ConfirmationResponseStatus::NoResponse | ConfirmationResponseStatus::Returned
123 )
124 }
125
126 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum ConfirmationType {
138 Bank,
140 AccountsReceivable,
142 AccountsPayable,
144 Loan,
146 Legal,
148 Investment,
150 Insurance,
152 RelatedParty,
154 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
176#[serde(rename_all = "snake_case")]
177pub enum ConfirmationForm {
178 #[default]
180 Positive,
181 Negative,
183 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
199#[serde(rename_all = "snake_case")]
200pub enum ConfirmationResponseStatus {
201 #[default]
203 NotSent,
204 Pending,
206 ReceivedAgrees,
208 ReceivedDisagrees,
210 ReceivedPartial,
212 NoResponse,
214 Returned,
216 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#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct ConfirmationResponse {
238 pub date_received: NaiveDate,
240
241 #[serde(with = "rust_decimal::serde::str")]
243 pub confirmed_amount: Decimal,
244
245 pub agrees: bool,
247
248 pub comments: String,
250
251 pub differences_noted: Vec<ConfirmedDifference>,
253
254 pub respondent_name: String,
256
257 pub appears_authentic: bool,
259
260 pub reliability_assessment: ResponseReliability,
262}
263
264impl ConfirmationResponse {
265 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
282#[serde(rename_all = "snake_case")]
283pub enum ResponseReliability {
284 #[default]
286 Reliable,
287 QuestionableReliability,
289 Unreliable,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct ConfirmedDifference {
296 pub description: String,
298
299 #[serde(with = "rust_decimal::serde::str")]
301 pub amount: Decimal,
302
303 pub difference_type: DifferenceType,
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
309#[serde(rename_all = "snake_case")]
310pub enum DifferenceType {
311 Timing,
313 Error,
315 Dispute,
317 Cutoff,
319 Classification,
321 Unknown,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct ConfirmationReconciliation {
328 #[serde(with = "rust_decimal::serde::str")]
330 pub client_balance: Decimal,
331
332 #[serde(with = "rust_decimal::serde::str")]
334 pub confirmed_balance: Decimal,
335
336 #[serde(with = "rust_decimal::serde::str")]
338 pub total_difference: Decimal,
339
340 pub reconciling_items: Vec<ReconcilingItem>,
342
343 #[serde(with = "rust_decimal::serde::str")]
345 pub unreconciled_difference: Decimal,
346
347 pub conclusion: ReconciliationConclusion,
349}
350
351impl ConfirmationReconciliation {
352 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct ReconcilingItem {
375 pub description: String,
377
378 #[serde(with = "rust_decimal::serde::str")]
380 pub amount: Decimal,
381
382 pub item_type: ReconcilingItemType,
384
385 pub evidence: String,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
391#[serde(rename_all = "snake_case")]
392pub enum ReconcilingItemType {
393 CashInTransit,
395 DepositInTransit,
397 OutstandingCheck,
399 BankCharges,
401 InterestNotRecorded,
403 CutoffAdjustment,
405 ErrorCorrection,
407 Other,
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
413#[serde(rename_all = "snake_case")]
414pub enum ReconciliationConclusion {
415 #[default]
417 NotCompleted,
418 FullyReconciled,
420 ReconciledTimingOnly,
422 PotentialMisstatement,
424 MisstatementIdentified,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct AlternativeProcedures {
431 pub reason: AlternativeProcedureReason,
433
434 pub procedures: Vec<AlternativeProcedure>,
436
437 pub evidence_obtained: Vec<String>,
439
440 pub conclusion: AlternativeProcedureConclusion,
442}
443
444impl AlternativeProcedures {
445 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
458#[serde(rename_all = "snake_case")]
459pub enum AlternativeProcedureReason {
460 NoResponse,
462 UnreliableResponse,
464 Undeliverable,
466 ManagementRefused,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct AlternativeProcedure {
473 pub description: String,
475
476 pub procedure_type: AlternativeProcedureType,
478
479 pub result: String,
481}
482
483#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
485#[serde(rename_all = "snake_case")]
486pub enum AlternativeProcedureType {
487 SubsequentCashReceipts,
489 SubsequentCashDisbursements,
491 ShippingDocuments,
493 ReceivingReports,
495 PurchaseOrders,
497 SalesContracts,
499 BankStatements,
501 Other,
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
507#[serde(rename_all = "snake_case")]
508pub enum AlternativeProcedureConclusion {
509 #[default]
511 NotCompleted,
512 SufficientEvidence,
514 InsufficientEvidence,
516 MisstatementIdentified,
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
522#[serde(rename_all = "snake_case")]
523pub enum ConfirmationConclusion {
524 #[default]
526 NotCompleted,
527 Confirmed,
529 ExceptionResolved,
531 PotentialMisstatement,
533 MisstatementIdentified,
535 AlternativesSatisfactory,
537 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}