Skip to main content

datasynth_core/models/subledger/ap/
payment.rs

1//! AP Payment model.
2
3use chrono::{DateTime, NaiveDate, Utc};
4use rust_decimal::Decimal;
5use rust_decimal_macros::dec;
6use serde::{Deserialize, Serialize};
7
8use crate::models::subledger::{CurrencyAmount, GLReference};
9
10/// AP Payment (payment to vendor).
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct APPayment {
13    /// Unique payment number.
14    pub payment_number: String,
15    /// Company code.
16    pub company_code: String,
17    /// Vendor ID.
18    pub vendor_id: String,
19    /// Vendor name.
20    pub vendor_name: String,
21    /// Payment date.
22    pub payment_date: NaiveDate,
23    /// Posting date.
24    pub posting_date: NaiveDate,
25    /// Value date (bank value date).
26    pub value_date: NaiveDate,
27    /// Payment type.
28    pub payment_type: APPaymentType,
29    /// Payment status.
30    pub status: PaymentStatus,
31    /// Payment amount.
32    pub amount: CurrencyAmount,
33    /// Bank charges.
34    pub bank_charges: Decimal,
35    /// Discount taken.
36    pub discount_taken: Decimal,
37    /// Withholding tax.
38    pub withholding_tax: Decimal,
39    /// Net payment amount.
40    pub net_payment: Decimal,
41    /// Payment method.
42    pub payment_method: APPaymentMethod,
43    /// House bank.
44    pub house_bank: String,
45    /// Bank account.
46    pub bank_account: String,
47    /// Vendor bank account.
48    pub vendor_bank_account: Option<String>,
49    /// Check number (if check payment).
50    pub check_number: Option<String>,
51    /// Wire reference.
52    pub wire_reference: Option<String>,
53    /// Paid invoices.
54    pub paid_invoices: Vec<PaymentAllocation>,
55    /// GL references.
56    pub gl_references: Vec<GLReference>,
57    /// Payment run ID.
58    pub payment_run_id: Option<String>,
59    /// Created timestamp.
60    #[serde(with = "crate::serde_timestamp::utc")]
61    pub created_at: DateTime<Utc>,
62    /// Created by user.
63    pub created_by: Option<String>,
64    /// Approved by.
65    pub approved_by: Option<String>,
66    /// Approval date.
67    pub approved_date: Option<NaiveDate>,
68    /// Notes.
69    pub notes: Option<String>,
70}
71
72impl APPayment {
73    /// Creates a new AP payment.
74    #[allow(clippy::too_many_arguments)]
75    pub fn new(
76        payment_number: String,
77        company_code: String,
78        vendor_id: String,
79        vendor_name: String,
80        payment_date: NaiveDate,
81        amount: Decimal,
82        currency: String,
83        payment_method: APPaymentMethod,
84        house_bank: String,
85        bank_account: String,
86    ) -> Self {
87        Self {
88            payment_number,
89            company_code,
90            vendor_id,
91            vendor_name,
92            payment_date,
93            posting_date: payment_date,
94            value_date: payment_date,
95            payment_type: APPaymentType::Standard,
96            status: PaymentStatus::Created,
97            amount: CurrencyAmount::single_currency(amount, currency),
98            bank_charges: Decimal::ZERO,
99            discount_taken: Decimal::ZERO,
100            withholding_tax: Decimal::ZERO,
101            net_payment: amount,
102            payment_method,
103            house_bank,
104            bank_account,
105            vendor_bank_account: None,
106            check_number: None,
107            wire_reference: None,
108            paid_invoices: Vec::new(),
109            gl_references: Vec::new(),
110            payment_run_id: None,
111            created_at: Utc::now(),
112            created_by: None,
113            approved_by: None,
114            approved_date: None,
115            notes: None,
116        }
117    }
118
119    /// Allocates payment to an invoice.
120    pub fn allocate_to_invoice(
121        &mut self,
122        invoice_number: String,
123        amount_paid: Decimal,
124        discount: Decimal,
125        withholding: Decimal,
126    ) {
127        let allocation = PaymentAllocation {
128            invoice_number,
129            amount_paid,
130            discount_taken: discount,
131            withholding_tax: withholding,
132            allocation_date: self.payment_date,
133        };
134
135        self.paid_invoices.push(allocation);
136        self.discount_taken += discount;
137        self.withholding_tax += withholding;
138        self.recalculate_net_payment();
139    }
140
141    /// Recalculates net payment amount.
142    fn recalculate_net_payment(&mut self) {
143        self.net_payment = self.amount.document_amount
144            - self.discount_taken
145            - self.withholding_tax
146            - self.bank_charges;
147    }
148
149    /// Sets bank charges.
150    pub fn with_bank_charges(mut self, charges: Decimal) -> Self {
151        self.bank_charges = charges;
152        self.recalculate_net_payment();
153        self
154    }
155
156    /// Sets check number.
157    pub fn with_check(mut self, check_number: String) -> Self {
158        self.check_number = Some(check_number);
159        self.payment_method = APPaymentMethod::Check;
160        self
161    }
162
163    /// Sets wire reference.
164    pub fn with_wire_reference(mut self, reference: String) -> Self {
165        self.wire_reference = Some(reference);
166        self
167    }
168
169    /// Sets vendor bank account.
170    pub fn with_vendor_bank(mut self, bank_account: String) -> Self {
171        self.vendor_bank_account = Some(bank_account);
172        self
173    }
174
175    /// Approves the payment.
176    pub fn approve(&mut self, approver: String, approval_date: NaiveDate) {
177        self.status = PaymentStatus::Approved;
178        self.approved_by = Some(approver);
179        self.approved_date = Some(approval_date);
180    }
181
182    /// Releases the payment for processing.
183    pub fn release(&mut self) {
184        if self.status == PaymentStatus::Approved {
185            self.status = PaymentStatus::Released;
186        }
187    }
188
189    /// Confirms payment was sent.
190    pub fn confirm_sent(&mut self, reference: Option<String>) {
191        self.status = PaymentStatus::Sent;
192        if let Some(ref_num) = reference {
193            self.wire_reference = Some(ref_num);
194        }
195    }
196
197    /// Confirms payment cleared the bank.
198    pub fn confirm_cleared(&mut self, value_date: NaiveDate) {
199        self.status = PaymentStatus::Cleared;
200        self.value_date = value_date;
201    }
202
203    /// Voids the payment.
204    pub fn void(&mut self, reason: String) {
205        self.status = PaymentStatus::Voided;
206        self.notes = Some(format!(
207            "{}Voided: {}",
208            self.notes
209                .as_ref()
210                .map(|n| format!("{n}. "))
211                .unwrap_or_default(),
212            reason
213        ));
214    }
215
216    /// Gets total settlement amount (payment + discount + withholding).
217    pub fn total_settlement(&self) -> Decimal {
218        self.paid_invoices
219            .iter()
220            .map(PaymentAllocation::total_settlement)
221            .sum()
222    }
223
224    /// Adds a GL reference.
225    pub fn add_gl_reference(&mut self, reference: GLReference) {
226        self.gl_references.push(reference);
227    }
228}
229
230/// Type of AP payment.
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
232pub enum APPaymentType {
233    /// Standard payment.
234    #[default]
235    Standard,
236    /// Down payment.
237    DownPayment,
238    /// Partial payment.
239    Partial,
240    /// Final payment.
241    Final,
242    /// Urgent/rush payment.
243    Urgent,
244    /// Intercompany payment.
245    Intercompany,
246}
247
248/// Payment status.
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
250pub enum PaymentStatus {
251    /// Created, awaiting approval.
252    #[default]
253    Created,
254    /// Approved, awaiting release.
255    Approved,
256    /// Released for processing.
257    Released,
258    /// Sent to bank.
259    Sent,
260    /// Cleared/reconciled.
261    Cleared,
262    /// Voided.
263    Voided,
264    /// Returned/rejected.
265    Returned,
266}
267
268/// Payment method for AP.
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
270pub enum APPaymentMethod {
271    /// Wire transfer.
272    #[default]
273    WireTransfer,
274    /// ACH/Direct debit.
275    ACH,
276    /// Check.
277    Check,
278    /// SEPA transfer.
279    SEPA,
280    /// Credit card.
281    CreditCard,
282    /// Virtual card.
283    VirtualCard,
284    /// Intercompany netting.
285    Netting,
286    /// Letter of credit.
287    LetterOfCredit,
288}
289
290/// Allocation of payment to an invoice.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct PaymentAllocation {
293    /// Invoice number.
294    pub invoice_number: String,
295    /// Amount paid.
296    pub amount_paid: Decimal,
297    /// Discount taken.
298    pub discount_taken: Decimal,
299    /// Withholding tax.
300    pub withholding_tax: Decimal,
301    /// Allocation date.
302    pub allocation_date: NaiveDate,
303}
304
305impl PaymentAllocation {
306    /// Total settlement for this allocation.
307    pub fn total_settlement(&self) -> Decimal {
308        self.amount_paid + self.discount_taken + self.withholding_tax
309    }
310}
311
312/// Payment proposal (before actual payment).
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct PaymentProposal {
315    /// Proposal ID.
316    pub proposal_id: String,
317    /// Company code.
318    pub company_code: String,
319    /// Run date.
320    pub run_date: NaiveDate,
321    /// Payment date.
322    pub payment_date: NaiveDate,
323    /// Proposal status.
324    pub status: ProposalStatus,
325    /// Payment method.
326    pub payment_method: APPaymentMethod,
327    /// Proposed payments.
328    pub proposed_payments: Vec<ProposedPayment>,
329    /// Total payment amount.
330    pub total_amount: Decimal,
331    /// Total discount available.
332    pub total_discount: Decimal,
333    /// Created by.
334    pub created_by: String,
335    /// Created at.
336    #[serde(with = "crate::serde_timestamp::utc")]
337    pub created_at: DateTime<Utc>,
338}
339
340impl PaymentProposal {
341    /// Creates a new payment proposal.
342    pub fn new(
343        proposal_id: String,
344        company_code: String,
345        run_date: NaiveDate,
346        payment_date: NaiveDate,
347        payment_method: APPaymentMethod,
348        created_by: String,
349    ) -> Self {
350        Self {
351            proposal_id,
352            company_code,
353            run_date,
354            payment_date,
355            status: ProposalStatus::Draft,
356            payment_method,
357            proposed_payments: Vec::new(),
358            total_amount: Decimal::ZERO,
359            total_discount: Decimal::ZERO,
360            created_by,
361            created_at: Utc::now(),
362        }
363    }
364
365    /// Adds a proposed payment.
366    pub fn add_payment(&mut self, payment: ProposedPayment) {
367        self.total_amount += payment.amount;
368        self.total_discount += payment.discount;
369        self.proposed_payments.push(payment);
370    }
371
372    /// Gets count of proposed payments.
373    pub fn payment_count(&self) -> usize {
374        self.proposed_payments.len()
375    }
376
377    /// Gets count of invoices.
378    pub fn invoice_count(&self) -> usize {
379        self.proposed_payments
380            .iter()
381            .map(|p| p.invoices.len())
382            .sum()
383    }
384
385    /// Submits for approval.
386    pub fn submit(&mut self) {
387        self.status = ProposalStatus::Submitted;
388    }
389
390    /// Approves the proposal.
391    pub fn approve(&mut self) {
392        self.status = ProposalStatus::Approved;
393    }
394
395    /// Executes the proposal (creates actual payments).
396    pub fn execute(&mut self) {
397        self.status = ProposalStatus::Executed;
398    }
399}
400
401/// Status of payment proposal.
402#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
403pub enum ProposalStatus {
404    /// Draft, can be modified.
405    Draft,
406    /// Submitted for approval.
407    Submitted,
408    /// Approved, ready to execute.
409    Approved,
410    /// Executed (payments created).
411    Executed,
412    /// Cancelled.
413    Cancelled,
414}
415
416/// A proposed payment to a vendor.
417#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct ProposedPayment {
419    /// Vendor ID.
420    pub vendor_id: String,
421    /// Vendor name.
422    pub vendor_name: String,
423    /// Payment amount.
424    pub amount: Decimal,
425    /// Discount amount.
426    pub discount: Decimal,
427    /// Withholding tax.
428    pub withholding_tax: Decimal,
429    /// Net payment.
430    pub net_payment: Decimal,
431    /// Currency.
432    pub currency: String,
433    /// Invoices included.
434    pub invoices: Vec<ProposedInvoice>,
435    /// Is selected for payment.
436    pub is_selected: bool,
437}
438
439/// Invoice in a payment proposal.
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct ProposedInvoice {
442    /// Invoice number.
443    pub invoice_number: String,
444    /// Invoice date.
445    pub invoice_date: NaiveDate,
446    /// Due date.
447    pub due_date: NaiveDate,
448    /// Open amount.
449    pub open_amount: Decimal,
450    /// Proposed payment.
451    pub payment_amount: Decimal,
452    /// Discount available.
453    pub discount: Decimal,
454    /// Days until due (negative if overdue).
455    pub days_until_due: i32,
456}
457
458/// Payment run configuration.
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct PaymentRunConfig {
461    /// Company codes to include.
462    pub company_codes: Vec<String>,
463    /// Payment methods to use.
464    pub payment_methods: Vec<APPaymentMethod>,
465    /// Due date cutoff (pay items due by this date).
466    pub due_date_cutoff: NaiveDate,
467    /// Include discount items (pay early for discount).
468    pub include_discount_items: bool,
469    /// Discount date cutoff.
470    pub discount_date_cutoff: Option<NaiveDate>,
471    /// Maximum payment amount per vendor.
472    pub max_amount_per_vendor: Option<Decimal>,
473    /// Minimum payment amount.
474    pub min_payment_amount: Decimal,
475    /// Exclude blocked items.
476    pub exclude_blocked: bool,
477    /// Vendor filter (if empty, all vendors).
478    pub vendor_filter: Vec<String>,
479}
480
481impl Default for PaymentRunConfig {
482    fn default() -> Self {
483        Self {
484            company_codes: Vec::new(),
485            payment_methods: vec![APPaymentMethod::WireTransfer, APPaymentMethod::Check],
486            due_date_cutoff: chrono::Local::now().date_naive() + chrono::Duration::days(7),
487            include_discount_items: true,
488            discount_date_cutoff: None,
489            max_amount_per_vendor: None,
490            min_payment_amount: dec!(0.01),
491            exclude_blocked: true,
492            vendor_filter: Vec::new(),
493        }
494    }
495}
496
497#[cfg(test)]
498#[allow(clippy::unwrap_used)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn test_payment_creation() {
504        let payment = APPayment::new(
505            "PAY001".to_string(),
506            "1000".to_string(),
507            "VEND001".to_string(),
508            "Test Vendor".to_string(),
509            NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
510            dec!(1000),
511            "USD".to_string(),
512            APPaymentMethod::WireTransfer,
513            "BANK01".to_string(),
514            "100001".to_string(),
515        );
516
517        assert_eq!(payment.amount.document_amount, dec!(1000));
518        assert_eq!(payment.status, PaymentStatus::Created);
519    }
520
521    #[test]
522    fn test_payment_allocation() {
523        let mut payment = APPayment::new(
524            "PAY001".to_string(),
525            "1000".to_string(),
526            "VEND001".to_string(),
527            "Test Vendor".to_string(),
528            NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
529            dec!(1000),
530            "USD".to_string(),
531            APPaymentMethod::WireTransfer,
532            "BANK01".to_string(),
533            "100001".to_string(),
534        );
535
536        payment.allocate_to_invoice(
537            "INV001".to_string(),
538            dec!(980),
539            dec!(20), // 2% discount
540            Decimal::ZERO,
541        );
542
543        assert_eq!(payment.discount_taken, dec!(20));
544        assert_eq!(payment.total_settlement(), dec!(1000));
545        assert_eq!(payment.paid_invoices.len(), 1);
546    }
547
548    #[test]
549    fn test_payment_workflow() {
550        let mut payment = APPayment::new(
551            "PAY001".to_string(),
552            "1000".to_string(),
553            "VEND001".to_string(),
554            "Test Vendor".to_string(),
555            NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
556            dec!(1000),
557            "USD".to_string(),
558            APPaymentMethod::Check,
559            "BANK01".to_string(),
560            "100001".to_string(),
561        )
562        .with_check("CHK12345".to_string());
563
564        assert_eq!(payment.status, PaymentStatus::Created);
565
566        payment.approve(
567            "APPROVER1".to_string(),
568            NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
569        );
570        assert_eq!(payment.status, PaymentStatus::Approved);
571
572        payment.release();
573        assert_eq!(payment.status, PaymentStatus::Released);
574
575        payment.confirm_sent(None);
576        assert_eq!(payment.status, PaymentStatus::Sent);
577
578        payment.confirm_cleared(NaiveDate::from_ymd_opt(2024, 2, 18).unwrap());
579        assert_eq!(payment.status, PaymentStatus::Cleared);
580    }
581
582    #[test]
583    fn test_payment_proposal() {
584        let mut proposal = PaymentProposal::new(
585            "PROP001".to_string(),
586            "1000".to_string(),
587            NaiveDate::from_ymd_opt(2024, 2, 14).unwrap(),
588            NaiveDate::from_ymd_opt(2024, 2, 15).unwrap(),
589            APPaymentMethod::WireTransfer,
590            "USER1".to_string(),
591        );
592
593        let payment = ProposedPayment {
594            vendor_id: "VEND001".to_string(),
595            vendor_name: "Test Vendor".to_string(),
596            amount: dec!(5000),
597            discount: dec!(100),
598            withholding_tax: Decimal::ZERO,
599            net_payment: dec!(4900),
600            currency: "USD".to_string(),
601            invoices: vec![ProposedInvoice {
602                invoice_number: "INV001".to_string(),
603                invoice_date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
604                due_date: NaiveDate::from_ymd_opt(2024, 2, 14).unwrap(),
605                open_amount: dec!(5000),
606                payment_amount: dec!(4900),
607                discount: dec!(100),
608                days_until_due: 0,
609            }],
610            is_selected: true,
611        };
612
613        proposal.add_payment(payment);
614
615        assert_eq!(proposal.payment_count(), 1);
616        assert_eq!(proposal.invoice_count(), 1);
617        assert_eq!(proposal.total_amount, dec!(5000));
618        assert_eq!(proposal.total_discount, dec!(100));
619    }
620}