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)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn test_ap_payment_creation() {
482 let payment = Payment::new_ap_payment(
483 "PAY-1000-0000000001",
484 "1000",
485 "V-000001",
486 Decimal::from(1000),
487 2024,
488 1,
489 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
490 "JSMITH",
491 );
492
493 assert_eq!(payment.amount, Decimal::from(1000));
494 assert!(payment.is_vendor);
495 assert_eq!(payment.payment_type, PaymentType::ApPayment);
496 }
497
498 #[test]
499 fn test_ar_receipt_creation() {
500 let payment = Payment::new_ar_receipt(
501 "REC-1000-0000000001",
502 "1000",
503 "C-000001",
504 Decimal::from(5000),
505 2024,
506 1,
507 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
508 "JSMITH",
509 );
510
511 assert_eq!(payment.amount, Decimal::from(5000));
512 assert!(!payment.is_vendor);
513 assert_eq!(payment.payment_type, PaymentType::ArReceipt);
514 }
515
516 #[test]
517 fn test_payment_allocation() {
518 let mut payment = Payment::new_ap_payment(
519 "PAY-1000-0000000001",
520 "1000",
521 "V-000001",
522 Decimal::from(1000),
523 2024,
524 1,
525 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
526 "JSMITH",
527 );
528
529 payment.allocate_to_invoice(
530 "VI-1000-0000000001",
531 DocumentType::VendorInvoice,
532 Decimal::from(980),
533 Decimal::from(20),
534 );
535
536 assert_eq!(payment.total_allocated(), Decimal::from(980));
537 assert_eq!(payment.total_discount, Decimal::from(20));
538 assert_eq!(payment.unallocated(), Decimal::from(20));
539 }
540
541 #[test]
542 fn test_payment_workflow() {
543 let mut payment = Payment::new_ap_payment(
544 "PAY-1000-0000000001",
545 "1000",
546 "V-000001",
547 Decimal::from(1000),
548 2024,
549 1,
550 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
551 "JSMITH",
552 );
553
554 payment.approve("MANAGER");
555 assert_eq!(payment.payment_status, PaymentStatus::Approved);
556
557 payment.send_to_bank("TREASURY");
558 assert_eq!(payment.payment_status, PaymentStatus::Sent);
559
560 payment.clear(
561 NaiveDate::from_ymd_opt(2024, 1, 17).unwrap(),
562 "STMT-2024-01-17-001",
563 );
564 assert!(payment.is_bank_cleared);
565 assert_eq!(payment.payment_status, PaymentStatus::Cleared);
566 }
567}