datasynth_core/models/subledger/ar/
invoice.rs

1//! AR Invoice model.
2
3use 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/// AR Invoice (customer invoice).
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ARInvoice {
16    /// Unique invoice number.
17    pub invoice_number: String,
18    /// Company code.
19    pub company_code: String,
20    /// Customer ID.
21    pub customer_id: String,
22    /// Customer name.
23    pub customer_name: String,
24    /// Invoice date.
25    pub invoice_date: NaiveDate,
26    /// Posting date.
27    pub posting_date: NaiveDate,
28    /// Due date.
29    pub due_date: NaiveDate,
30    /// Baseline date for payment terms.
31    pub baseline_date: NaiveDate,
32    /// Invoice type.
33    pub invoice_type: ARInvoiceType,
34    /// Invoice status.
35    pub status: SubledgerDocumentStatus,
36    /// Invoice lines.
37    pub lines: Vec<ARInvoiceLine>,
38    /// Net amount (before tax).
39    pub net_amount: CurrencyAmount,
40    /// Tax amount.
41    pub tax_amount: CurrencyAmount,
42    /// Gross amount (after tax).
43    pub gross_amount: CurrencyAmount,
44    /// Amount paid.
45    pub amount_paid: Decimal,
46    /// Amount remaining.
47    pub amount_remaining: Decimal,
48    /// Payment terms.
49    pub payment_terms: PaymentTerms,
50    /// Tax details.
51    pub tax_details: Vec<TaxInfo>,
52    /// GL reference.
53    pub gl_reference: Option<GLReference>,
54    /// Clearing information.
55    pub clearing_info: Vec<ClearingInfo>,
56    /// Dunning information.
57    pub dunning_info: DunningInfo,
58    /// Reference documents (sales order, delivery).
59    pub reference_documents: Vec<ARDocumentReference>,
60    /// Cost center.
61    pub cost_center: Option<String>,
62    /// Profit center.
63    pub profit_center: Option<String>,
64    /// Sales organization.
65    pub sales_org: Option<String>,
66    /// Distribution channel.
67    pub distribution_channel: Option<String>,
68    /// Division.
69    pub division: Option<String>,
70    /// Created timestamp.
71    pub created_at: DateTime<Utc>,
72    /// Created by user.
73    pub created_by: Option<String>,
74    /// Last modified timestamp.
75    pub modified_at: Option<DateTime<Utc>>,
76    /// Notes.
77    pub notes: Option<String>,
78}
79
80impl ARInvoice {
81    /// Creates a new AR invoice.
82    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    /// Adds an invoice line.
129    pub fn add_line(&mut self, line: ARInvoiceLine) {
130        self.lines.push(line);
131        self.recalculate_totals();
132    }
133
134    /// Recalculates totals from lines.
135    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    /// Applies a payment to the invoice.
150    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    /// Checks if invoice is overdue.
165    pub fn is_overdue(&self, as_of_date: NaiveDate) -> bool {
166        self.status == SubledgerDocumentStatus::Open && as_of_date > self.due_date
167    }
168
169    /// Calculates days overdue.
170    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    /// Gets discount amount if paid by discount date.
179    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    /// Sets the GL reference.
188    pub fn set_gl_reference(&mut self, reference: GLReference) {
189        self.gl_reference = Some(reference);
190    }
191
192    /// Adds a reference document.
193    pub fn add_reference(&mut self, reference: ARDocumentReference) {
194        self.reference_documents.push(reference);
195    }
196
197    /// Reverses the invoice.
198    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/// Type of AR invoice.
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
215pub enum ARInvoiceType {
216    /// Standard invoice.
217    #[default]
218    Standard,
219    /// Down payment request.
220    DownPaymentRequest,
221    /// Recurring invoice.
222    Recurring,
223    /// Credit invoice (negative).
224    CreditInvoice,
225    /// Debit invoice (adjustment).
226    DebitInvoice,
227    /// Pro forma invoice.
228    ProForma,
229    /// Intercompany invoice.
230    Intercompany,
231}
232
233/// AR invoice line item.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct ARInvoiceLine {
236    /// Line number.
237    pub line_number: u32,
238    /// Material/product ID.
239    pub material_id: Option<String>,
240    /// Description.
241    pub description: String,
242    /// Quantity.
243    pub quantity: Decimal,
244    /// Unit of measure.
245    pub unit: String,
246    /// Unit price.
247    pub unit_price: Decimal,
248    /// Net amount (quantity * unit_price).
249    pub net_amount: Decimal,
250    /// Tax code.
251    pub tax_code: Option<String>,
252    /// Tax rate.
253    pub tax_rate: Decimal,
254    /// Tax amount.
255    pub tax_amount: Decimal,
256    /// Gross amount.
257    pub gross_amount: Decimal,
258    /// Revenue account.
259    pub revenue_account: String,
260    /// Cost center.
261    pub cost_center: Option<String>,
262    /// Profit center.
263    pub profit_center: Option<String>,
264    /// Reference (sales order line).
265    pub reference: Option<String>,
266}
267
268impl ARInvoiceLine {
269    /// Creates a new invoice line.
270    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    /// Sets tax information.
299    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    /// Sets material ID.
308    pub fn with_material(mut self, material_id: String) -> Self {
309        self.material_id = Some(material_id);
310        self
311    }
312
313    /// Sets cost/profit center.
314    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/// Reference to related documents.
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct ARDocumentReference {
324    /// Reference document type.
325    pub document_type: ARReferenceDocType,
326    /// Document number.
327    pub document_number: String,
328    /// Document date.
329    pub document_date: NaiveDate,
330}
331
332/// Type of reference document.
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334pub enum ARReferenceDocType {
335    /// Sales order.
336    SalesOrder,
337    /// Delivery.
338    Delivery,
339    /// Contract.
340    Contract,
341    /// Quotation.
342    Quotation,
343    /// Return order.
344    ReturnOrder,
345}
346
347/// Summary of open AR items for a customer.
348#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct CustomerARSummary {
350    /// Customer ID.
351    pub customer_id: String,
352    /// Customer name.
353    pub customer_name: String,
354    /// Total open amount.
355    pub total_open: Decimal,
356    /// Total overdue amount.
357    pub total_overdue: Decimal,
358    /// Number of open invoices.
359    pub open_invoice_count: u32,
360    /// Number of overdue invoices.
361    pub overdue_invoice_count: u32,
362    /// Oldest open invoice date.
363    pub oldest_open_date: Option<NaiveDate>,
364    /// Credit limit.
365    pub credit_limit: Option<Decimal>,
366    /// Credit utilization percentage.
367    pub credit_utilization: Option<Decimal>,
368    /// Payment behavior score.
369    pub payment_score: Option<Decimal>,
370}
371
372impl CustomerARSummary {
373    /// Creates from a list of invoices.
374    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}