1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct APPayment {
13 pub payment_number: String,
15 pub company_code: String,
17 pub vendor_id: String,
19 pub vendor_name: String,
21 pub payment_date: NaiveDate,
23 pub posting_date: NaiveDate,
25 pub value_date: NaiveDate,
27 pub payment_type: APPaymentType,
29 pub status: PaymentStatus,
31 pub amount: CurrencyAmount,
33 pub bank_charges: Decimal,
35 pub discount_taken: Decimal,
37 pub withholding_tax: Decimal,
39 pub net_payment: Decimal,
41 pub payment_method: APPaymentMethod,
43 pub house_bank: String,
45 pub bank_account: String,
47 pub vendor_bank_account: Option<String>,
49 pub check_number: Option<String>,
51 pub wire_reference: Option<String>,
53 pub paid_invoices: Vec<PaymentAllocation>,
55 pub gl_references: Vec<GLReference>,
57 pub payment_run_id: Option<String>,
59 #[serde(with = "crate::serde_timestamp::utc")]
61 pub created_at: DateTime<Utc>,
62 pub created_by: Option<String>,
64 pub approved_by: Option<String>,
66 pub approved_date: Option<NaiveDate>,
68 pub notes: Option<String>,
70}
71
72impl APPayment {
73 #[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 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 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 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 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 pub fn with_wire_reference(mut self, reference: String) -> Self {
165 self.wire_reference = Some(reference);
166 self
167 }
168
169 pub fn with_vendor_bank(mut self, bank_account: String) -> Self {
171 self.vendor_bank_account = Some(bank_account);
172 self
173 }
174
175 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 pub fn release(&mut self) {
184 if self.status == PaymentStatus::Approved {
185 self.status = PaymentStatus::Released;
186 }
187 }
188
189 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 pub fn confirm_cleared(&mut self, value_date: NaiveDate) {
199 self.status = PaymentStatus::Cleared;
200 self.value_date = value_date;
201 }
202
203 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 pub fn total_settlement(&self) -> Decimal {
218 self.paid_invoices
219 .iter()
220 .map(PaymentAllocation::total_settlement)
221 .sum()
222 }
223
224 pub fn add_gl_reference(&mut self, reference: GLReference) {
226 self.gl_references.push(reference);
227 }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
232pub enum APPaymentType {
233 #[default]
235 Standard,
236 DownPayment,
238 Partial,
240 Final,
242 Urgent,
244 Intercompany,
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
250pub enum PaymentStatus {
251 #[default]
253 Created,
254 Approved,
256 Released,
258 Sent,
260 Cleared,
262 Voided,
264 Returned,
266}
267
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
270pub enum APPaymentMethod {
271 #[default]
273 WireTransfer,
274 ACH,
276 Check,
278 SEPA,
280 CreditCard,
282 VirtualCard,
284 Netting,
286 LetterOfCredit,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct PaymentAllocation {
293 pub invoice_number: String,
295 pub amount_paid: Decimal,
297 pub discount_taken: Decimal,
299 pub withholding_tax: Decimal,
301 pub allocation_date: NaiveDate,
303}
304
305impl PaymentAllocation {
306 pub fn total_settlement(&self) -> Decimal {
308 self.amount_paid + self.discount_taken + self.withholding_tax
309 }
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct PaymentProposal {
315 pub proposal_id: String,
317 pub company_code: String,
319 pub run_date: NaiveDate,
321 pub payment_date: NaiveDate,
323 pub status: ProposalStatus,
325 pub payment_method: APPaymentMethod,
327 pub proposed_payments: Vec<ProposedPayment>,
329 pub total_amount: Decimal,
331 pub total_discount: Decimal,
333 pub created_by: String,
335 #[serde(with = "crate::serde_timestamp::utc")]
337 pub created_at: DateTime<Utc>,
338}
339
340impl PaymentProposal {
341 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 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 pub fn payment_count(&self) -> usize {
374 self.proposed_payments.len()
375 }
376
377 pub fn invoice_count(&self) -> usize {
379 self.proposed_payments
380 .iter()
381 .map(|p| p.invoices.len())
382 .sum()
383 }
384
385 pub fn submit(&mut self) {
387 self.status = ProposalStatus::Submitted;
388 }
389
390 pub fn approve(&mut self) {
392 self.status = ProposalStatus::Approved;
393 }
394
395 pub fn execute(&mut self) {
397 self.status = ProposalStatus::Executed;
398 }
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
403pub enum ProposalStatus {
404 Draft,
406 Submitted,
408 Approved,
410 Executed,
412 Cancelled,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct ProposedPayment {
419 pub vendor_id: String,
421 pub vendor_name: String,
423 pub amount: Decimal,
425 pub discount: Decimal,
427 pub withholding_tax: Decimal,
429 pub net_payment: Decimal,
431 pub currency: String,
433 pub invoices: Vec<ProposedInvoice>,
435 pub is_selected: bool,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct ProposedInvoice {
442 pub invoice_number: String,
444 pub invoice_date: NaiveDate,
446 pub due_date: NaiveDate,
448 pub open_amount: Decimal,
450 pub payment_amount: Decimal,
452 pub discount: Decimal,
454 pub days_until_due: i32,
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct PaymentRunConfig {
461 pub company_codes: Vec<String>,
463 pub payment_methods: Vec<APPaymentMethod>,
465 pub due_date_cutoff: NaiveDate,
467 pub include_discount_items: bool,
469 pub discount_date_cutoff: Option<NaiveDate>,
471 pub max_amount_per_vendor: Option<Decimal>,
473 pub min_payment_amount: Decimal,
475 pub exclude_blocked: bool,
477 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), 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}