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 pub created_at: DateTime<Utc>,
61 pub created_by: Option<String>,
63 pub approved_by: Option<String>,
65 pub approved_date: Option<NaiveDate>,
67 pub notes: Option<String>,
69}
70
71impl APPayment {
72 #[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 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 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 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 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 pub fn with_wire_reference(mut self, reference: String) -> Self {
164 self.wire_reference = Some(reference);
165 self
166 }
167
168 pub fn with_vendor_bank(mut self, bank_account: String) -> Self {
170 self.vendor_bank_account = Some(bank_account);
171 self
172 }
173
174 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 pub fn release(&mut self) {
183 if self.status == PaymentStatus::Approved {
184 self.status = PaymentStatus::Released;
185 }
186 }
187
188 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 pub fn confirm_cleared(&mut self, value_date: NaiveDate) {
198 self.status = PaymentStatus::Cleared;
199 self.value_date = value_date;
200 }
201
202 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 pub fn total_settlement(&self) -> Decimal {
217 self.paid_invoices
218 .iter()
219 .map(|a| a.total_settlement())
220 .sum()
221 }
222
223 pub fn add_gl_reference(&mut self, reference: GLReference) {
225 self.gl_references.push(reference);
226 }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
231pub enum APPaymentType {
232 #[default]
234 Standard,
235 DownPayment,
237 Partial,
239 Final,
241 Urgent,
243 Intercompany,
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
249pub enum PaymentStatus {
250 #[default]
252 Created,
253 Approved,
255 Released,
257 Sent,
259 Cleared,
261 Voided,
263 Returned,
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
269pub enum APPaymentMethod {
270 #[default]
272 WireTransfer,
273 ACH,
275 Check,
277 SEPA,
279 CreditCard,
281 VirtualCard,
283 Netting,
285 LetterOfCredit,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct PaymentAllocation {
292 pub invoice_number: String,
294 pub amount_paid: Decimal,
296 pub discount_taken: Decimal,
298 pub withholding_tax: Decimal,
300 pub allocation_date: NaiveDate,
302}
303
304impl PaymentAllocation {
305 pub fn total_settlement(&self) -> Decimal {
307 self.amount_paid + self.discount_taken + self.withholding_tax
308 }
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct PaymentProposal {
314 pub proposal_id: String,
316 pub company_code: String,
318 pub run_date: NaiveDate,
320 pub payment_date: NaiveDate,
322 pub status: ProposalStatus,
324 pub payment_method: APPaymentMethod,
326 pub proposed_payments: Vec<ProposedPayment>,
328 pub total_amount: Decimal,
330 pub total_discount: Decimal,
332 pub created_by: String,
334 pub created_at: DateTime<Utc>,
336}
337
338impl PaymentProposal {
339 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 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 pub fn payment_count(&self) -> usize {
372 self.proposed_payments.len()
373 }
374
375 pub fn invoice_count(&self) -> usize {
377 self.proposed_payments
378 .iter()
379 .map(|p| p.invoices.len())
380 .sum()
381 }
382
383 pub fn submit(&mut self) {
385 self.status = ProposalStatus::Submitted;
386 }
387
388 pub fn approve(&mut self) {
390 self.status = ProposalStatus::Approved;
391 }
392
393 pub fn execute(&mut self) {
395 self.status = ProposalStatus::Executed;
396 }
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
401pub enum ProposalStatus {
402 Draft,
404 Submitted,
406 Approved,
408 Executed,
410 Cancelled,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct ProposedPayment {
417 pub vendor_id: String,
419 pub vendor_name: String,
421 pub amount: Decimal,
423 pub discount: Decimal,
425 pub withholding_tax: Decimal,
427 pub net_payment: Decimal,
429 pub currency: String,
431 pub invoices: Vec<ProposedInvoice>,
433 pub is_selected: bool,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct ProposedInvoice {
440 pub invoice_number: String,
442 pub invoice_date: NaiveDate,
444 pub due_date: NaiveDate,
446 pub open_amount: Decimal,
448 pub payment_amount: Decimal,
450 pub discount: Decimal,
452 pub days_until_due: i32,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct PaymentRunConfig {
459 pub company_codes: Vec<String>,
461 pub payment_methods: Vec<APPaymentMethod>,
463 pub due_date_cutoff: NaiveDate,
465 pub include_discount_items: bool,
467 pub discount_date_cutoff: Option<NaiveDate>,
469 pub max_amount_per_vendor: Option<Decimal>,
471 pub min_payment_amount: Decimal,
473 pub exclude_blocked: bool,
475 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), 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}