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