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 #[serde(with = "crate::serde_timestamp::utc")]
56 pub created_at: DateTime<Utc>,
57 pub created_by: Option<String>,
59 pub notes: Option<String>,
61}
62
63impl ARReceipt {
64 #[allow(clippy::too_many_arguments)]
66 pub fn new(
67 receipt_number: String,
68 company_code: String,
69 customer_id: String,
70 customer_name: String,
71 receipt_date: NaiveDate,
72 amount: Decimal,
73 currency: String,
74 payment_method: PaymentMethod,
75 bank_account: String,
76 ) -> Self {
77 Self {
78 receipt_number,
79 company_code,
80 customer_id,
81 customer_name,
82 receipt_date,
83 posting_date: receipt_date,
84 value_date: receipt_date,
85 receipt_type: ARReceiptType::Standard,
86 status: SubledgerDocumentStatus::Open,
87 amount: CurrencyAmount::single_currency(amount, currency),
88 bank_charges: Decimal::ZERO,
89 discount_taken: Decimal::ZERO,
90 write_off_amount: Decimal::ZERO,
91 net_applied: Decimal::ZERO,
92 unapplied_amount: amount,
93 payment_method,
94 bank_account,
95 bank_reference: None,
96 check_number: None,
97 applied_invoices: Vec::new(),
98 gl_references: Vec::new(),
99 created_at: Utc::now(),
100 created_by: None,
101 notes: None,
102 }
103 }
104
105 pub fn apply_to_invoice(
107 &mut self,
108 invoice_number: String,
109 amount_applied: Decimal,
110 discount: Decimal,
111 ) {
112 let application = ReceiptApplication {
113 invoice_number,
114 amount_applied,
115 discount_taken: discount,
116 write_off: Decimal::ZERO,
117 application_date: self.receipt_date,
118 };
119
120 self.applied_invoices.push(application);
121 self.net_applied += amount_applied;
122 self.discount_taken += discount;
123 self.unapplied_amount = self.amount.document_amount - self.net_applied;
124
125 if self.unapplied_amount <= Decimal::ZERO {
126 self.status = SubledgerDocumentStatus::Cleared;
127 }
128 }
129
130 pub fn with_bank_charges(mut self, charges: Decimal) -> Self {
132 self.bank_charges = charges;
133 self.unapplied_amount -= charges;
134 self
135 }
136
137 pub fn with_check(mut self, check_number: String) -> Self {
139 self.check_number = Some(check_number);
140 self.payment_method = PaymentMethod::Check;
141 self
142 }
143
144 pub fn with_bank_reference(mut self, reference: String) -> Self {
146 self.bank_reference = Some(reference);
147 self
148 }
149
150 pub fn add_gl_reference(&mut self, reference: GLReference) {
152 self.gl_references.push(reference);
153 }
154
155 pub fn total_settlement(&self) -> Decimal {
157 self.net_applied + self.discount_taken + self.write_off_amount
158 }
159
160 pub fn reverse(&mut self, reason: String) {
162 self.status = SubledgerDocumentStatus::Reversed;
163 self.notes = Some(format!(
164 "{}Reversed: {}",
165 self.notes
166 .as_ref()
167 .map(|n| format!("{n}. "))
168 .unwrap_or_default(),
169 reason
170 ));
171 }
172
173 #[allow(clippy::too_many_arguments)]
175 pub fn on_account(
176 receipt_number: String,
177 company_code: String,
178 customer_id: String,
179 customer_name: String,
180 receipt_date: NaiveDate,
181 amount: Decimal,
182 currency: String,
183 payment_method: PaymentMethod,
184 bank_account: String,
185 ) -> Self {
186 let mut receipt = Self::new(
187 receipt_number,
188 company_code,
189 customer_id,
190 customer_name,
191 receipt_date,
192 amount,
193 currency,
194 payment_method,
195 bank_account,
196 );
197 receipt.receipt_type = ARReceiptType::OnAccount;
198 receipt
199 }
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
204pub enum ARReceiptType {
205 #[default]
207 Standard,
208 OnAccount,
210 DownPayment,
212 Refund,
214 WriteOff,
216 Netting,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
222pub enum PaymentMethod {
223 #[default]
225 WireTransfer,
226 Check,
228 ACH,
230 CreditCard,
232 Cash,
234 LetterOfCredit,
236 Netting,
238 Other,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct ReceiptApplication {
245 pub invoice_number: String,
247 pub amount_applied: Decimal,
249 pub discount_taken: Decimal,
251 pub write_off: Decimal,
253 pub application_date: NaiveDate,
255}
256
257impl ReceiptApplication {
258 pub fn total_settlement(&self) -> Decimal {
260 self.amount_applied + self.discount_taken + self.write_off
261 }
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct ARReceiptBatch {
267 pub batch_id: String,
269 pub company_code: String,
271 pub batch_date: NaiveDate,
273 pub receipts: Vec<ARReceipt>,
275 pub total_amount: Decimal,
277 pub status: BatchStatus,
279 pub created_by: String,
281 #[serde(with = "crate::serde_timestamp::utc")]
283 pub created_at: DateTime<Utc>,
284}
285
286impl ARReceiptBatch {
287 pub fn new(
289 batch_id: String,
290 company_code: String,
291 batch_date: NaiveDate,
292 created_by: String,
293 ) -> Self {
294 Self {
295 batch_id,
296 company_code,
297 batch_date,
298 receipts: Vec::new(),
299 total_amount: Decimal::ZERO,
300 status: BatchStatus::Open,
301 created_by,
302 created_at: Utc::now(),
303 }
304 }
305
306 pub fn add_receipt(&mut self, receipt: ARReceipt) {
308 self.total_amount += receipt.amount.document_amount;
309 self.receipts.push(receipt);
310 }
311
312 pub fn post(&mut self) {
314 self.status = BatchStatus::Posted;
315 }
316
317 pub fn count(&self) -> usize {
319 self.receipts.len()
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
325pub enum BatchStatus {
326 Open,
328 Submitted,
330 Approved,
332 Posted,
334 Cancelled,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct BankStatementLine {
341 pub line_id: String,
343 pub bank_account: String,
345 pub statement_date: NaiveDate,
347 pub value_date: NaiveDate,
349 pub amount: Decimal,
351 pub currency: String,
353 pub bank_reference: String,
355 pub counterparty_name: Option<String>,
357 pub counterparty_account: Option<String>,
359 pub payment_reference: Option<String>,
361 pub is_matched: bool,
363 pub matched_receipt: Option<String>,
365}
366
367impl BankStatementLine {
368 pub fn is_receipt(&self) -> bool {
370 self.amount > Decimal::ZERO
371 }
372
373 pub fn match_to_receipt(&mut self, receipt_number: String) {
375 self.is_matched = true;
376 self.matched_receipt = Some(receipt_number);
377 }
378}
379
380#[cfg(test)]
381#[allow(clippy::unwrap_used)]
382mod tests {
383 use super::*;
384 use rust_decimal_macros::dec;
385
386 #[test]
387 fn test_receipt_creation() {
388 let receipt = ARReceipt::new(
389 "REC001".to_string(),
390 "1000".to_string(),
391 "CUST001".to_string(),
392 "Test Customer".to_string(),
393 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
394 dec!(1000),
395 "USD".to_string(),
396 PaymentMethod::WireTransfer,
397 "1000".to_string(),
398 );
399
400 assert_eq!(receipt.amount.document_amount, dec!(1000));
401 assert_eq!(receipt.unapplied_amount, dec!(1000));
402 assert_eq!(receipt.status, SubledgerDocumentStatus::Open);
403 }
404
405 #[test]
406 fn test_apply_to_invoice() {
407 let mut receipt = ARReceipt::new(
408 "REC001".to_string(),
409 "1000".to_string(),
410 "CUST001".to_string(),
411 "Test Customer".to_string(),
412 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
413 dec!(1000),
414 "USD".to_string(),
415 PaymentMethod::WireTransfer,
416 "1000".to_string(),
417 );
418
419 receipt.apply_to_invoice("INV001".to_string(), dec!(800), dec!(20));
420
421 assert_eq!(receipt.net_applied, dec!(800));
422 assert_eq!(receipt.discount_taken, dec!(20));
423 assert_eq!(receipt.unapplied_amount, dec!(200));
424 assert_eq!(receipt.applied_invoices.len(), 1);
425 }
426
427 #[test]
428 fn test_receipt_fully_applied() {
429 let mut receipt = ARReceipt::new(
430 "REC001".to_string(),
431 "1000".to_string(),
432 "CUST001".to_string(),
433 "Test Customer".to_string(),
434 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
435 dec!(1000),
436 "USD".to_string(),
437 PaymentMethod::WireTransfer,
438 "1000".to_string(),
439 );
440
441 receipt.apply_to_invoice("INV001".to_string(), dec!(1000), Decimal::ZERO);
442
443 assert_eq!(receipt.status, SubledgerDocumentStatus::Cleared);
444 assert_eq!(receipt.unapplied_amount, Decimal::ZERO);
445 }
446
447 #[test]
448 fn test_batch_totals() {
449 let mut batch = ARReceiptBatch::new(
450 "BATCH001".to_string(),
451 "1000".to_string(),
452 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
453 "USER1".to_string(),
454 );
455
456 let receipt1 = ARReceipt::new(
457 "REC001".to_string(),
458 "1000".to_string(),
459 "CUST001".to_string(),
460 "Customer 1".to_string(),
461 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
462 dec!(500),
463 "USD".to_string(),
464 PaymentMethod::WireTransfer,
465 "1000".to_string(),
466 );
467
468 let receipt2 = ARReceipt::new(
469 "REC002".to_string(),
470 "1000".to_string(),
471 "CUST002".to_string(),
472 "Customer 2".to_string(),
473 NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
474 dec!(750),
475 "USD".to_string(),
476 PaymentMethod::Check,
477 "1000".to_string(),
478 );
479
480 batch.add_receipt(receipt1);
481 batch.add_receipt(receipt2);
482
483 assert_eq!(batch.count(), 2);
484 assert_eq!(batch.total_amount, dec!(1250));
485 }
486}