Skip to main content

stateset_embedded/
invoices.rs

1//! Invoice operations for billing and accounts receivable
2//!
3//! # Example
4//!
5//! ```ignore
6//! use stateset_embedded::{Commerce, CreateInvoice, CreateInvoiceItem};
7//! use rust_decimal_macros::dec;
8//! use uuid::Uuid;
9//!
10//! let commerce = Commerce::new("./store.db")?;
11//!
12//! // Create an invoice
13//! let invoice = commerce.invoices().create(CreateInvoice {
14//!     customer_id: Uuid::new_v4().into(),
15//!     order_id: Some(Uuid::new_v4().into()),
16//!     billing_email: Some("customer@example.com".into()),
17//!     billing_name: Some("Alice Smith".into()),
18//!     billing_address: Some("123 Main St, City, ST 12345".into()),
19//!     items: vec![CreateInvoiceItem {
20//!         description: "Professional Services".into(),
21//!         quantity: dec!(10),
22//!         unit_price: dec!(150.00),
23//!         ..Default::default()
24//!     }],
25//!     ..Default::default()
26//! })?;
27//!
28//! // Send the invoice
29//! let invoice = commerce.invoices().send(invoice.id.into())?;
30//!
31//! // Record a payment
32//! commerce.invoices().record_payment(invoice.id.into(), stateset_embedded::RecordInvoicePayment {
33//!     amount: dec!(1500.00),
34//!     payment_method: Some("credit_card".into()),
35//!     reference: Some("PAY-12345".into()),
36//!     ..Default::default()
37//! })?;
38//! # Ok::<(), stateset_embedded::CommerceError>(())
39//! ```
40
41use crate::Database;
42use stateset_core::{
43    CreateInvoice, CreateInvoiceItem, Invoice, InvoiceFilter, InvoiceItem, RecordInvoicePayment,
44    Result, UpdateInvoice,
45};
46use std::sync::Arc;
47use uuid::Uuid;
48
49/// Invoice operations for billing and accounts receivable
50pub struct Invoices {
51    db: Arc<dyn Database>,
52}
53
54impl std::fmt::Debug for Invoices {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        f.debug_struct("Invoices").finish_non_exhaustive()
57    }
58}
59
60impl Invoices {
61    pub(crate) fn new(db: Arc<dyn Database>) -> Self {
62        Self { db }
63    }
64
65    /// Create a new invoice
66    ///
67    /// # Example
68    ///
69    /// ```rust,no_run
70    /// use stateset_embedded::{Commerce, CreateInvoice, CreateInvoiceItem, InvoiceType};
71    /// use rust_decimal_macros::dec;
72    /// use uuid::Uuid;
73    ///
74    /// let commerce = Commerce::new("./store.db")?;
75    ///
76    /// let invoice = commerce.invoices().create(CreateInvoice {
77    ///     customer_id: Uuid::new_v4().into(),
78    ///     invoice_type: Some(InvoiceType::Standard),
79    ///     billing_email: Some("billing@company.com".into()),
80    ///     billing_name: Some("Acme Corp".into()),
81    ///     items: vec![
82    ///         CreateInvoiceItem {
83    ///             description: "Consulting - November".into(),
84    ///             quantity: dec!(40),
85    ///             unit_price: dec!(200.00),
86    ///             ..Default::default()
87    ///         },
88    ///         CreateInvoiceItem {
89    ///             description: "Software License".into(),
90    ///             quantity: dec!(1),
91    ///             unit_price: dec!(500.00),
92    ///             ..Default::default()
93    ///         },
94    ///     ],
95    ///     notes: Some("Payment due within 30 days".into()),
96    ///     ..Default::default()
97    /// })?;
98    /// # Ok::<(), stateset_embedded::CommerceError>(())
99    /// ```
100    pub fn create(&self, input: CreateInvoice) -> Result<Invoice> {
101        self.db.invoices().create(input)
102    }
103
104    /// Get an invoice by ID
105    pub fn get(&self, id: Uuid) -> Result<Option<Invoice>> {
106        self.db.invoices().get(id.into())
107    }
108
109    /// Get an invoice by invoice number (e.g., "INV-20231215123456")
110    pub fn get_by_number(&self, invoice_number: &str) -> Result<Option<Invoice>> {
111        self.db.invoices().get_by_number(invoice_number)
112    }
113
114    /// Update an invoice
115    pub fn update(&self, id: Uuid, input: UpdateInvoice) -> Result<Invoice> {
116        self.db.invoices().update(id.into(), input)
117    }
118
119    /// List invoices with optional filtering
120    pub fn list(&self, filter: InvoiceFilter) -> Result<Vec<Invoice>> {
121        self.db.invoices().list(filter)
122    }
123
124    /// Get all invoices for a customer
125    pub fn for_customer(&self, customer_id: Uuid) -> Result<Vec<Invoice>> {
126        self.db.invoices().for_customer(customer_id.into())
127    }
128
129    /// Get all invoices for an order
130    pub fn for_order(&self, order_id: Uuid) -> Result<Vec<Invoice>> {
131        self.db.invoices().for_order(order_id.into())
132    }
133
134    // === Status Transitions ===
135
136    /// Send an invoice to the customer
137    pub fn send(&self, id: Uuid) -> Result<Invoice> {
138        self.db.invoices().send(id.into())
139    }
140
141    /// Mark invoice as viewed by customer
142    pub fn mark_viewed(&self, id: Uuid) -> Result<Invoice> {
143        self.db.invoices().mark_viewed(id.into())
144    }
145
146    /// Void an invoice
147    pub fn void(&self, id: Uuid) -> Result<Invoice> {
148        self.db.invoices().void(id.into())
149    }
150
151    /// Write off an uncollectible invoice
152    pub fn write_off(&self, id: Uuid) -> Result<Invoice> {
153        self.db.invoices().write_off(id.into())
154    }
155
156    /// Mark invoice as disputed
157    pub fn dispute(&self, id: Uuid) -> Result<Invoice> {
158        self.db.invoices().dispute(id.into())
159    }
160
161    // === Line Item Operations ===
162
163    /// Add an item to an invoice
164    pub fn add_item(&self, invoice_id: Uuid, item: CreateInvoiceItem) -> Result<InvoiceItem> {
165        self.db.invoices().add_item(invoice_id.into(), item)
166    }
167
168    /// Update a line item
169    pub fn update_item(&self, item_id: Uuid, input: CreateInvoiceItem) -> Result<InvoiceItem> {
170        self.db.invoices().update_item(item_id, input)
171    }
172
173    /// Remove an item from an invoice
174    pub fn remove_item(&self, item_id: Uuid) -> Result<()> {
175        self.db.invoices().remove_item(item_id)
176    }
177
178    /// Get items for an invoice
179    pub fn get_items(&self, invoice_id: Uuid) -> Result<Vec<InvoiceItem>> {
180        self.db.invoices().get_items(invoice_id.into())
181    }
182
183    // === Payment Operations ===
184
185    /// Record a payment against an invoice
186    ///
187    /// # Example
188    ///
189    /// ```rust,no_run
190    /// use stateset_embedded::{Commerce, RecordInvoicePayment};
191    /// use rust_decimal_macros::dec;
192    /// use uuid::Uuid;
193    ///
194    /// let commerce = Commerce::new("./store.db")?;
195    ///
196    /// // Record full payment
197    /// let invoice = commerce.invoices().record_payment(Uuid::new_v4(), RecordInvoicePayment {
198    ///     amount: dec!(1500.00),
199    ///     payment_method: Some("wire_transfer".into()),
200    ///     reference: Some("Wire ref: 123456789".into()),
201    ///     notes: Some("Paid in full".into()),
202    ///     ..Default::default()
203    /// })?;
204    ///
205    /// // Record partial payment
206    /// let invoice = commerce.invoices().record_payment(Uuid::new_v4(), RecordInvoicePayment {
207    ///     amount: dec!(500.00),
208    ///     payment_method: Some("check".into()),
209    ///     reference: Some("Check #1234".into()),
210    ///     ..Default::default()
211    /// })?;
212    /// # Ok::<(), stateset_embedded::CommerceError>(())
213    /// ```
214    pub fn record_payment(&self, id: Uuid, payment: RecordInvoicePayment) -> Result<Invoice> {
215        self.db.invoices().record_payment(id.into(), payment)
216    }
217
218    // === Queries ===
219
220    /// Get all overdue invoices
221    ///
222    /// Returns invoices that are past their due date and not fully paid.
223    pub fn get_overdue(&self) -> Result<Vec<Invoice>> {
224        self.db.invoices().get_overdue()
225    }
226
227    /// Recalculate invoice totals
228    ///
229    /// Use this after modifying line items to update subtotal, tax, and total.
230    pub fn recalculate(&self, id: Uuid) -> Result<Invoice> {
231        self.db.invoices().recalculate(id.into())
232    }
233
234    /// Count invoices matching a filter
235    pub fn count(&self, filter: InvoiceFilter) -> Result<u64> {
236        self.db.invoices().count(filter)
237    }
238
239    /// Get the total outstanding balance for a customer
240    pub fn customer_balance(&self, customer_id: Uuid) -> Result<rust_decimal::Decimal> {
241        let invoices = self.for_customer(customer_id)?;
242        let balance = invoices.iter().map(|inv| inv.total - inv.amount_paid).sum();
243        Ok(balance)
244    }
245}