1use chrono::{DateTime, NaiveDate, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6
7use crate::models::subledger::{CurrencyAmount, GLReference, SubledgerDocumentStatus};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ARReceipt {
12 pub receipt_number: String,
14 pub company_code: String,
16 pub customer_id: String,
18 pub customer_name: String,
20 pub receipt_date: NaiveDate,
22 pub posting_date: NaiveDate,
24 pub value_date: NaiveDate,
26 pub receipt_type: ARReceiptType,
28 pub status: SubledgerDocumentStatus,
30 pub amount: CurrencyAmount,
32 pub bank_charges: Decimal,
34 pub discount_taken: Decimal,
36 pub write_off_amount: Decimal,
38 pub net_applied: Decimal,
40 pub unapplied_amount: Decimal,
42 pub payment_method: PaymentMethod,
44 pub bank_account: String,
46 pub bank_reference: Option<String>,
48 pub check_number: Option<String>,
50 pub applied_invoices: Vec<ReceiptApplication>,
52 pub gl_references: Vec<GLReference>,
54 pub created_at: DateTime<Utc>,
56 pub created_by: Option<String>,
58 pub notes: Option<String>,
60}
61
62impl ARReceipt {
63 #[allow(clippy::too_many_arguments)]
65 pub fn new(
66 receipt_number: String,
67 company_code: String,
68 customer_id: String,
69 customer_name: String,
70 receipt_date: NaiveDate,
71 amount: Decimal,
72 currency: String,
73 payment_method: PaymentMethod,
74 bank_account: String,
75 ) -> Self {
76 Self {
77 receipt_number,
78 company_code,
79 customer_id,
80 customer_name,
81 receipt_date,
82 posting_date: receipt_date,
83 value_date: receipt_date,
84 receipt_type: ARReceiptType::Standard,
85 status: SubledgerDocumentStatus::Open,
86 amount: CurrencyAmount::single_currency(amount, currency),
87 bank_charges: Decimal::ZERO,
88 discount_taken: Decimal::ZERO,
89 write_off_amount: Decimal::ZERO,
90 net_applied: Decimal::ZERO,
91 unapplied_amount: amount,
92 payment_method,
93 bank_account,
94 bank_reference: None,
95 check_number: None,
96 applied_invoices: Vec::new(),
97 gl_references: Vec::new(),
98 created_at: Utc::now(),
99 created_by: None,
100 notes: None,
101 }
102 }
103
104 pub fn apply_to_invoice(
106 &mut self,
107 invoice_number: String,
108 amount_applied: Decimal,
109 discount: Decimal,
110 ) {
111 let application = ReceiptApplication {
112 invoice_number,
113 amount_applied,
114 discount_taken: discount,
115 write_off: Decimal::ZERO,
116 application_date: self.receipt_date,
117 };
118
119 self.applied_invoices.push(application);
120 self.net_applied += amount_applied;
121 self.discount_taken += discount;
122 self.unapplied_amount = self.amount.document_amount - self.net_applied;
123
124 if self.unapplied_amount <= Decimal::ZERO {
125 self.status = SubledgerDocumentStatus::Cleared;
126 }
127 }
128
129 pub fn with_bank_charges(mut self, charges: Decimal) -> Self {
131 self.bank_charges = charges;
132 self.unapplied_amount -= charges;
133 self
134 }
135
136 pub fn with_check(mut self, check_number: String) -> Self {
138 self.check_number = Some(check_number);
139 self.payment_method = PaymentMethod::Check;
140 self
141 }
142
143 pub fn with_bank_reference(mut self, reference: String) -> Self {
145 self.bank_reference = Some(reference);
146 self
147 }
148
149 pub fn add_gl_reference(&mut self, reference: GLReference) {
151 self.gl_references.push(reference);
152 }
153
154 pub fn total_settlement(&self) -> Decimal {
156 self.net_applied + self.discount_taken + self.write_off_amount
157 }
158
159 pub fn reverse(&mut self, reason: String) {
161 self.status = SubledgerDocumentStatus::Reversed;
162 self.notes = Some(format!(
163 "{}Reversed: {}",
164 self.notes
165 .as_ref()
166 .map(|n| format!("{}. ", n))
167 .unwrap_or_default(),
168 reason
169 ));
170 }
171
172 #[allow(clippy::too_many_arguments)]
174 pub fn on_account(
175 receipt_number: String,
176 company_code: String,
177 customer_id: String,
178 customer_name: String,
179 receipt_date: NaiveDate,
180 amount: Decimal,
181 currency: String,
182 payment_method: PaymentMethod,
183 bank_account: String,
184 ) -> Self {
185 let mut receipt = Self::new(
186 receipt_number,
187 company_code,
188 customer_id,
189 customer_name,
190 receipt_date,
191 amount,
192 currency,
193 payment_method,
194 bank_account,
195 );
196 receipt.receipt_type = ARReceiptType::OnAccount;
197 receipt
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
203pub enum ARReceiptType {
204 #[default]
206 Standard,
207 OnAccount,
209 DownPayment,
211 Refund,
213 WriteOff,
215 Netting,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
221pub enum PaymentMethod {
222 #[default]
224 WireTransfer,
225 Check,
227 ACH,
229 CreditCard,
231 Cash,
233 LetterOfCredit,
235 Netting,
237 Other,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct ReceiptApplication {
244 pub invoice_number: String,
246 pub amount_applied: Decimal,
248 pub discount_taken: Decimal,
250 pub write_off: Decimal,
252 pub application_date: NaiveDate,
254}
255
256impl ReceiptApplication {
257 pub fn total_settlement(&self) -> Decimal {
259 self.amount_applied + self.discount_taken + self.write_off
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct ARReceiptBatch {
266 pub batch_id: String,
268 pub company_code: String,
270 pub batch_date: NaiveDate,
272 pub receipts: Vec<ARReceipt>,
274 pub total_amount: Decimal,
276 pub status: BatchStatus,
278 pub created_by: String,
280 pub created_at: DateTime<Utc>,
282}
283
284impl ARReceiptBatch {
285 pub fn new(
287 batch_id: String,
288 company_code: String,
289 batch_date: NaiveDate,
290 created_by: String,
291 ) -> Self {
292 Self {
293 batch_id,
294 company_code,
295 batch_date,
296 receipts: Vec::new(),
297 total_amount: Decimal::ZERO,
298 status: BatchStatus::Open,
299 created_by,
300 created_at: Utc::now(),
301 }
302 }
303
304 pub fn add_receipt(&mut self, receipt: ARReceipt) {
306 self.total_amount += receipt.amount.document_amount;
307 self.receipts.push(receipt);
308 }
309
310 pub fn post(&mut self) {
312 self.status = BatchStatus::Posted;
313 }
314
315 pub fn count(&self) -> usize {
317 self.receipts.len()
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
323pub enum BatchStatus {
324 Open,
326 Submitted,
328 Approved,
330 Posted,
332 Cancelled,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct BankStatementLine {
339 pub line_id: String,
341 pub bank_account: String,
343 pub statement_date: NaiveDate,
345 pub value_date: NaiveDate,
347 pub amount: Decimal,
349 pub currency: String,
351 pub bank_reference: String,
353 pub counterparty_name: Option<String>,
355 pub counterparty_account: Option<String>,
357 pub payment_reference: Option<String>,
359 pub is_matched: bool,
361 pub matched_receipt: Option<String>,
363}
364
365impl BankStatementLine {
366 pub fn is_receipt(&self) -> bool {
368 self.amount > Decimal::ZERO
369 }
370
371 pub fn match_to_receipt(&mut self, receipt_number: String) {
373 self.is_matched = true;
374 self.matched_receipt = Some(receipt_number);
375 }
376}
377
378#[cfg(test)]
379#[allow(clippy::unwrap_used)]
380mod tests {
381 use super::*;
382 use rust_decimal_macros::dec;
383
384 #[test]
385 fn test_receipt_creation() {
386 let receipt = ARReceipt::new(
387 "REC001".to_string(),
388 "1000".to_string(),
389 "CUST001".to_string(),
390 "Test Customer".to_string(),
391 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
392 dec!(1000),
393 "USD".to_string(),
394 PaymentMethod::WireTransfer,
395 "1000".to_string(),
396 );
397
398 assert_eq!(receipt.amount.document_amount, dec!(1000));
399 assert_eq!(receipt.unapplied_amount, dec!(1000));
400 assert_eq!(receipt.status, SubledgerDocumentStatus::Open);
401 }
402
403 #[test]
404 fn test_apply_to_invoice() {
405 let mut receipt = ARReceipt::new(
406 "REC001".to_string(),
407 "1000".to_string(),
408 "CUST001".to_string(),
409 "Test Customer".to_string(),
410 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
411 dec!(1000),
412 "USD".to_string(),
413 PaymentMethod::WireTransfer,
414 "1000".to_string(),
415 );
416
417 receipt.apply_to_invoice("INV001".to_string(), dec!(800), dec!(20));
418
419 assert_eq!(receipt.net_applied, dec!(800));
420 assert_eq!(receipt.discount_taken, dec!(20));
421 assert_eq!(receipt.unapplied_amount, dec!(200));
422 assert_eq!(receipt.applied_invoices.len(), 1);
423 }
424
425 #[test]
426 fn test_receipt_fully_applied() {
427 let mut receipt = ARReceipt::new(
428 "REC001".to_string(),
429 "1000".to_string(),
430 "CUST001".to_string(),
431 "Test Customer".to_string(),
432 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
433 dec!(1000),
434 "USD".to_string(),
435 PaymentMethod::WireTransfer,
436 "1000".to_string(),
437 );
438
439 receipt.apply_to_invoice("INV001".to_string(), dec!(1000), Decimal::ZERO);
440
441 assert_eq!(receipt.status, SubledgerDocumentStatus::Cleared);
442 assert_eq!(receipt.unapplied_amount, Decimal::ZERO);
443 }
444
445 #[test]
446 fn test_batch_totals() {
447 let mut batch = ARReceiptBatch::new(
448 "BATCH001".to_string(),
449 "1000".to_string(),
450 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
451 "USER1".to_string(),
452 );
453
454 let receipt1 = ARReceipt::new(
455 "REC001".to_string(),
456 "1000".to_string(),
457 "CUST001".to_string(),
458 "Customer 1".to_string(),
459 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
460 dec!(500),
461 "USD".to_string(),
462 PaymentMethod::WireTransfer,
463 "1000".to_string(),
464 );
465
466 let receipt2 = ARReceipt::new(
467 "REC002".to_string(),
468 "1000".to_string(),
469 "CUST002".to_string(),
470 "Customer 2".to_string(),
471 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
472 dec!(750),
473 "USD".to_string(),
474 PaymentMethod::Check,
475 "1000".to_string(),
476 );
477
478 batch.add_receipt(receipt1);
479 batch.add_receipt(receipt2);
480
481 assert_eq!(batch.count(), 2);
482 assert_eq!(batch.total_amount, dec!(1250));
483 }
484}