1use chrono::NaiveDate;
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8
9use super::{DocumentHeader, DocumentReference, DocumentStatus, DocumentType, ReferenceType};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum PaymentType {
15 #[default]
17 ApPayment,
18 ArReceipt,
20 DownPayment,
22 Advance,
24 Refund,
26 Clearing,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
32#[serde(rename_all = "snake_case")]
33pub enum PaymentMethod {
34 #[default]
36 BankTransfer,
37 Check,
39 Wire,
41 CreditCard,
43 DirectDebit,
45 Cash,
47 LetterOfCredit,
49}
50
51impl PaymentMethod {
52 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
66#[serde(rename_all = "snake_case")]
67pub enum PaymentStatus {
68 #[default]
70 Pending,
71 Approved,
73 Sent,
75 Cleared,
77 Rejected,
79 Returned,
81 Cancelled,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct PaymentAllocation {
88 pub invoice_id: String,
90 pub invoice_type: DocumentType,
92 #[serde(with = "rust_decimal::serde::str")]
94 pub amount: Decimal,
95 #[serde(with = "rust_decimal::serde::str")]
97 pub discount_taken: Decimal,
98 #[serde(with = "rust_decimal::serde::str")]
100 pub write_off: Decimal,
101 #[serde(with = "rust_decimal::serde::str")]
103 pub withholding_tax: Decimal,
104 pub is_cleared: bool,
106}
107
108impl PaymentAllocation {
109 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 pub fn with_discount(mut self, discount: Decimal) -> Self {
124 self.discount_taken = discount;
125 self
126 }
127
128 pub fn total_applied(&self) -> Decimal {
130 self.amount + self.discount_taken + self.write_off
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct Payment {
137 pub header: DocumentHeader,
139
140 pub payment_type: PaymentType,
142
143 pub business_partner_id: String,
145
146 pub is_vendor: bool,
148
149 pub payment_method: PaymentMethod,
151
152 pub payment_status: PaymentStatus,
154
155 #[serde(with = "rust_decimal::serde::str")]
157 pub amount: Decimal,
158
159 pub currency: String,
161
162 pub house_bank: String,
164
165 pub bank_account_id: String,
167
168 pub partner_bank_account: Option<String>,
170
171 pub value_date: NaiveDate,
173
174 pub check_number: Option<String>,
176
177 pub wire_reference: Option<String>,
179
180 pub allocations: Vec<PaymentAllocation>,
182
183 #[serde(with = "rust_decimal::serde::str")]
185 pub total_discount: Decimal,
186
187 #[serde(with = "rust_decimal::serde::str")]
189 pub total_write_off: Decimal,
190
191 #[serde(with = "rust_decimal::serde::str")]
193 pub bank_charges: Decimal,
194
195 #[serde(with = "rust_decimal::serde::str")]
197 pub exchange_rate: Decimal,
198
199 #[serde(with = "rust_decimal::serde::str")]
201 pub fx_gain_loss: Decimal,
202
203 pub payment_run_id: Option<String>,
205
206 pub is_bank_cleared: bool,
208
209 pub bank_statement_ref: Option<String>,
211
212 pub cleared_date: Option<NaiveDate>,
214
215 pub is_voided: bool,
217
218 pub void_reason: Option<String>,
220}
221
222impl Payment {
223 #[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 #[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 pub fn with_payment_method(mut self, method: PaymentMethod) -> Self {
331 self.payment_method = method;
332 self
333 }
334
335 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 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 pub fn with_value_date(mut self, date: NaiveDate) -> Self {
355 self.value_date = date;
356 self
357 }
358
359 pub fn add_allocation(&mut self, allocation: PaymentAllocation) {
361 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 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 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 pub fn total_allocated(&self) -> Decimal {
400 self.allocations.iter().map(|a| a.amount).sum()
401 }
402
403 pub fn unallocated(&self) -> Decimal {
405 self.amount - self.total_allocated()
406 }
407
408 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 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 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 for allocation in &mut self.allocations {
430 allocation.is_cleared = true;
431 }
432 }
433
434 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 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 pub fn generate_gl_entries(&self) -> Vec<(String, Decimal, Decimal)> {
450 let mut entries = Vec::new();
451
452 if self.is_vendor {
453 entries.push(("210000".to_string(), self.amount, Decimal::ZERO)); entries.push(("110000".to_string(), Decimal::ZERO, self.amount)); if self.total_discount > Decimal::ZERO {
458 entries.push(("740000".to_string(), Decimal::ZERO, self.total_discount));
459 }
461 } else {
462 entries.push(("110000".to_string(), self.amount, Decimal::ZERO)); entries.push(("120000".to_string(), Decimal::ZERO, self.amount)); if self.total_discount > Decimal::ZERO {
467 entries.push(("440000".to_string(), self.total_discount, Decimal::ZERO));
468 }
470 }
471
472 entries
473 }
474}
475
476#[cfg(test)]
477#[allow(clippy::unwrap_used)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn test_ap_payment_creation() {
483 let payment = Payment::new_ap_payment(
484 "PAY-1000-0000000001",
485 "1000",
486 "V-000001",
487 Decimal::from(1000),
488 2024,
489 1,
490 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
491 "JSMITH",
492 );
493
494 assert_eq!(payment.amount, Decimal::from(1000));
495 assert!(payment.is_vendor);
496 assert_eq!(payment.payment_type, PaymentType::ApPayment);
497 }
498
499 #[test]
500 fn test_ar_receipt_creation() {
501 let payment = Payment::new_ar_receipt(
502 "REC-1000-0000000001",
503 "1000",
504 "C-000001",
505 Decimal::from(5000),
506 2024,
507 1,
508 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
509 "JSMITH",
510 );
511
512 assert_eq!(payment.amount, Decimal::from(5000));
513 assert!(!payment.is_vendor);
514 assert_eq!(payment.payment_type, PaymentType::ArReceipt);
515 }
516
517 #[test]
518 fn test_payment_allocation() {
519 let mut payment = Payment::new_ap_payment(
520 "PAY-1000-0000000001",
521 "1000",
522 "V-000001",
523 Decimal::from(1000),
524 2024,
525 1,
526 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
527 "JSMITH",
528 );
529
530 payment.allocate_to_invoice(
531 "VI-1000-0000000001",
532 DocumentType::VendorInvoice,
533 Decimal::from(980),
534 Decimal::from(20),
535 );
536
537 assert_eq!(payment.total_allocated(), Decimal::from(980));
538 assert_eq!(payment.total_discount, Decimal::from(20));
539 assert_eq!(payment.unallocated(), Decimal::from(20));
540 }
541
542 #[test]
543 fn test_payment_workflow() {
544 let mut payment = Payment::new_ap_payment(
545 "PAY-1000-0000000001",
546 "1000",
547 "V-000001",
548 Decimal::from(1000),
549 2024,
550 1,
551 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
552 "JSMITH",
553 );
554
555 payment.approve("MANAGER");
556 assert_eq!(payment.payment_status, PaymentStatus::Approved);
557
558 payment.send_to_bank("TREASURY");
559 assert_eq!(payment.payment_status, PaymentStatus::Sent);
560
561 payment.clear(
562 NaiveDate::from_ymd_opt(2024, 1, 17).unwrap(),
563 "STMT-2024-01-17-001",
564 );
565 assert!(payment.is_bank_cleared);
566 assert_eq!(payment.payment_status, PaymentStatus::Cleared);
567 }
568}