Skip to main content

stateset_embedded/
accounts_receivable.rs

1//! Accounts Receivable operations
2//!
3//! Comprehensive accounts receivable management supporting:
4//! - AR aging reports and analysis
5//! - Collection activity tracking
6//! - Credit memos and applications
7//! - Write-offs
8//! - Payment applications
9//! - Customer statements
10//!
11//! # Example
12//!
13//! ```rust,ignore
14//! use stateset_embedded::Commerce;
15//!
16//! let commerce = Commerce::new("./store.db")?;
17//!
18//! // Get AR aging summary
19//! let aging = commerce.accounts_receivable().get_aging_summary()?;
20//! println!("Total outstanding: ${}", aging.total);
21//! println!("Current: ${}", aging.current);
22//! println!("30+ days: ${}", aging.days_1_30);
23//! # Ok::<(), stateset_embedded::CommerceError>(())
24//! ```
25
26use rust_decimal::Decimal;
27use stateset_core::{
28    ApplyCreditMemo, ApplyPaymentToInvoices, ArAgingFilter, ArAgingSummary, ArPaymentApplication,
29    CollectionActivity, CollectionActivityFilter, CollectionStatus, CreateCollectionActivity,
30    CreateCreditMemo, CreateWriteOff, CreditMemo, CreditMemoFilter, CustomerArAging,
31    CustomerArSummary, CustomerStatement, DunningLetterType, GenerateStatementRequest, Invoice,
32    Result, WriteOff, WriteOffFilter,
33};
34use stateset_db::Database;
35use std::sync::Arc;
36use uuid::Uuid;
37
38/// Accounts Receivable interface.
39pub struct AccountsReceivable {
40    db: Arc<dyn Database>,
41}
42
43impl std::fmt::Debug for AccountsReceivable {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        f.debug_struct("AccountsReceivable").finish_non_exhaustive()
46    }
47}
48
49impl AccountsReceivable {
50    pub(crate) fn new(db: Arc<dyn Database>) -> Self {
51        Self { db }
52    }
53
54    // ========================================================================
55    // Aging Reports
56    // ========================================================================
57
58    /// Get overall AR aging summary.
59    ///
60    /// # Example
61    ///
62    /// ```rust,ignore
63    /// use stateset_embedded::Commerce;
64    ///
65    /// let commerce = Commerce::new(":memory:")?;
66    ///
67    /// let aging = commerce.accounts_receivable().get_aging_summary()?;
68    /// println!("Current: ${}", aging.current);
69    /// println!("1-30 days: ${}", aging.days_1_30);
70    /// println!("31-60 days: ${}", aging.days_31_60);
71    /// println!("61-90 days: ${}", aging.days_61_90);
72    /// println!("90+ days: ${}", aging.days_over_90);
73    /// println!("Total: ${}", aging.total);
74    /// # Ok::<(), stateset_embedded::CommerceError>(())
75    /// ```
76    pub fn get_aging_summary(&self) -> Result<ArAgingSummary> {
77        self.db.accounts_receivable().get_aging_summary()
78    }
79
80    /// Get AR aging for a specific customer.
81    pub fn get_customer_aging(&self, customer_id: Uuid) -> Result<Option<CustomerArAging>> {
82        self.db.accounts_receivable().get_customer_aging(customer_id)
83    }
84
85    /// Get detailed AR aging report with filtering.
86    pub fn get_aging_report(&self, filter: ArAgingFilter) -> Result<Vec<CustomerArAging>> {
87        self.db.accounts_receivable().get_aging_report(filter)
88    }
89
90    // ========================================================================
91    // Collection Activities
92    // ========================================================================
93
94    /// Log a collection activity (call, email, dunning letter, etc.).
95    ///
96    /// # Example
97    ///
98    /// ```rust,ignore
99    /// use stateset_embedded::{Commerce, CreateCollectionActivity, CollectionActivityType};
100    /// use uuid::Uuid;
101    ///
102    /// let commerce = Commerce::new(":memory:")?;
103    ///
104    /// let activity = commerce.accounts_receivable().log_collection_activity(
105    ///     CreateCollectionActivity {
106    ///         invoice_id: Uuid::new_v4(),
107    ///         activity_type: CollectionActivityType::PhoneCall,
108    ///         notes: Some("Spoke with customer, promised to pay by Friday".into()),
109    ///         contact_method: Some("Phone".into()),
110    ///         contact_result: Some("Promise to pay".into()),
111    ///         performed_by: Some("John Collector".into()),
112    ///         ..Default::default()
113    ///     }
114    /// )?;
115    /// # Ok::<(), stateset_embedded::CommerceError>(())
116    /// ```
117    pub fn log_collection_activity(
118        &self,
119        input: CreateCollectionActivity,
120    ) -> Result<CollectionActivity> {
121        self.db.accounts_receivable().log_collection_activity(input)
122    }
123
124    /// List collection activities with filtering.
125    pub fn list_collection_activities(
126        &self,
127        filter: CollectionActivityFilter,
128    ) -> Result<Vec<CollectionActivity>> {
129        self.db.accounts_receivable().list_collection_activities(filter)
130    }
131
132    /// Update the collection status of an invoice.
133    pub fn update_collection_status(
134        &self,
135        invoice_id: Uuid,
136        status: CollectionStatus,
137    ) -> Result<()> {
138        self.db.accounts_receivable().update_collection_status(invoice_id.into(), status)
139    }
140
141    // ========================================================================
142    // Dunning
143    // ========================================================================
144
145    /// Get invoices that are due for dunning letters.
146    pub fn get_invoices_due_for_dunning(&self) -> Result<Vec<Invoice>> {
147        self.db.accounts_receivable().get_invoices_due_for_dunning()
148    }
149
150    /// Send a dunning letter and log the activity.
151    ///
152    /// # Example
153    ///
154    /// ```rust,ignore
155    /// use stateset_embedded::{Commerce, DunningLetterType};
156    /// use uuid::Uuid;
157    ///
158    /// let commerce = Commerce::new(":memory:")?;
159    ///
160    /// let activity = commerce.accounts_receivable().send_dunning_letter(
161    ///     Uuid::new_v4(), // invoice_id
162    ///     DunningLetterType::Reminder1,
163    ///     Some("ar_system"),
164    /// )?;
165    /// # Ok::<(), stateset_embedded::CommerceError>(())
166    /// ```
167    pub fn send_dunning_letter(
168        &self,
169        invoice_id: Uuid,
170        letter_type: DunningLetterType,
171        sent_by: Option<&str>,
172    ) -> Result<CollectionActivity> {
173        self.db.accounts_receivable().send_dunning_letter(invoice_id.into(), letter_type, sent_by)
174    }
175
176    // ========================================================================
177    // Write-offs
178    // ========================================================================
179
180    /// Create a write-off for an uncollectable invoice.
181    ///
182    /// # Example
183    ///
184    /// ```rust,ignore
185    /// use stateset_embedded::{Commerce, CreateWriteOff, WriteOffReason};
186    /// use rust_decimal_macros::dec;
187    /// use uuid::Uuid;
188    ///
189    /// let commerce = Commerce::new(":memory:")?;
190    ///
191    /// let write_off = commerce.accounts_receivable().create_write_off(CreateWriteOff {
192    ///     invoice_id: Uuid::new_v4(),
193    ///     amount: dec!(500.00),
194    ///     reason: WriteOffReason::Uncollectable,
195    ///     notes: Some("Customer bankruptcy".into()),
196    ///     approved_by: Some("CFO".into()),
197    /// })?;
198    /// # Ok::<(), stateset_embedded::CommerceError>(())
199    /// ```
200    pub fn create_write_off(&self, input: CreateWriteOff) -> Result<WriteOff> {
201        self.db.accounts_receivable().create_write_off(input)
202    }
203
204    /// Get a write-off by ID.
205    pub fn get_write_off(&self, id: Uuid) -> Result<Option<WriteOff>> {
206        self.db.accounts_receivable().get_write_off(id)
207    }
208
209    /// List write-offs with filtering.
210    pub fn list_write_offs(&self, filter: WriteOffFilter) -> Result<Vec<WriteOff>> {
211        self.db.accounts_receivable().list_write_offs(filter)
212    }
213
214    /// Reverse a write-off (restore invoice to collections).
215    pub fn reverse_write_off(&self, id: Uuid) -> Result<WriteOff> {
216        self.db.accounts_receivable().reverse_write_off(id)
217    }
218
219    // ========================================================================
220    // Credit Memos
221    // ========================================================================
222
223    /// Create a credit memo for a customer.
224    ///
225    /// # Example
226    ///
227    /// ```rust,ignore
228    /// use stateset_embedded::{Commerce, CreateCreditMemo, CreditMemoReason};
229    /// use rust_decimal_macros::dec;
230    /// use uuid::Uuid;
231    ///
232    /// let commerce = Commerce::new(":memory:")?;
233    ///
234    /// let memo = commerce.accounts_receivable().create_credit_memo(CreateCreditMemo {
235    ///     customer_id: Uuid::new_v4(),
236    ///     original_invoice_id: Some(Uuid::new_v4()),
237    ///     reason: CreditMemoReason::ReturnCredit,
238    ///     amount: dec!(150.00),
239    ///     notes: Some("Credit for returned merchandise".into()),
240    /// })?;
241    /// # Ok::<(), stateset_embedded::CommerceError>(())
242    /// ```
243    pub fn create_credit_memo(&self, input: CreateCreditMemo) -> Result<CreditMemo> {
244        self.db.accounts_receivable().create_credit_memo(input)
245    }
246
247    /// Get a credit memo by ID.
248    pub fn get_credit_memo(&self, id: Uuid) -> Result<Option<CreditMemo>> {
249        self.db.accounts_receivable().get_credit_memo(id)
250    }
251
252    /// Get a credit memo by number.
253    pub fn get_credit_memo_by_number(&self, number: &str) -> Result<Option<CreditMemo>> {
254        self.db.accounts_receivable().get_credit_memo_by_number(number)
255    }
256
257    /// List credit memos with filtering.
258    pub fn list_credit_memos(&self, filter: CreditMemoFilter) -> Result<Vec<CreditMemo>> {
259        self.db.accounts_receivable().list_credit_memos(filter)
260    }
261
262    /// Apply a credit memo to an invoice.
263    pub fn apply_credit_memo(&self, input: ApplyCreditMemo) -> Result<CreditMemo> {
264        self.db.accounts_receivable().apply_credit_memo(input)
265    }
266
267    /// Void a credit memo (only if not yet applied).
268    pub fn void_credit_memo(&self, id: Uuid) -> Result<CreditMemo> {
269        self.db.accounts_receivable().void_credit_memo(id)
270    }
271
272    /// Get unapplied credit memos for a customer.
273    pub fn get_unapplied_credits(&self, customer_id: Uuid) -> Result<Vec<CreditMemo>> {
274        self.db.accounts_receivable().get_unapplied_credits(customer_id)
275    }
276
277    // ========================================================================
278    // Payment Applications
279    // ========================================================================
280
281    /// Apply a payment to one or more invoices.
282    ///
283    /// # Example
284    ///
285    /// ```rust,ignore
286    /// use stateset_embedded::{Commerce, ApplyPaymentToInvoices, InvoicePaymentApplication};
287    /// use rust_decimal_macros::dec;
288    /// use uuid::Uuid;
289    ///
290    /// let commerce = Commerce::new(":memory:")?;
291    ///
292    /// let applications = commerce.accounts_receivable().apply_payment_to_invoices(
293    ///     ApplyPaymentToInvoices {
294    ///         payment_id: Uuid::new_v4(),
295    ///         applications: vec![
296    ///             InvoicePaymentApplication {
297    ///                 invoice_id: Uuid::new_v4(),
298    ///                 amount: dec!(500.00),
299    ///             },
300    ///             InvoicePaymentApplication {
301    ///                 invoice_id: Uuid::new_v4(),
302    ///                 amount: dec!(250.00),
303    ///             },
304    ///         ],
305    ///     }
306    /// )?;
307    /// # Ok::<(), stateset_embedded::CommerceError>(())
308    /// ```
309    pub fn apply_payment_to_invoices(
310        &self,
311        input: ApplyPaymentToInvoices,
312    ) -> Result<Vec<ArPaymentApplication>> {
313        self.db.accounts_receivable().apply_payment_to_invoices(input)
314    }
315
316    /// Get all payment applications for a payment.
317    pub fn get_payment_applications(&self, payment_id: Uuid) -> Result<Vec<ArPaymentApplication>> {
318        self.db.accounts_receivable().get_payment_applications(payment_id)
319    }
320
321    /// Unapply a payment application (remove application, restore invoice balance).
322    pub fn unapply_payment(&self, application_id: Uuid) -> Result<()> {
323        self.db.accounts_receivable().unapply_payment(application_id)
324    }
325
326    // ========================================================================
327    // Customer Summary & Statements
328    // ========================================================================
329
330    /// Get AR summary for a customer.
331    pub fn get_customer_summary(&self, customer_id: Uuid) -> Result<Option<CustomerArSummary>> {
332        self.db.accounts_receivable().get_customer_summary(customer_id)
333    }
334
335    /// Generate a customer statement.
336    ///
337    /// # Example
338    ///
339    /// ```rust,ignore
340    /// use stateset_embedded::{Commerce, GenerateStatementRequest};
341    /// use chrono::Utc;
342    /// use uuid::Uuid;
343    ///
344    /// let commerce = Commerce::new(":memory:")?;
345    ///
346    /// let statement = commerce.accounts_receivable().generate_statement(
347    ///     GenerateStatementRequest {
348    ///         customer_id: Uuid::new_v4(),
349    ///         period_start: None, // defaults to 30 days ago
350    ///         period_end: None,   // defaults to now
351    ///         include_paid_invoices: Some(false),
352    ///     }
353    /// )?;
354    ///
355    /// println!("Statement for: {}", statement.customer_name);
356    /// println!("Closing balance: ${}", statement.closing_balance);
357    /// # Ok::<(), stateset_embedded::CommerceError>(())
358    /// ```
359    pub fn generate_statement(
360        &self,
361        request: GenerateStatementRequest,
362    ) -> Result<CustomerStatement> {
363        self.db.accounts_receivable().generate_statement(request)
364    }
365
366    // ========================================================================
367    // Analytics
368    // ========================================================================
369
370    /// Get total outstanding receivables.
371    pub fn get_total_outstanding(&self) -> Result<Decimal> {
372        self.db.accounts_receivable().get_total_outstanding()
373    }
374
375    /// Calculate Days Sales Outstanding (DSO).
376    pub fn get_dso(&self, days: i32) -> Result<Decimal> {
377        self.db.accounts_receivable().get_dso(days)
378    }
379
380    /// Get average days to pay for a customer.
381    pub fn get_average_days_to_pay(&self, customer_id: Uuid) -> Result<Option<i32>> {
382        self.db.accounts_receivable().get_average_days_to_pay(customer_id)
383    }
384
385    /// Get AR summary for multiple customers.
386    pub fn get_customers_batch(&self, ids: Vec<Uuid>) -> Result<Vec<CustomerArSummary>> {
387        self.db.accounts_receivable().get_customers_batch(ids)
388    }
389}