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}