use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::models::subledger::{CurrencyAmount, GLReference, SubledgerDocumentStatus};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ARReceipt {
pub receipt_number: String,
pub company_code: String,
pub customer_id: String,
pub customer_name: String,
pub receipt_date: NaiveDate,
pub posting_date: NaiveDate,
pub value_date: NaiveDate,
pub receipt_type: ARReceiptType,
pub status: SubledgerDocumentStatus,
pub amount: CurrencyAmount,
pub bank_charges: Decimal,
pub discount_taken: Decimal,
pub write_off_amount: Decimal,
pub net_applied: Decimal,
pub unapplied_amount: Decimal,
pub payment_method: PaymentMethod,
pub bank_account: String,
pub bank_reference: Option<String>,
pub check_number: Option<String>,
pub applied_invoices: Vec<ReceiptApplication>,
pub gl_references: Vec<GLReference>,
#[serde(with = "crate::serde_timestamp::utc")]
pub created_at: DateTime<Utc>,
pub created_by: Option<String>,
pub notes: Option<String>,
}
impl ARReceipt {
#[allow(clippy::too_many_arguments)]
pub fn new(
receipt_number: String,
company_code: String,
customer_id: String,
customer_name: String,
receipt_date: NaiveDate,
amount: Decimal,
currency: String,
payment_method: PaymentMethod,
bank_account: String,
) -> Self {
Self {
receipt_number,
company_code,
customer_id,
customer_name,
receipt_date,
posting_date: receipt_date,
value_date: receipt_date,
receipt_type: ARReceiptType::Standard,
status: SubledgerDocumentStatus::Open,
amount: CurrencyAmount::single_currency(amount, currency),
bank_charges: Decimal::ZERO,
discount_taken: Decimal::ZERO,
write_off_amount: Decimal::ZERO,
net_applied: Decimal::ZERO,
unapplied_amount: amount,
payment_method,
bank_account,
bank_reference: None,
check_number: None,
applied_invoices: Vec::new(),
gl_references: Vec::new(),
created_at: Utc::now(),
created_by: None,
notes: None,
}
}
pub fn apply_to_invoice(
&mut self,
invoice_number: String,
amount_applied: Decimal,
discount: Decimal,
) {
let application = ReceiptApplication {
invoice_number,
amount_applied,
discount_taken: discount,
write_off: Decimal::ZERO,
application_date: self.receipt_date,
};
self.applied_invoices.push(application);
self.net_applied += amount_applied;
self.discount_taken += discount;
self.unapplied_amount = self.amount.document_amount - self.net_applied;
if self.unapplied_amount <= Decimal::ZERO {
self.status = SubledgerDocumentStatus::Cleared;
}
}
pub fn with_bank_charges(mut self, charges: Decimal) -> Self {
self.bank_charges = charges;
self.unapplied_amount -= charges;
self
}
pub fn with_check(mut self, check_number: String) -> Self {
self.check_number = Some(check_number);
self.payment_method = PaymentMethod::Check;
self
}
pub fn with_bank_reference(mut self, reference: String) -> Self {
self.bank_reference = Some(reference);
self
}
pub fn add_gl_reference(&mut self, reference: GLReference) {
self.gl_references.push(reference);
}
pub fn total_settlement(&self) -> Decimal {
self.net_applied + self.discount_taken + self.write_off_amount
}
pub fn reverse(&mut self, reason: String) {
self.status = SubledgerDocumentStatus::Reversed;
self.notes = Some(format!(
"{}Reversed: {}",
self.notes
.as_ref()
.map(|n| format!("{n}. "))
.unwrap_or_default(),
reason
));
}
#[allow(clippy::too_many_arguments)]
pub fn on_account(
receipt_number: String,
company_code: String,
customer_id: String,
customer_name: String,
receipt_date: NaiveDate,
amount: Decimal,
currency: String,
payment_method: PaymentMethod,
bank_account: String,
) -> Self {
let mut receipt = Self::new(
receipt_number,
company_code,
customer_id,
customer_name,
receipt_date,
amount,
currency,
payment_method,
bank_account,
);
receipt.receipt_type = ARReceiptType::OnAccount;
receipt
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ARReceiptType {
#[default]
Standard,
OnAccount,
DownPayment,
Refund,
WriteOff,
Netting,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum PaymentMethod {
#[default]
WireTransfer,
Check,
ACH,
CreditCard,
Cash,
LetterOfCredit,
Netting,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiptApplication {
pub invoice_number: String,
pub amount_applied: Decimal,
pub discount_taken: Decimal,
pub write_off: Decimal,
pub application_date: NaiveDate,
}
impl ReceiptApplication {
pub fn total_settlement(&self) -> Decimal {
self.amount_applied + self.discount_taken + self.write_off
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ARReceiptBatch {
pub batch_id: String,
pub company_code: String,
pub batch_date: NaiveDate,
pub receipts: Vec<ARReceipt>,
pub total_amount: Decimal,
pub status: BatchStatus,
pub created_by: String,
#[serde(with = "crate::serde_timestamp::utc")]
pub created_at: DateTime<Utc>,
}
impl ARReceiptBatch {
pub fn new(
batch_id: String,
company_code: String,
batch_date: NaiveDate,
created_by: String,
) -> Self {
Self {
batch_id,
company_code,
batch_date,
receipts: Vec::new(),
total_amount: Decimal::ZERO,
status: BatchStatus::Open,
created_by,
created_at: Utc::now(),
}
}
pub fn add_receipt(&mut self, receipt: ARReceipt) {
self.total_amount += receipt.amount.document_amount;
self.receipts.push(receipt);
}
pub fn post(&mut self) {
self.status = BatchStatus::Posted;
}
pub fn count(&self) -> usize {
self.receipts.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BatchStatus {
Open,
Submitted,
Approved,
Posted,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankStatementLine {
pub line_id: String,
pub bank_account: String,
pub statement_date: NaiveDate,
pub value_date: NaiveDate,
pub amount: Decimal,
pub currency: String,
pub bank_reference: String,
pub counterparty_name: Option<String>,
pub counterparty_account: Option<String>,
pub payment_reference: Option<String>,
pub is_matched: bool,
pub matched_receipt: Option<String>,
}
impl BankStatementLine {
pub fn is_receipt(&self) -> bool {
self.amount > Decimal::ZERO
}
pub fn match_to_receipt(&mut self, receipt_number: String) {
self.is_matched = true;
self.matched_receipt = Some(receipt_number);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_receipt_creation() {
let receipt = ARReceipt::new(
"REC001".to_string(),
"1000".to_string(),
"CUST001".to_string(),
"Test Customer".to_string(),
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
dec!(1000),
"USD".to_string(),
PaymentMethod::WireTransfer,
"1000".to_string(),
);
assert_eq!(receipt.amount.document_amount, dec!(1000));
assert_eq!(receipt.unapplied_amount, dec!(1000));
assert_eq!(receipt.status, SubledgerDocumentStatus::Open);
}
#[test]
fn test_apply_to_invoice() {
let mut receipt = ARReceipt::new(
"REC001".to_string(),
"1000".to_string(),
"CUST001".to_string(),
"Test Customer".to_string(),
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
dec!(1000),
"USD".to_string(),
PaymentMethod::WireTransfer,
"1000".to_string(),
);
receipt.apply_to_invoice("INV001".to_string(), dec!(800), dec!(20));
assert_eq!(receipt.net_applied, dec!(800));
assert_eq!(receipt.discount_taken, dec!(20));
assert_eq!(receipt.unapplied_amount, dec!(200));
assert_eq!(receipt.applied_invoices.len(), 1);
}
#[test]
fn test_receipt_fully_applied() {
let mut receipt = ARReceipt::new(
"REC001".to_string(),
"1000".to_string(),
"CUST001".to_string(),
"Test Customer".to_string(),
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
dec!(1000),
"USD".to_string(),
PaymentMethod::WireTransfer,
"1000".to_string(),
);
receipt.apply_to_invoice("INV001".to_string(), dec!(1000), Decimal::ZERO);
assert_eq!(receipt.status, SubledgerDocumentStatus::Cleared);
assert_eq!(receipt.unapplied_amount, Decimal::ZERO);
}
#[test]
fn test_batch_totals() {
let mut batch = ARReceiptBatch::new(
"BATCH001".to_string(),
"1000".to_string(),
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
"USER1".to_string(),
);
let receipt1 = ARReceipt::new(
"REC001".to_string(),
"1000".to_string(),
"CUST001".to_string(),
"Customer 1".to_string(),
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
dec!(500),
"USD".to_string(),
PaymentMethod::WireTransfer,
"1000".to_string(),
);
let receipt2 = ARReceipt::new(
"REC002".to_string(),
"1000".to_string(),
"CUST002".to_string(),
"Customer 2".to_string(),
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
dec!(750),
"USD".to_string(),
PaymentMethod::Check,
"1000".to_string(),
);
batch.add_receipt(receipt1);
batch.add_receipt(receipt2);
assert_eq!(batch.count(), 2);
assert_eq!(batch.total_amount, dec!(1250));
}
}