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)]
379mod tests {
380 use super::*;
381 use rust_decimal_macros::dec;
382
383 #[test]
384 fn test_receipt_creation() {
385 let receipt = ARReceipt::new(
386 "REC001".to_string(),
387 "1000".to_string(),
388 "CUST001".to_string(),
389 "Test Customer".to_string(),
390 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
391 dec!(1000),
392 "USD".to_string(),
393 PaymentMethod::WireTransfer,
394 "1000".to_string(),
395 );
396
397 assert_eq!(receipt.amount.document_amount, dec!(1000));
398 assert_eq!(receipt.unapplied_amount, dec!(1000));
399 assert_eq!(receipt.status, SubledgerDocumentStatus::Open);
400 }
401
402 #[test]
403 fn test_apply_to_invoice() {
404 let mut receipt = ARReceipt::new(
405 "REC001".to_string(),
406 "1000".to_string(),
407 "CUST001".to_string(),
408 "Test Customer".to_string(),
409 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
410 dec!(1000),
411 "USD".to_string(),
412 PaymentMethod::WireTransfer,
413 "1000".to_string(),
414 );
415
416 receipt.apply_to_invoice("INV001".to_string(), dec!(800), dec!(20));
417
418 assert_eq!(receipt.net_applied, dec!(800));
419 assert_eq!(receipt.discount_taken, dec!(20));
420 assert_eq!(receipt.unapplied_amount, dec!(200));
421 assert_eq!(receipt.applied_invoices.len(), 1);
422 }
423
424 #[test]
425 fn test_receipt_fully_applied() {
426 let mut receipt = ARReceipt::new(
427 "REC001".to_string(),
428 "1000".to_string(),
429 "CUST001".to_string(),
430 "Test Customer".to_string(),
431 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
432 dec!(1000),
433 "USD".to_string(),
434 PaymentMethod::WireTransfer,
435 "1000".to_string(),
436 );
437
438 receipt.apply_to_invoice("INV001".to_string(), dec!(1000), Decimal::ZERO);
439
440 assert_eq!(receipt.status, SubledgerDocumentStatus::Cleared);
441 assert_eq!(receipt.unapplied_amount, Decimal::ZERO);
442 }
443
444 #[test]
445 fn test_batch_totals() {
446 let mut batch = ARReceiptBatch::new(
447 "BATCH001".to_string(),
448 "1000".to_string(),
449 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
450 "USER1".to_string(),
451 );
452
453 let receipt1 = ARReceipt::new(
454 "REC001".to_string(),
455 "1000".to_string(),
456 "CUST001".to_string(),
457 "Customer 1".to_string(),
458 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
459 dec!(500),
460 "USD".to_string(),
461 PaymentMethod::WireTransfer,
462 "1000".to_string(),
463 );
464
465 let receipt2 = ARReceipt::new(
466 "REC002".to_string(),
467 "1000".to_string(),
468 "CUST002".to_string(),
469 "Customer 2".to_string(),
470 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
471 dec!(750),
472 "USD".to_string(),
473 PaymentMethod::Check,
474 "1000".to_string(),
475 );
476
477 batch.add_receipt(receipt1);
478 batch.add_receipt(receipt2);
479
480 assert_eq!(batch.count(), 2);
481 assert_eq!(batch.total_amount, dec!(1250));
482 }
483}