datasynth_core/models/documents/
payment.rs

1//! Payment document models.
2//!
3//! Represents AP payments and AR receipts in the P2P and O2C flows.
4
5use chrono::NaiveDate;
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8
9use super::{DocumentHeader, DocumentReference, DocumentStatus, DocumentType, ReferenceType};
10
11/// Payment type.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum PaymentType {
15    /// Outgoing payment (AP)
16    #[default]
17    ApPayment,
18    /// Incoming payment (AR)
19    ArReceipt,
20    /// Down payment
21    DownPayment,
22    /// Advance payment
23    Advance,
24    /// Refund
25    Refund,
26    /// Clearing (internal)
27    Clearing,
28}
29
30/// Payment method.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
32#[serde(rename_all = "snake_case")]
33pub enum PaymentMethod {
34    /// Bank transfer / ACH
35    #[default]
36    BankTransfer,
37    /// Check
38    Check,
39    /// Wire transfer
40    Wire,
41    /// Credit card
42    CreditCard,
43    /// Direct debit
44    DirectDebit,
45    /// Cash
46    Cash,
47    /// Letter of credit
48    LetterOfCredit,
49}
50
51impl PaymentMethod {
52    /// Get typical processing days for this method.
53    pub fn processing_days(&self) -> u8 {
54        match self {
55            Self::Wire | Self::Cash => 0,
56            Self::BankTransfer | Self::DirectDebit => 1,
57            Self::CreditCard => 2,
58            Self::Check => 5,
59            Self::LetterOfCredit => 7,
60        }
61    }
62}
63
64/// Payment status.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
66#[serde(rename_all = "snake_case")]
67pub enum PaymentStatus {
68    /// Created/pending
69    #[default]
70    Pending,
71    /// Approved
72    Approved,
73    /// Sent to bank
74    Sent,
75    /// Cleared by bank
76    Cleared,
77    /// Rejected
78    Rejected,
79    /// Returned
80    Returned,
81    /// Cancelled
82    Cancelled,
83}
84
85/// Payment allocation to an invoice.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct PaymentAllocation {
88    /// Invoice document ID
89    pub invoice_id: String,
90    /// Invoice type
91    pub invoice_type: DocumentType,
92    /// Allocated amount
93    #[serde(with = "rust_decimal::serde::str")]
94    pub amount: Decimal,
95    /// Discount taken
96    #[serde(with = "rust_decimal::serde::str")]
97    pub discount_taken: Decimal,
98    /// Write-off amount
99    #[serde(with = "rust_decimal::serde::str")]
100    pub write_off: Decimal,
101    /// Withholding tax
102    #[serde(with = "rust_decimal::serde::str")]
103    pub withholding_tax: Decimal,
104    /// Is this allocation cleared?
105    pub is_cleared: bool,
106}
107
108impl PaymentAllocation {
109    /// Create a new allocation.
110    pub fn new(invoice_id: impl Into<String>, invoice_type: DocumentType, amount: Decimal) -> Self {
111        Self {
112            invoice_id: invoice_id.into(),
113            invoice_type,
114            amount,
115            discount_taken: Decimal::ZERO,
116            write_off: Decimal::ZERO,
117            withholding_tax: Decimal::ZERO,
118            is_cleared: false,
119        }
120    }
121
122    /// Set discount taken.
123    pub fn with_discount(mut self, discount: Decimal) -> Self {
124        self.discount_taken = discount;
125        self
126    }
127
128    /// Total applied amount (including discount).
129    pub fn total_applied(&self) -> Decimal {
130        self.amount + self.discount_taken + self.write_off
131    }
132}
133
134/// Payment document (AP Payment or AR Receipt).
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct Payment {
137    /// Document header
138    pub header: DocumentHeader,
139
140    /// Payment type
141    pub payment_type: PaymentType,
142
143    /// Business partner ID (vendor or customer)
144    pub business_partner_id: String,
145
146    /// Is this a vendor (true) or customer (false)?
147    pub is_vendor: bool,
148
149    /// Payment method
150    pub payment_method: PaymentMethod,
151
152    /// Payment status
153    pub payment_status: PaymentStatus,
154
155    /// Payment amount
156    #[serde(with = "rust_decimal::serde::str")]
157    pub amount: Decimal,
158
159    /// Payment currency
160    pub currency: String,
161
162    /// Bank account (house bank)
163    pub house_bank: String,
164
165    /// Bank account ID
166    pub bank_account_id: String,
167
168    /// Partner bank details
169    pub partner_bank_account: Option<String>,
170
171    /// Value date (when funds are available)
172    pub value_date: NaiveDate,
173
174    /// Check number (if check payment)
175    pub check_number: Option<String>,
176
177    /// Wire reference
178    pub wire_reference: Option<String>,
179
180    /// Allocations to invoices
181    pub allocations: Vec<PaymentAllocation>,
182
183    /// Total discount taken
184    #[serde(with = "rust_decimal::serde::str")]
185    pub total_discount: Decimal,
186
187    /// Total write-off
188    #[serde(with = "rust_decimal::serde::str")]
189    pub total_write_off: Decimal,
190
191    /// Bank charges
192    #[serde(with = "rust_decimal::serde::str")]
193    pub bank_charges: Decimal,
194
195    /// Exchange rate (if foreign currency)
196    #[serde(with = "rust_decimal::serde::str")]
197    pub exchange_rate: Decimal,
198
199    /// FX gain/loss
200    #[serde(with = "rust_decimal::serde::str")]
201    pub fx_gain_loss: Decimal,
202
203    /// Payment run ID (if from automatic payment run)
204    pub payment_run_id: Option<String>,
205
206    /// Is this payment cleared by bank?
207    pub is_bank_cleared: bool,
208
209    /// Bank statement reference
210    pub bank_statement_ref: Option<String>,
211
212    /// Cleared date
213    pub cleared_date: Option<NaiveDate>,
214
215    /// Is this payment voided?
216    pub is_voided: bool,
217
218    /// Void reason
219    pub void_reason: Option<String>,
220}
221
222impl Payment {
223    /// Create a new AP payment.
224    #[allow(clippy::too_many_arguments)]
225    pub fn new_ap_payment(
226        payment_id: impl Into<String>,
227        company_code: impl Into<String>,
228        vendor_id: impl Into<String>,
229        amount: Decimal,
230        fiscal_year: u16,
231        fiscal_period: u8,
232        payment_date: NaiveDate,
233        created_by: impl Into<String>,
234    ) -> Self {
235        let header = DocumentHeader::new(
236            payment_id,
237            DocumentType::ApPayment,
238            company_code,
239            fiscal_year,
240            fiscal_period,
241            payment_date,
242            created_by,
243        )
244        .with_currency("USD");
245
246        Self {
247            header,
248            payment_type: PaymentType::ApPayment,
249            business_partner_id: vendor_id.into(),
250            is_vendor: true,
251            payment_method: PaymentMethod::BankTransfer,
252            payment_status: PaymentStatus::Pending,
253            amount,
254            currency: "USD".to_string(),
255            house_bank: "BANK01".to_string(),
256            bank_account_id: "001".to_string(),
257            partner_bank_account: None,
258            value_date: payment_date,
259            check_number: None,
260            wire_reference: None,
261            allocations: Vec::new(),
262            total_discount: Decimal::ZERO,
263            total_write_off: Decimal::ZERO,
264            bank_charges: Decimal::ZERO,
265            exchange_rate: Decimal::ONE,
266            fx_gain_loss: Decimal::ZERO,
267            payment_run_id: None,
268            is_bank_cleared: false,
269            bank_statement_ref: None,
270            cleared_date: None,
271            is_voided: false,
272            void_reason: None,
273        }
274    }
275
276    /// Create a new AR receipt.
277    #[allow(clippy::too_many_arguments)]
278    pub fn new_ar_receipt(
279        payment_id: impl Into<String>,
280        company_code: impl Into<String>,
281        customer_id: impl Into<String>,
282        amount: Decimal,
283        fiscal_year: u16,
284        fiscal_period: u8,
285        payment_date: NaiveDate,
286        created_by: impl Into<String>,
287    ) -> Self {
288        let header = DocumentHeader::new(
289            payment_id,
290            DocumentType::CustomerReceipt,
291            company_code,
292            fiscal_year,
293            fiscal_period,
294            payment_date,
295            created_by,
296        )
297        .with_currency("USD");
298
299        Self {
300            header,
301            payment_type: PaymentType::ArReceipt,
302            business_partner_id: customer_id.into(),
303            is_vendor: false,
304            payment_method: PaymentMethod::BankTransfer,
305            payment_status: PaymentStatus::Pending,
306            amount,
307            currency: "USD".to_string(),
308            house_bank: "BANK01".to_string(),
309            bank_account_id: "001".to_string(),
310            partner_bank_account: None,
311            value_date: payment_date,
312            check_number: None,
313            wire_reference: None,
314            allocations: Vec::new(),
315            total_discount: Decimal::ZERO,
316            total_write_off: Decimal::ZERO,
317            bank_charges: Decimal::ZERO,
318            exchange_rate: Decimal::ONE,
319            fx_gain_loss: Decimal::ZERO,
320            payment_run_id: None,
321            is_bank_cleared: false,
322            bank_statement_ref: None,
323            cleared_date: None,
324            is_voided: false,
325            void_reason: None,
326        }
327    }
328
329    /// Set payment method.
330    pub fn with_payment_method(mut self, method: PaymentMethod) -> Self {
331        self.payment_method = method;
332        self
333    }
334
335    /// Set house bank.
336    pub fn with_bank(
337        mut self,
338        house_bank: impl Into<String>,
339        account_id: impl Into<String>,
340    ) -> Self {
341        self.house_bank = house_bank.into();
342        self.bank_account_id = account_id.into();
343        self
344    }
345
346    /// Set check number.
347    pub fn with_check_number(mut self, check_number: impl Into<String>) -> Self {
348        self.check_number = Some(check_number.into());
349        self.payment_method = PaymentMethod::Check;
350        self
351    }
352
353    /// Set value date.
354    pub fn with_value_date(mut self, date: NaiveDate) -> Self {
355        self.value_date = date;
356        self
357    }
358
359    /// Add an allocation.
360    pub fn add_allocation(&mut self, allocation: PaymentAllocation) {
361        // Add reference to the invoice
362        self.header.add_reference(
363            DocumentReference::new(
364                allocation.invoice_type,
365                allocation.invoice_id.clone(),
366                self.header.document_type,
367                self.header.document_id.clone(),
368                ReferenceType::Payment,
369                self.header.company_code.clone(),
370                self.header.document_date,
371            )
372            .with_amount(allocation.amount),
373        );
374
375        self.allocations.push(allocation);
376        self.recalculate_totals();
377    }
378
379    /// Allocate to an invoice.
380    pub fn allocate_to_invoice(
381        &mut self,
382        invoice_id: impl Into<String>,
383        invoice_type: DocumentType,
384        amount: Decimal,
385        discount: Decimal,
386    ) {
387        let allocation =
388            PaymentAllocation::new(invoice_id, invoice_type, amount).with_discount(discount);
389        self.add_allocation(allocation);
390    }
391
392    /// Recalculate totals.
393    pub fn recalculate_totals(&mut self) {
394        self.total_discount = self.allocations.iter().map(|a| a.discount_taken).sum();
395        self.total_write_off = self.allocations.iter().map(|a| a.write_off).sum();
396    }
397
398    /// Total allocated amount.
399    pub fn total_allocated(&self) -> Decimal {
400        self.allocations.iter().map(|a| a.amount).sum()
401    }
402
403    /// Unallocated amount.
404    pub fn unallocated(&self) -> Decimal {
405        self.amount - self.total_allocated()
406    }
407
408    /// Approve the payment.
409    pub fn approve(&mut self, user: impl Into<String>) {
410        self.payment_status = PaymentStatus::Approved;
411        self.header.update_status(DocumentStatus::Approved, user);
412    }
413
414    /// Send to bank.
415    pub fn send_to_bank(&mut self, user: impl Into<String>) {
416        self.payment_status = PaymentStatus::Sent;
417        self.header.update_status(DocumentStatus::Released, user);
418    }
419
420    /// Record bank clearing.
421    pub fn clear(&mut self, clear_date: NaiveDate, statement_ref: impl Into<String>) {
422        self.is_bank_cleared = true;
423        self.cleared_date = Some(clear_date);
424        self.bank_statement_ref = Some(statement_ref.into());
425        self.payment_status = PaymentStatus::Cleared;
426        self.header.update_status(DocumentStatus::Cleared, "SYSTEM");
427
428        // Mark all allocations as cleared
429        for allocation in &mut self.allocations {
430            allocation.is_cleared = true;
431        }
432    }
433
434    /// Void the payment.
435    pub fn void(&mut self, reason: impl Into<String>, user: impl Into<String>) {
436        self.is_voided = true;
437        self.void_reason = Some(reason.into());
438        self.payment_status = PaymentStatus::Cancelled;
439        self.header.update_status(DocumentStatus::Cancelled, user);
440    }
441
442    /// Post the payment.
443    pub fn post(&mut self, user: impl Into<String>, posting_date: NaiveDate) {
444        self.header.posting_date = Some(posting_date);
445        self.header.update_status(DocumentStatus::Posted, user);
446    }
447
448    /// Generate GL entries for payment.
449    pub fn generate_gl_entries(&self) -> Vec<(String, Decimal, Decimal)> {
450        let mut entries = Vec::new();
451
452        if self.is_vendor {
453            // AP Payment: DR AP, CR Bank
454            entries.push(("210000".to_string(), self.amount, Decimal::ZERO)); // AP
455            entries.push(("110000".to_string(), Decimal::ZERO, self.amount)); // Bank
456
457            if self.total_discount > Decimal::ZERO {
458                entries.push(("740000".to_string(), Decimal::ZERO, self.total_discount));
459                // Purchase discount
460            }
461        } else {
462            // AR Receipt: DR Bank, CR AR
463            entries.push(("110000".to_string(), self.amount, Decimal::ZERO)); // Bank
464            entries.push(("120000".to_string(), Decimal::ZERO, self.amount)); // AR
465
466            if self.total_discount > Decimal::ZERO {
467                entries.push(("440000".to_string(), self.total_discount, Decimal::ZERO));
468                // Sales discount
469            }
470        }
471
472        entries
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    #[test]
481    fn test_ap_payment_creation() {
482        let payment = Payment::new_ap_payment(
483            "PAY-1000-0000000001",
484            "1000",
485            "V-000001",
486            Decimal::from(1000),
487            2024,
488            1,
489            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
490            "JSMITH",
491        );
492
493        assert_eq!(payment.amount, Decimal::from(1000));
494        assert!(payment.is_vendor);
495        assert_eq!(payment.payment_type, PaymentType::ApPayment);
496    }
497
498    #[test]
499    fn test_ar_receipt_creation() {
500        let payment = Payment::new_ar_receipt(
501            "REC-1000-0000000001",
502            "1000",
503            "C-000001",
504            Decimal::from(5000),
505            2024,
506            1,
507            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
508            "JSMITH",
509        );
510
511        assert_eq!(payment.amount, Decimal::from(5000));
512        assert!(!payment.is_vendor);
513        assert_eq!(payment.payment_type, PaymentType::ArReceipt);
514    }
515
516    #[test]
517    fn test_payment_allocation() {
518        let mut payment = Payment::new_ap_payment(
519            "PAY-1000-0000000001",
520            "1000",
521            "V-000001",
522            Decimal::from(1000),
523            2024,
524            1,
525            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
526            "JSMITH",
527        );
528
529        payment.allocate_to_invoice(
530            "VI-1000-0000000001",
531            DocumentType::VendorInvoice,
532            Decimal::from(980),
533            Decimal::from(20),
534        );
535
536        assert_eq!(payment.total_allocated(), Decimal::from(980));
537        assert_eq!(payment.total_discount, Decimal::from(20));
538        assert_eq!(payment.unallocated(), Decimal::from(20));
539    }
540
541    #[test]
542    fn test_payment_workflow() {
543        let mut payment = Payment::new_ap_payment(
544            "PAY-1000-0000000001",
545            "1000",
546            "V-000001",
547            Decimal::from(1000),
548            2024,
549            1,
550            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
551            "JSMITH",
552        );
553
554        payment.approve("MANAGER");
555        assert_eq!(payment.payment_status, PaymentStatus::Approved);
556
557        payment.send_to_bank("TREASURY");
558        assert_eq!(payment.payment_status, PaymentStatus::Sent);
559
560        payment.clear(
561            NaiveDate::from_ymd_opt(2024, 1, 17).unwrap(),
562            "STMT-2024-01-17-001",
563        );
564        assert!(payment.is_bank_cleared);
565        assert_eq!(payment.payment_status, PaymentStatus::Cleared);
566    }
567}