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}