1use chrono::{DateTime, NaiveDate, Utc};
4use rust_decimal::Decimal;
5use rust_decimal_macros::dec;
6use serde::{Deserialize, Serialize};
7
8use crate::models::subledger::{
9 ClearingInfo, CurrencyAmount, DunningInfo, GLReference, PaymentTerms, SubledgerDocumentStatus,
10 TaxInfo,
11};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ARInvoice {
16 pub invoice_number: String,
18 pub company_code: String,
20 pub customer_id: String,
22 pub customer_name: String,
24 pub invoice_date: NaiveDate,
26 pub posting_date: NaiveDate,
28 pub due_date: NaiveDate,
30 pub baseline_date: NaiveDate,
32 pub invoice_type: ARInvoiceType,
34 pub status: SubledgerDocumentStatus,
36 pub lines: Vec<ARInvoiceLine>,
38 pub net_amount: CurrencyAmount,
40 pub tax_amount: CurrencyAmount,
42 pub gross_amount: CurrencyAmount,
44 pub amount_paid: Decimal,
46 pub amount_remaining: Decimal,
48 pub payment_terms: PaymentTerms,
50 pub tax_details: Vec<TaxInfo>,
52 pub gl_reference: Option<GLReference>,
54 pub clearing_info: Vec<ClearingInfo>,
56 pub dunning_info: DunningInfo,
58 pub reference_documents: Vec<ARDocumentReference>,
60 pub cost_center: Option<String>,
62 pub profit_center: Option<String>,
64 pub sales_org: Option<String>,
66 pub distribution_channel: Option<String>,
68 pub division: Option<String>,
70 pub created_at: DateTime<Utc>,
72 pub created_by: Option<String>,
74 pub modified_at: Option<DateTime<Utc>>,
76 pub notes: Option<String>,
78}
79
80impl ARInvoice {
81 pub fn new(
83 invoice_number: String,
84 company_code: String,
85 customer_id: String,
86 customer_name: String,
87 invoice_date: NaiveDate,
88 payment_terms: PaymentTerms,
89 currency: String,
90 ) -> Self {
91 let due_date = payment_terms.calculate_due_date(invoice_date);
92
93 Self {
94 invoice_number,
95 company_code,
96 customer_id,
97 customer_name,
98 invoice_date,
99 posting_date: invoice_date,
100 due_date,
101 baseline_date: invoice_date,
102 invoice_type: ARInvoiceType::Standard,
103 status: SubledgerDocumentStatus::Open,
104 lines: Vec::new(),
105 net_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency.clone()),
106 tax_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency.clone()),
107 gross_amount: CurrencyAmount::single_currency(Decimal::ZERO, currency),
108 amount_paid: Decimal::ZERO,
109 amount_remaining: Decimal::ZERO,
110 payment_terms,
111 tax_details: Vec::new(),
112 gl_reference: None,
113 clearing_info: Vec::new(),
114 dunning_info: DunningInfo::default(),
115 reference_documents: Vec::new(),
116 cost_center: None,
117 profit_center: None,
118 sales_org: None,
119 distribution_channel: None,
120 division: None,
121 created_at: Utc::now(),
122 created_by: None,
123 modified_at: None,
124 notes: None,
125 }
126 }
127
128 pub fn add_line(&mut self, line: ARInvoiceLine) {
130 self.lines.push(line);
131 self.recalculate_totals();
132 }
133
134 pub fn recalculate_totals(&mut self) {
136 let net_total: Decimal = self.lines.iter().map(|l| l.net_amount).sum();
137 let tax_total: Decimal = self.lines.iter().map(|l| l.tax_amount).sum();
138 let gross_total = net_total + tax_total;
139
140 self.net_amount.document_amount = net_total;
141 self.net_amount.local_amount = net_total * self.net_amount.exchange_rate;
142 self.tax_amount.document_amount = tax_total;
143 self.tax_amount.local_amount = tax_total * self.tax_amount.exchange_rate;
144 self.gross_amount.document_amount = gross_total;
145 self.gross_amount.local_amount = gross_total * self.gross_amount.exchange_rate;
146 self.amount_remaining = gross_total - self.amount_paid;
147 }
148
149 pub fn apply_payment(&mut self, amount: Decimal, clearing: ClearingInfo) {
151 self.amount_paid += amount;
152 self.amount_remaining = self.gross_amount.document_amount - self.amount_paid;
153 self.clearing_info.push(clearing);
154
155 self.status = if self.amount_remaining <= Decimal::ZERO {
156 SubledgerDocumentStatus::Cleared
157 } else {
158 SubledgerDocumentStatus::PartiallyCleared
159 };
160
161 self.modified_at = Some(Utc::now());
162 }
163
164 pub fn is_overdue(&self, as_of_date: NaiveDate) -> bool {
166 self.status == SubledgerDocumentStatus::Open && as_of_date > self.due_date
167 }
168
169 pub fn days_overdue(&self, as_of_date: NaiveDate) -> i64 {
171 if self.is_overdue(as_of_date) {
172 (as_of_date - self.due_date).num_days()
173 } else {
174 0
175 }
176 }
177
178 pub fn available_discount(&self, payment_date: NaiveDate) -> Decimal {
180 self.payment_terms.calculate_discount(
181 self.gross_amount.document_amount,
182 payment_date,
183 self.baseline_date,
184 )
185 }
186
187 pub fn set_gl_reference(&mut self, reference: GLReference) {
189 self.gl_reference = Some(reference);
190 }
191
192 pub fn add_reference(&mut self, reference: ARDocumentReference) {
194 self.reference_documents.push(reference);
195 }
196
197 pub fn reverse(&mut self, reversal_date: NaiveDate, reason: String) {
199 self.status = SubledgerDocumentStatus::Reversed;
200 self.notes = Some(format!(
201 "{}Reversed on {}: {}",
202 self.notes
203 .as_ref()
204 .map(|n| format!("{}. ", n))
205 .unwrap_or_default(),
206 reversal_date,
207 reason
208 ));
209 self.modified_at = Some(Utc::now());
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
215pub enum ARInvoiceType {
216 #[default]
218 Standard,
219 DownPaymentRequest,
221 Recurring,
223 CreditInvoice,
225 DebitInvoice,
227 ProForma,
229 Intercompany,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct ARInvoiceLine {
236 pub line_number: u32,
238 pub material_id: Option<String>,
240 pub description: String,
242 pub quantity: Decimal,
244 pub unit: String,
246 pub unit_price: Decimal,
248 pub net_amount: Decimal,
250 pub tax_code: Option<String>,
252 pub tax_rate: Decimal,
254 pub tax_amount: Decimal,
256 pub gross_amount: Decimal,
258 pub revenue_account: String,
260 pub cost_center: Option<String>,
262 pub profit_center: Option<String>,
264 pub reference: Option<String>,
266}
267
268impl ARInvoiceLine {
269 pub fn new(
271 line_number: u32,
272 description: String,
273 quantity: Decimal,
274 unit: String,
275 unit_price: Decimal,
276 revenue_account: String,
277 ) -> Self {
278 let net_amount = (quantity * unit_price).round_dp(2);
279 Self {
280 line_number,
281 material_id: None,
282 description,
283 quantity,
284 unit,
285 unit_price,
286 net_amount,
287 tax_code: None,
288 tax_rate: Decimal::ZERO,
289 tax_amount: Decimal::ZERO,
290 gross_amount: net_amount,
291 revenue_account,
292 cost_center: None,
293 profit_center: None,
294 reference: None,
295 }
296 }
297
298 pub fn with_tax(mut self, tax_code: String, tax_rate: Decimal) -> Self {
300 self.tax_code = Some(tax_code);
301 self.tax_rate = tax_rate;
302 self.tax_amount = (self.net_amount * tax_rate / dec!(100)).round_dp(2);
303 self.gross_amount = self.net_amount + self.tax_amount;
304 self
305 }
306
307 pub fn with_material(mut self, material_id: String) -> Self {
309 self.material_id = Some(material_id);
310 self
311 }
312
313 pub fn with_cost_center(mut self, cost_center: String, profit_center: Option<String>) -> Self {
315 self.cost_center = Some(cost_center);
316 self.profit_center = profit_center;
317 self
318 }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct ARDocumentReference {
324 pub document_type: ARReferenceDocType,
326 pub document_number: String,
328 pub document_date: NaiveDate,
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334pub enum ARReferenceDocType {
335 SalesOrder,
337 Delivery,
339 Contract,
341 Quotation,
343 ReturnOrder,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct CustomerARSummary {
350 pub customer_id: String,
352 pub customer_name: String,
354 pub total_open: Decimal,
356 pub total_overdue: Decimal,
358 pub open_invoice_count: u32,
360 pub overdue_invoice_count: u32,
362 pub oldest_open_date: Option<NaiveDate>,
364 pub credit_limit: Option<Decimal>,
366 pub credit_utilization: Option<Decimal>,
368 pub payment_score: Option<Decimal>,
370}
371
372impl CustomerARSummary {
373 pub fn from_invoices(
375 customer_id: String,
376 customer_name: String,
377 invoices: &[ARInvoice],
378 as_of_date: NaiveDate,
379 credit_limit: Option<Decimal>,
380 ) -> Self {
381 let open_invoices: Vec<_> = invoices
382 .iter()
383 .filter(|i| {
384 i.customer_id == customer_id
385 && matches!(
386 i.status,
387 SubledgerDocumentStatus::Open | SubledgerDocumentStatus::PartiallyCleared
388 )
389 })
390 .collect();
391
392 let total_open: Decimal = open_invoices.iter().map(|i| i.amount_remaining).sum();
393 let overdue_invoices: Vec<_> = open_invoices
394 .iter()
395 .filter(|i| i.is_overdue(as_of_date))
396 .collect();
397 let total_overdue: Decimal = overdue_invoices.iter().map(|i| i.amount_remaining).sum();
398
399 let oldest_open_date = open_invoices.iter().map(|i| i.invoice_date).min();
400
401 let credit_utilization = credit_limit.map(|limit| {
402 if limit > Decimal::ZERO {
403 (total_open / limit * dec!(100)).round_dp(2)
404 } else {
405 Decimal::ZERO
406 }
407 });
408
409 Self {
410 customer_id,
411 customer_name,
412 total_open,
413 total_overdue,
414 open_invoice_count: open_invoices.len() as u32,
415 overdue_invoice_count: overdue_invoices.len() as u32,
416 oldest_open_date,
417 credit_limit,
418 credit_utilization,
419 payment_score: None,
420 }
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 fn create_test_invoice() -> ARInvoice {
429 let mut invoice = ARInvoice::new(
430 "INV001".to_string(),
431 "1000".to_string(),
432 "CUST001".to_string(),
433 "Test Customer".to_string(),
434 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
435 PaymentTerms::net_30(),
436 "USD".to_string(),
437 );
438
439 let line = ARInvoiceLine::new(
440 1,
441 "Product A".to_string(),
442 dec!(10),
443 "EA".to_string(),
444 dec!(100),
445 "4000".to_string(),
446 )
447 .with_tax("VAT".to_string(), dec!(20));
448
449 invoice.add_line(line);
450 invoice
451 }
452
453 #[test]
454 fn test_invoice_totals() {
455 let invoice = create_test_invoice();
456 assert_eq!(invoice.net_amount.document_amount, dec!(1000));
457 assert_eq!(invoice.tax_amount.document_amount, dec!(200));
458 assert_eq!(invoice.gross_amount.document_amount, dec!(1200));
459 }
460
461 #[test]
462 fn test_invoice_due_date() {
463 let invoice = create_test_invoice();
464 assert_eq!(
465 invoice.due_date,
466 NaiveDate::from_ymd_opt(2024, 2, 14).unwrap()
467 );
468 }
469
470 #[test]
471 fn test_invoice_overdue() {
472 let invoice = create_test_invoice();
473 let before_due = NaiveDate::from_ymd_opt(2024, 2, 10).unwrap();
474 let after_due = NaiveDate::from_ymd_opt(2024, 2, 20).unwrap();
475
476 assert!(!invoice.is_overdue(before_due));
477 assert!(invoice.is_overdue(after_due));
478 assert_eq!(invoice.days_overdue(after_due), 6);
479 }
480
481 #[test]
482 fn test_apply_payment() {
483 let mut invoice = create_test_invoice();
484 let clearing = ClearingInfo {
485 clearing_document: "PAY001".to_string(),
486 clearing_date: NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
487 clearing_amount: dec!(600),
488 clearing_type: crate::models::subledger::ClearingType::Payment,
489 };
490
491 invoice.apply_payment(dec!(600), clearing);
492 assert_eq!(invoice.amount_paid, dec!(600));
493 assert_eq!(invoice.amount_remaining, dec!(600));
494 assert_eq!(invoice.status, SubledgerDocumentStatus::PartiallyCleared);
495 }
496}