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