Skip to main content

datasynth_core/models/subledger/ar/
receipt.rs

1//! AR Receipt (customer payment) model.
2
3use chrono::{DateTime, NaiveDate, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6
7use crate::models::subledger::{CurrencyAmount, GLReference, SubledgerDocumentStatus};
8
9/// AR Receipt (payment from customer).
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ARReceipt {
12    /// Unique receipt number.
13    pub receipt_number: String,
14    /// Company code.
15    pub company_code: String,
16    /// Customer ID.
17    pub customer_id: String,
18    /// Customer name.
19    pub customer_name: String,
20    /// Receipt date.
21    pub receipt_date: NaiveDate,
22    /// Posting date.
23    pub posting_date: NaiveDate,
24    /// Value date (bank value date).
25    pub value_date: NaiveDate,
26    /// Receipt type.
27    pub receipt_type: ARReceiptType,
28    /// Receipt status.
29    pub status: SubledgerDocumentStatus,
30    /// Receipt amount.
31    pub amount: CurrencyAmount,
32    /// Bank charges deducted.
33    pub bank_charges: Decimal,
34    /// Discount taken.
35    pub discount_taken: Decimal,
36    /// Write-off amount.
37    pub write_off_amount: Decimal,
38    /// Net amount applied to invoices.
39    pub net_applied: Decimal,
40    /// Unapplied amount.
41    pub unapplied_amount: Decimal,
42    /// Payment method.
43    pub payment_method: PaymentMethod,
44    /// Bank account.
45    pub bank_account: String,
46    /// Bank reference.
47    pub bank_reference: Option<String>,
48    /// Check number (if check payment).
49    pub check_number: Option<String>,
50    /// Applied invoices.
51    pub applied_invoices: Vec<ReceiptApplication>,
52    /// GL references.
53    pub gl_references: Vec<GLReference>,
54    /// Created timestamp.
55    pub created_at: DateTime<Utc>,
56    /// Created by user.
57    pub created_by: Option<String>,
58    /// Notes.
59    pub notes: Option<String>,
60}
61
62impl ARReceipt {
63    /// Creates a new AR receipt.
64    #[allow(clippy::too_many_arguments)]
65    pub fn new(
66        receipt_number: String,
67        company_code: String,
68        customer_id: String,
69        customer_name: String,
70        receipt_date: NaiveDate,
71        amount: Decimal,
72        currency: String,
73        payment_method: PaymentMethod,
74        bank_account: String,
75    ) -> Self {
76        Self {
77            receipt_number,
78            company_code,
79            customer_id,
80            customer_name,
81            receipt_date,
82            posting_date: receipt_date,
83            value_date: receipt_date,
84            receipt_type: ARReceiptType::Standard,
85            status: SubledgerDocumentStatus::Open,
86            amount: CurrencyAmount::single_currency(amount, currency),
87            bank_charges: Decimal::ZERO,
88            discount_taken: Decimal::ZERO,
89            write_off_amount: Decimal::ZERO,
90            net_applied: Decimal::ZERO,
91            unapplied_amount: amount,
92            payment_method,
93            bank_account,
94            bank_reference: None,
95            check_number: None,
96            applied_invoices: Vec::new(),
97            gl_references: Vec::new(),
98            created_at: Utc::now(),
99            created_by: None,
100            notes: None,
101        }
102    }
103
104    /// Applies receipt to an invoice.
105    pub fn apply_to_invoice(
106        &mut self,
107        invoice_number: String,
108        amount_applied: Decimal,
109        discount: Decimal,
110    ) {
111        let application = ReceiptApplication {
112            invoice_number,
113            amount_applied,
114            discount_taken: discount,
115            write_off: Decimal::ZERO,
116            application_date: self.receipt_date,
117        };
118
119        self.applied_invoices.push(application);
120        self.net_applied += amount_applied;
121        self.discount_taken += discount;
122        self.unapplied_amount = self.amount.document_amount - self.net_applied;
123
124        if self.unapplied_amount <= Decimal::ZERO {
125            self.status = SubledgerDocumentStatus::Cleared;
126        }
127    }
128
129    /// Sets bank charges.
130    pub fn with_bank_charges(mut self, charges: Decimal) -> Self {
131        self.bank_charges = charges;
132        self.unapplied_amount -= charges;
133        self
134    }
135
136    /// Sets check number.
137    pub fn with_check(mut self, check_number: String) -> Self {
138        self.check_number = Some(check_number);
139        self.payment_method = PaymentMethod::Check;
140        self
141    }
142
143    /// Sets bank reference.
144    pub fn with_bank_reference(mut self, reference: String) -> Self {
145        self.bank_reference = Some(reference);
146        self
147    }
148
149    /// Adds a GL reference.
150    pub fn add_gl_reference(&mut self, reference: GLReference) {
151        self.gl_references.push(reference);
152    }
153
154    /// Gets total amount including discount.
155    pub fn total_settlement(&self) -> Decimal {
156        self.net_applied + self.discount_taken + self.write_off_amount
157    }
158
159    /// Reverses the receipt.
160    pub fn reverse(&mut self, reason: String) {
161        self.status = SubledgerDocumentStatus::Reversed;
162        self.notes = Some(format!(
163            "{}Reversed: {}",
164            self.notes
165                .as_ref()
166                .map(|n| format!("{}. ", n))
167                .unwrap_or_default(),
168            reason
169        ));
170    }
171
172    /// Creates on-account receipt (not applied to specific invoices).
173    #[allow(clippy::too_many_arguments)]
174    pub fn on_account(
175        receipt_number: String,
176        company_code: String,
177        customer_id: String,
178        customer_name: String,
179        receipt_date: NaiveDate,
180        amount: Decimal,
181        currency: String,
182        payment_method: PaymentMethod,
183        bank_account: String,
184    ) -> Self {
185        let mut receipt = Self::new(
186            receipt_number,
187            company_code,
188            customer_id,
189            customer_name,
190            receipt_date,
191            amount,
192            currency,
193            payment_method,
194            bank_account,
195        );
196        receipt.receipt_type = ARReceiptType::OnAccount;
197        receipt
198    }
199}
200
201/// Type of AR receipt.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
203pub enum ARReceiptType {
204    /// Standard receipt applied to invoices.
205    #[default]
206    Standard,
207    /// On-account receipt (unapplied).
208    OnAccount,
209    /// Down payment receipt.
210    DownPayment,
211    /// Refund (negative receipt).
212    Refund,
213    /// Write-off receipt.
214    WriteOff,
215    /// Netting (AR against AP).
216    Netting,
217}
218
219/// Payment method for receipts.
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
221pub enum PaymentMethod {
222    /// Wire transfer.
223    #[default]
224    WireTransfer,
225    /// Check.
226    Check,
227    /// ACH/Direct debit.
228    ACH,
229    /// Credit card.
230    CreditCard,
231    /// Cash.
232    Cash,
233    /// Letter of credit.
234    LetterOfCredit,
235    /// Netting.
236    Netting,
237    /// Other.
238    Other,
239}
240
241/// Application of receipt to an invoice.
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct ReceiptApplication {
244    /// Invoice number.
245    pub invoice_number: String,
246    /// Amount applied.
247    pub amount_applied: Decimal,
248    /// Discount taken.
249    pub discount_taken: Decimal,
250    /// Write-off amount.
251    pub write_off: Decimal,
252    /// Application date.
253    pub application_date: NaiveDate,
254}
255
256impl ReceiptApplication {
257    /// Total settlement for this application.
258    pub fn total_settlement(&self) -> Decimal {
259        self.amount_applied + self.discount_taken + self.write_off
260    }
261}
262
263/// Batch of receipts for processing.
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct ARReceiptBatch {
266    /// Batch ID.
267    pub batch_id: String,
268    /// Company code.
269    pub company_code: String,
270    /// Batch date.
271    pub batch_date: NaiveDate,
272    /// Receipts in batch.
273    pub receipts: Vec<ARReceipt>,
274    /// Total batch amount.
275    pub total_amount: Decimal,
276    /// Batch status.
277    pub status: BatchStatus,
278    /// Created by.
279    pub created_by: String,
280    /// Created at.
281    pub created_at: DateTime<Utc>,
282}
283
284impl ARReceiptBatch {
285    /// Creates a new receipt batch.
286    pub fn new(
287        batch_id: String,
288        company_code: String,
289        batch_date: NaiveDate,
290        created_by: String,
291    ) -> Self {
292        Self {
293            batch_id,
294            company_code,
295            batch_date,
296            receipts: Vec::new(),
297            total_amount: Decimal::ZERO,
298            status: BatchStatus::Open,
299            created_by,
300            created_at: Utc::now(),
301        }
302    }
303
304    /// Adds a receipt to the batch.
305    pub fn add_receipt(&mut self, receipt: ARReceipt) {
306        self.total_amount += receipt.amount.document_amount;
307        self.receipts.push(receipt);
308    }
309
310    /// Posts the batch.
311    pub fn post(&mut self) {
312        self.status = BatchStatus::Posted;
313    }
314
315    /// Gets receipt count.
316    pub fn count(&self) -> usize {
317        self.receipts.len()
318    }
319}
320
321/// Status of a batch.
322#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
323pub enum BatchStatus {
324    /// Batch is open for additions.
325    Open,
326    /// Batch is submitted for approval.
327    Submitted,
328    /// Batch is approved.
329    Approved,
330    /// Batch is posted.
331    Posted,
332    /// Batch is cancelled.
333    Cancelled,
334}
335
336/// Bank statement line for automatic matching.
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct BankStatementLine {
339    /// Statement line ID.
340    pub line_id: String,
341    /// Bank account.
342    pub bank_account: String,
343    /// Statement date.
344    pub statement_date: NaiveDate,
345    /// Value date.
346    pub value_date: NaiveDate,
347    /// Amount (positive = receipt, negative = payment).
348    pub amount: Decimal,
349    /// Currency.
350    pub currency: String,
351    /// Bank reference.
352    pub bank_reference: String,
353    /// Payer/payee name.
354    pub counterparty_name: Option<String>,
355    /// Payer/payee account.
356    pub counterparty_account: Option<String>,
357    /// Payment reference/remittance info.
358    pub payment_reference: Option<String>,
359    /// Is matched.
360    pub is_matched: bool,
361    /// Matched receipt number.
362    pub matched_receipt: Option<String>,
363}
364
365impl BankStatementLine {
366    /// Checks if this is an incoming payment.
367    pub fn is_receipt(&self) -> bool {
368        self.amount > Decimal::ZERO
369    }
370
371    /// Marks as matched.
372    pub fn match_to_receipt(&mut self, receipt_number: String) {
373        self.is_matched = true;
374        self.matched_receipt = Some(receipt_number);
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use rust_decimal_macros::dec;
382
383    #[test]
384    fn test_receipt_creation() {
385        let receipt = ARReceipt::new(
386            "REC001".to_string(),
387            "1000".to_string(),
388            "CUST001".to_string(),
389            "Test Customer".to_string(),
390            NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
391            dec!(1000),
392            "USD".to_string(),
393            PaymentMethod::WireTransfer,
394            "1000".to_string(),
395        );
396
397        assert_eq!(receipt.amount.document_amount, dec!(1000));
398        assert_eq!(receipt.unapplied_amount, dec!(1000));
399        assert_eq!(receipt.status, SubledgerDocumentStatus::Open);
400    }
401
402    #[test]
403    fn test_apply_to_invoice() {
404        let mut receipt = ARReceipt::new(
405            "REC001".to_string(),
406            "1000".to_string(),
407            "CUST001".to_string(),
408            "Test Customer".to_string(),
409            NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
410            dec!(1000),
411            "USD".to_string(),
412            PaymentMethod::WireTransfer,
413            "1000".to_string(),
414        );
415
416        receipt.apply_to_invoice("INV001".to_string(), dec!(800), dec!(20));
417
418        assert_eq!(receipt.net_applied, dec!(800));
419        assert_eq!(receipt.discount_taken, dec!(20));
420        assert_eq!(receipt.unapplied_amount, dec!(200));
421        assert_eq!(receipt.applied_invoices.len(), 1);
422    }
423
424    #[test]
425    fn test_receipt_fully_applied() {
426        let mut receipt = ARReceipt::new(
427            "REC001".to_string(),
428            "1000".to_string(),
429            "CUST001".to_string(),
430            "Test Customer".to_string(),
431            NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
432            dec!(1000),
433            "USD".to_string(),
434            PaymentMethod::WireTransfer,
435            "1000".to_string(),
436        );
437
438        receipt.apply_to_invoice("INV001".to_string(), dec!(1000), Decimal::ZERO);
439
440        assert_eq!(receipt.status, SubledgerDocumentStatus::Cleared);
441        assert_eq!(receipt.unapplied_amount, Decimal::ZERO);
442    }
443
444    #[test]
445    fn test_batch_totals() {
446        let mut batch = ARReceiptBatch::new(
447            "BATCH001".to_string(),
448            "1000".to_string(),
449            NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
450            "USER1".to_string(),
451        );
452
453        let receipt1 = ARReceipt::new(
454            "REC001".to_string(),
455            "1000".to_string(),
456            "CUST001".to_string(),
457            "Customer 1".to_string(),
458            NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
459            dec!(500),
460            "USD".to_string(),
461            PaymentMethod::WireTransfer,
462            "1000".to_string(),
463        );
464
465        let receipt2 = ARReceipt::new(
466            "REC002".to_string(),
467            "1000".to_string(),
468            "CUST002".to_string(),
469            "Customer 2".to_string(),
470            NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
471            dec!(750),
472            "USD".to_string(),
473            PaymentMethod::Check,
474            "1000".to_string(),
475        );
476
477        batch.add_receipt(receipt1);
478        batch.add_receipt(receipt2);
479
480        assert_eq!(batch.count(), 2);
481        assert_eq!(batch.total_amount, dec!(1250));
482    }
483}