1use super::parse_helpers::parse_decimal as parse_decimal_with_context;
4use super::{
5 build_in_clause, map_db_error, params_refs, parse_datetime_opt_row, parse_datetime_row,
6 parse_decimal_opt_row, parse_decimal_row, parse_enum, parse_enum_row, parse_uuid_opt_row,
7 parse_uuid_row, sum_decimal_query, uuid_params, with_immediate_transaction,
8};
9use r2d2::Pool;
10use r2d2_sqlite::SqliteConnectionManager;
11use rusqlite::{Row, params};
12use rust_decimal::{Decimal, RoundingStrategy};
13use stateset_core::{
14 BatchResult, CommerceError, CreateInvoice, CreateInvoiceItem, CustomerId, Invoice,
15 InvoiceFilter, InvoiceId, InvoiceItem, InvoiceRepository, InvoiceStatus, OrderId, OrderItemId,
16 ProductId, RecordInvoicePayment, Result, UpdateInvoice, generate_invoice_number,
17 validate_batch_size,
18};
19use uuid::Uuid;
20
21fn money(amount: Decimal) -> Decimal {
30 amount.round_dp_with_strategy(2, RoundingStrategy::MidpointAwayFromZero)
31}
32
33#[derive(Debug)]
34pub struct SqliteInvoiceRepository {
35 pool: Pool<SqliteConnectionManager>,
36}
37
38impl SqliteInvoiceRepository {
39 #[must_use]
40 pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
41 Self { pool }
42 }
43
44 fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
45 self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))
46 }
47
48 fn row_to_invoice(row: &Row<'_>) -> rusqlite::Result<Invoice> {
49 Ok(Invoice {
50 id: InvoiceId::from(parse_uuid_row(&row.get::<_, String>("id")?, "invoice", "id")?),
51 invoice_number: row.get("invoice_number")?,
52 customer_id: CustomerId::from(parse_uuid_row(
53 &row.get::<_, String>("customer_id")?,
54 "invoice",
55 "customer_id",
56 )?),
57 order_id: parse_uuid_opt_row(
58 row.get::<_, Option<String>>("order_id")?,
59 "invoice",
60 "order_id",
61 )?
62 .map(OrderId::from),
63 status: parse_enum_row(&row.get::<_, String>("status")?, "invoice", "status")?,
64 invoice_type: parse_enum_row(
65 &row.get::<_, String>("invoice_type")?,
66 "invoice",
67 "invoice_type",
68 )?,
69 invoice_date: parse_datetime_row(
70 &row.get::<_, String>("invoice_date")?,
71 "invoice",
72 "invoice_date",
73 )?,
74 due_date: parse_datetime_row(
75 &row.get::<_, String>("due_date")?,
76 "invoice",
77 "due_date",
78 )?,
79 payment_terms: row.get("payment_terms")?,
80 currency: row.get("currency")?,
81 billing_name: row.get("billing_name")?,
82 billing_email: row.get("billing_email")?,
83 billing_address: row.get("billing_address")?,
84 billing_city: row.get("billing_city")?,
85 billing_state: row.get("billing_state")?,
86 billing_postal_code: row.get("billing_postal_code")?,
87 billing_country: row.get("billing_country")?,
88 subtotal: parse_decimal_row(&row.get::<_, String>("subtotal")?, "invoice", "subtotal")?,
89 discount_amount: parse_decimal_row(
90 &row.get::<_, String>("discount_amount")?,
91 "invoice",
92 "discount_amount",
93 )?,
94 discount_percent: parse_decimal_opt_row(
95 row.get::<_, Option<String>>("discount_percent")?,
96 "invoice",
97 "discount_percent",
98 )?,
99 tax_amount: parse_decimal_row(
100 &row.get::<_, String>("tax_amount")?,
101 "invoice",
102 "tax_amount",
103 )?,
104 tax_rate: parse_decimal_opt_row(
105 row.get::<_, Option<String>>("tax_rate")?,
106 "invoice",
107 "tax_rate",
108 )?,
109 shipping_amount: parse_decimal_row(
110 &row.get::<_, String>("shipping_amount")?,
111 "invoice",
112 "shipping_amount",
113 )?,
114 total: parse_decimal_row(&row.get::<_, String>("total")?, "invoice", "total")?,
115 amount_paid: parse_decimal_row(
116 &row.get::<_, String>("amount_paid")?,
117 "invoice",
118 "amount_paid",
119 )?,
120 balance_due: parse_decimal_row(
121 &row.get::<_, String>("balance_due")?,
122 "invoice",
123 "balance_due",
124 )?,
125 po_number: row.get("po_number")?,
126 notes: row.get("notes")?,
127 terms: row.get("terms")?,
128 footer: row.get("footer")?,
129 sent_at: parse_datetime_opt_row(
130 row.get::<_, Option<String>>("sent_at")?,
131 "invoice",
132 "sent_at",
133 )?,
134 viewed_at: parse_datetime_opt_row(
135 row.get::<_, Option<String>>("viewed_at")?,
136 "invoice",
137 "viewed_at",
138 )?,
139 paid_at: parse_datetime_opt_row(
140 row.get::<_, Option<String>>("paid_at")?,
141 "invoice",
142 "paid_at",
143 )?,
144 voided_at: parse_datetime_opt_row(
145 row.get::<_, Option<String>>("voided_at")?,
146 "invoice",
147 "voided_at",
148 )?,
149 items: Vec::new(),
150 created_at: parse_datetime_row(
151 &row.get::<_, String>("created_at")?,
152 "invoice",
153 "created_at",
154 )?,
155 updated_at: parse_datetime_row(
156 &row.get::<_, String>("updated_at")?,
157 "invoice",
158 "updated_at",
159 )?,
160 })
161 }
162
163 fn row_to_invoice_item(row: &Row<'_>) -> rusqlite::Result<InvoiceItem> {
164 Ok(InvoiceItem {
165 id: parse_uuid_row(&row.get::<_, String>("id")?, "invoice_item", "id")?,
166 invoice_id: InvoiceId::from(parse_uuid_row(
167 &row.get::<_, String>("invoice_id")?,
168 "invoice_item",
169 "invoice_id",
170 )?),
171 order_item_id: parse_uuid_opt_row(
172 row.get::<_, Option<String>>("order_item_id")?,
173 "invoice_item",
174 "order_item_id",
175 )?
176 .map(OrderItemId::from),
177 product_id: parse_uuid_opt_row(
178 row.get::<_, Option<String>>("product_id")?,
179 "invoice_item",
180 "product_id",
181 )?
182 .map(ProductId::from),
183 sku: row.get("sku")?,
184 description: row.get("description")?,
185 quantity: parse_decimal_row(
186 &row.get::<_, String>("quantity")?,
187 "invoice_item",
188 "quantity",
189 )?,
190 unit_of_measure: row.get("unit_of_measure")?,
191 unit_price: parse_decimal_row(
192 &row.get::<_, String>("unit_price")?,
193 "invoice_item",
194 "unit_price",
195 )?,
196 discount_amount: parse_decimal_row(
197 &row.get::<_, String>("discount_amount")?,
198 "invoice_item",
199 "discount_amount",
200 )?,
201 tax_amount: parse_decimal_row(
202 &row.get::<_, String>("tax_amount")?,
203 "invoice_item",
204 "tax_amount",
205 )?,
206 line_total: parse_decimal_row(
207 &row.get::<_, String>("line_total")?,
208 "invoice_item",
209 "line_total",
210 )?,
211 sort_order: row.get("sort_order")?,
212 created_at: parse_datetime_row(
213 &row.get::<_, String>("created_at")?,
214 "invoice_item",
215 "created_at",
216 )?,
217 updated_at: parse_datetime_row(
218 &row.get::<_, String>("updated_at")?,
219 "invoice_item",
220 "updated_at",
221 )?,
222 })
223 }
224
225 fn get_invoice_items_with_conn(
226 conn: &rusqlite::Connection,
227 invoice_id: InvoiceId,
228 ) -> Result<Vec<InvoiceItem>> {
229 let mut stmt = conn
230 .prepare("SELECT * FROM invoice_items WHERE invoice_id = ? ORDER BY sort_order")
231 .map_err(map_db_error)?;
232 let rows = stmt
233 .query_map([invoice_id.to_string()], Self::row_to_invoice_item)
234 .map_err(map_db_error)?;
235
236 let mut items = Vec::new();
237 for row in rows {
238 items.push(row.map_err(map_db_error)?);
239 }
240 Ok(items)
241 }
242
243 fn get_invoice_items_batch(
244 conn: &rusqlite::Connection,
245 ids: &[InvoiceId],
246 ) -> Result<std::collections::HashMap<String, Vec<InvoiceItem>>> {
247 let mut map: std::collections::HashMap<String, Vec<InvoiceItem>> =
248 std::collections::HashMap::with_capacity(ids.len());
249 let id_strs: Vec<String> = ids.iter().map(ToString::to_string).collect();
250 for chunk in id_strs.chunks(500) {
251 let placeholders = build_in_clause(chunk.len());
252 let sql = format!(
253 "SELECT * FROM invoice_items WHERE invoice_id IN ({placeholders}) ORDER BY sort_order"
254 );
255 let param_refs: Vec<&dyn rusqlite::ToSql> =
256 chunk.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
257 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
258 let rows = stmt
259 .query_map(param_refs.as_slice(), |row| {
260 let parent: String = row.get("invoice_id")?;
261 Ok((parent, Self::row_to_invoice_item(row)?))
262 })
263 .map_err(map_db_error)?;
264 for row in rows {
265 let (parent, item) = row.map_err(map_db_error)?;
266 map.entry(parent).or_default().push(item);
267 }
268 }
269 Ok(map)
270 }
271
272 fn get_invoice_with_conn(
273 conn: &rusqlite::Connection,
274 id: InvoiceId,
275 ) -> Result<Option<Invoice>> {
276 let result = conn.query_row(
277 "SELECT * FROM invoices WHERE id = ?",
278 [id.to_string()],
279 Self::row_to_invoice,
280 );
281
282 match result {
283 Ok(mut invoice) => {
284 invoice.items = Self::get_invoice_items_with_conn(conn, id)?;
285 Ok(Some(invoice))
286 }
287 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
288 Err(e) => Err(map_db_error(e)),
289 }
290 }
291
292 fn recalculate_with_conn(conn: &rusqlite::Connection, id: InvoiceId) -> Result<()> {
293 let invoice_id_param = id.to_string();
295 let invoice_params: [&dyn rusqlite::ToSql; 1] = [&invoice_id_param];
296 let subtotal = sum_decimal_query(
297 conn,
298 "SELECT line_total FROM invoice_items WHERE invoice_id = ?",
299 &invoice_params,
300 "invoice_item",
301 "line_total",
302 )?;
303
304 let (discount_amount, tax_amount, shipping_amount, amount_paid): (String, String, String, String) = conn
305 .query_row(
306 "SELECT discount_amount, tax_amount, shipping_amount, amount_paid FROM invoices WHERE id = ?",
307 [id.to_string()],
308 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
309 )
310 .map_err(map_db_error)?;
311
312 let subtotal = money(subtotal);
313 let total = money(
314 subtotal - parse_decimal_with_context(&discount_amount, "invoice", "discount_amount")?
315 + parse_decimal_with_context(&tax_amount, "invoice", "tax_amount")?
316 + parse_decimal_with_context(&shipping_amount, "invoice", "shipping_amount")?,
317 );
318 let balance_due =
319 money(total - parse_decimal_with_context(&amount_paid, "invoice", "amount_paid")?);
320
321 conn.execute(
322 "UPDATE invoices SET subtotal = ?, total = ?, balance_due = ?, updated_at = ? WHERE id = ?",
323 params![
324 subtotal.to_string(),
325 total.to_string(),
326 balance_due.to_string(),
327 chrono::Utc::now().to_rfc3339(),
328 id.to_string()
329 ],
330 )
331 .map_err(map_db_error)?;
332
333 Ok(())
334 }
335
336 fn get_invoice_items(&self, invoice_id: InvoiceId) -> Result<Vec<InvoiceItem>> {
337 let conn = self.conn()?;
338 Self::get_invoice_items_with_conn(&conn, invoice_id)
339 }
340}
341
342impl InvoiceRepository for SqliteInvoiceRepository {
343 fn create(&self, input: CreateInvoice) -> Result<Invoice> {
344 let mut conn = self.conn()?;
345 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
346 let id = InvoiceId::new();
347 let now = chrono::Utc::now();
348 let invoice_number = generate_invoice_number();
349 let invoice_date = input.invoice_date.unwrap_or(now);
350 let due_date = input.due_date.unwrap_or_else(|| {
351 invoice_date + chrono::Duration::days(i64::from(input.days_until_due.unwrap_or(30)))
352 });
353
354 tx.execute(
355 "INSERT INTO invoices (id, invoice_number, customer_id, order_id, status, invoice_type,
356 invoice_date, due_date, payment_terms, currency, billing_name, billing_email,
357 billing_address, billing_city, billing_state, billing_postal_code, billing_country,
358 subtotal, discount_amount, discount_percent, tax_amount, tax_rate, shipping_amount,
359 total, amount_paid, balance_due, po_number, notes, terms, footer, created_at, updated_at)
360 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
361 params![
362 id.to_string(),
363 invoice_number,
364 input.customer_id.to_string(),
365 input.order_id.map(|id| id.to_string()),
366 InvoiceStatus::Draft.to_string(),
367 input.invoice_type.unwrap_or_default().to_string(),
368 invoice_date.to_rfc3339(),
369 due_date.to_rfc3339(),
370 input.payment_terms,
371 input.currency.unwrap_or_default(),
372 input.billing_name,
373 input.billing_email,
374 input.billing_address,
375 input.billing_city,
376 input.billing_state,
377 input.billing_postal_code,
378 input.billing_country,
379 "0",
380 money(input.discount_amount.unwrap_or_default()).to_string(),
381 input.discount_percent.map(|d| d.to_string()),
382 money(input.tax_amount.unwrap_or_default()).to_string(),
383 input.tax_rate.map(|d| d.to_string()),
384 money(input.shipping_amount.unwrap_or_default()).to_string(),
385 "0",
386 "0",
387 "0",
388 input.po_number,
389 input.notes,
390 input.terms,
391 input.footer,
392 now.to_rfc3339(),
393 now.to_rfc3339(),
394 ],
395 ).map_err(map_db_error)?;
396
397 for (i, item) in input.items.into_iter().enumerate() {
399 let item_id = Uuid::new_v4();
400 let mut item_with_order = item;
401 if item_with_order.sort_order.is_none() {
402 item_with_order.sort_order = Some(i as i32);
403 }
404
405 let line_total = money(
406 item_with_order.quantity * item_with_order.unit_price
407 - item_with_order.discount_amount.unwrap_or_default()
408 + item_with_order.tax_amount.unwrap_or_default(),
409 );
410
411 tx.execute(
412 "INSERT INTO invoice_items (id, invoice_id, order_item_id, product_id, sku, description,
413 quantity, unit_of_measure, unit_price, discount_amount, tax_amount, line_total,
414 sort_order, created_at, updated_at)
415 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
416 params![
417 item_id.to_string(),
418 id.to_string(),
419 item_with_order.order_item_id.map(|id| id.to_string()),
420 item_with_order.product_id.map(|id| id.to_string()),
421 item_with_order.sku,
422 item_with_order.description,
423 item_with_order.quantity.to_string(),
424 item_with_order.unit_of_measure,
425 item_with_order.unit_price.to_string(),
426 money(item_with_order.discount_amount.unwrap_or_default()).to_string(),
427 money(item_with_order.tax_amount.unwrap_or_default()).to_string(),
428 line_total.to_string(),
429 item_with_order.sort_order.unwrap_or(0),
430 now.to_rfc3339(),
431 now.to_rfc3339(),
432 ],
433 )
434 .map_err(map_db_error)?;
435 }
436
437 Self::recalculate_with_conn(&tx, id)?;
439
440 tx.commit().map_err(map_db_error)?;
441
442 Self::get_invoice_with_conn(&conn, id)?.ok_or(CommerceError::NotFound)
443 }
444
445 fn get(&self, id: InvoiceId) -> Result<Option<Invoice>> {
446 let conn = self.conn()?;
447 Self::get_invoice_with_conn(&conn, id)
448 }
449
450 fn get_by_number(&self, invoice_number: &str) -> Result<Option<Invoice>> {
451 let conn = self.conn()?;
452 let result = conn.query_row(
453 "SELECT * FROM invoices WHERE invoice_number = ?",
454 [invoice_number],
455 Self::row_to_invoice,
456 );
457
458 match result {
459 Ok(mut invoice) => {
460 invoice.items = Self::get_invoice_items_with_conn(&conn, invoice.id)?;
461 Ok(Some(invoice))
462 }
463 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
464 Err(e) => Err(map_db_error(e)),
465 }
466 }
467
468 fn update(&self, id: InvoiceId, input: UpdateInvoice) -> Result<Invoice> {
469 let mut conn = self.conn()?;
470 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
471 let now = chrono::Utc::now();
472 let invoice = tx
473 .query_row(
474 "SELECT * FROM invoices WHERE id = ?",
475 [id.to_string()],
476 Self::row_to_invoice,
477 )
478 .map_err(map_db_error)?;
479
480 tx.execute(
481 "UPDATE invoices SET due_date = ?, payment_terms = ?, billing_name = ?, billing_email = ?,
482 billing_address = ?, billing_city = ?, billing_state = ?, billing_postal_code = ?,
483 billing_country = ?, discount_amount = ?, discount_percent = ?, tax_amount = ?,
484 tax_rate = ?, shipping_amount = ?, po_number = ?, notes = ?, terms = ?, footer = ?,
485 updated_at = ? WHERE id = ?",
486 params![
487 input.due_date.map_or_else(|| invoice.due_date.to_rfc3339(), |d| d.to_rfc3339()),
488 input.payment_terms.or(invoice.payment_terms),
489 input.billing_name.or(invoice.billing_name),
490 input.billing_email.or(invoice.billing_email),
491 input.billing_address.or(invoice.billing_address),
492 input.billing_city.or(invoice.billing_city),
493 input.billing_state.or(invoice.billing_state),
494 input.billing_postal_code.or(invoice.billing_postal_code),
495 input.billing_country.or(invoice.billing_country),
496 input.discount_amount.unwrap_or(invoice.discount_amount).to_string(),
497 input.discount_percent.map(|d| d.to_string()).or(invoice.discount_percent.map(|d| d.to_string())),
498 input.tax_amount.unwrap_or(invoice.tax_amount).to_string(),
499 input.tax_rate.map(|d| d.to_string()).or(invoice.tax_rate.map(|d| d.to_string())),
500 input.shipping_amount.unwrap_or(invoice.shipping_amount).to_string(),
501 input.po_number.or(invoice.po_number),
502 input.notes.or(invoice.notes),
503 input.terms.or(invoice.terms),
504 input.footer.or(invoice.footer),
505 now.to_rfc3339(),
506 id.to_string(),
507 ],
508 ).map_err(map_db_error)?;
509
510 Self::recalculate_with_conn(&tx, id)?;
511 tx.commit().map_err(map_db_error)?;
512
513 Self::get_invoice_with_conn(&conn, id)?.ok_or(CommerceError::NotFound)
514 }
515
516 fn list(&self, filter: InvoiceFilter) -> Result<Vec<Invoice>> {
517 let conn = self.conn()?;
518
519 let mut sql = "SELECT * FROM invoices WHERE 1=1".to_string();
520 let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
521
522 if let Some(customer_id) = &filter.customer_id {
523 sql.push_str(" AND customer_id = ?");
524 params_vec.push(Box::new(customer_id.to_string()));
525 }
526 if let Some(order_id) = &filter.order_id {
527 sql.push_str(" AND order_id = ?");
528 params_vec.push(Box::new(order_id.to_string()));
529 }
530 if let Some(status) = &filter.status {
531 sql.push_str(" AND status = ?");
532 params_vec.push(Box::new(status.to_string()));
533 }
534 if let Some(invoice_type) = &filter.invoice_type {
535 sql.push_str(" AND invoice_type = ?");
536 params_vec.push(Box::new(invoice_type.to_string()));
537 }
538 if filter.overdue_only.unwrap_or(false) {
539 sql.push_str(" AND due_date < datetime('now') AND status NOT IN ('paid', 'voided')");
540 }
541 if let Some(from_date) = &filter.from_date {
542 sql.push_str(" AND invoice_date >= ?");
543 params_vec.push(Box::new(from_date.to_rfc3339()));
544 }
545 if let Some(to_date) = &filter.to_date {
546 sql.push_str(" AND invoice_date <= ?");
547 params_vec.push(Box::new(to_date.to_rfc3339()));
548 }
549 if let Some(due_from) = &filter.due_from {
550 sql.push_str(" AND due_date >= ?");
551 params_vec.push(Box::new(due_from.to_rfc3339()));
552 }
553 if let Some(due_to) = &filter.due_to {
554 sql.push_str(" AND due_date <= ?");
555 params_vec.push(Box::new(due_to.to_rfc3339()));
556 }
557 if let Some(invoice_number) = &filter.invoice_number {
558 sql.push_str(" AND invoice_number LIKE ?");
561 params_vec.push(Box::new(format!("%{invoice_number}%")));
562 }
563
564 sql.push_str(" ORDER BY invoice_date DESC");
565
566 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
567 let params_refs: Vec<&dyn rusqlite::ToSql> =
568 params_vec.iter().map(std::convert::AsRef::as_ref).collect();
569 let rows =
570 stmt.query_map(params_refs.as_slice(), Self::row_to_invoice).map_err(map_db_error)?;
571
572 let mut matched: Vec<Invoice> = Vec::new();
577 for row in rows {
578 let invoice = row.map_err(map_db_error)?;
579 if let Some(min_total) = filter.min_total {
580 if invoice.total < min_total {
581 continue;
582 }
583 }
584 if let Some(max_total) = filter.max_total {
585 if invoice.total > max_total {
586 continue;
587 }
588 }
589 if let Some(min_balance) = filter.min_balance {
590 if invoice.balance_due < min_balance {
591 continue;
592 }
593 }
594 matched.push(invoice);
595 }
596
597 let offset = filter.offset.unwrap_or(0) as usize;
598 let mut page = if offset >= matched.len() { Vec::new() } else { matched.split_off(offset) };
599 if let Some(limit) = filter.limit {
600 page.truncate(limit as usize);
601 }
602
603 let ids: Vec<InvoiceId> = page.iter().map(|i| i.id).collect();
605 let mut items_by_id = Self::get_invoice_items_batch(&conn, &ids)?;
606 for invoice in &mut page {
607 invoice.items = items_by_id.remove(&invoice.id.to_string()).unwrap_or_default();
608 }
609 Ok(page)
610 }
611
612 fn for_customer(&self, customer_id: CustomerId) -> Result<Vec<Invoice>> {
613 self.list(InvoiceFilter { customer_id: Some(customer_id), ..Default::default() })
614 }
615
616 fn for_order(&self, order_id: OrderId) -> Result<Vec<Invoice>> {
617 self.list(InvoiceFilter { order_id: Some(order_id), ..Default::default() })
618 }
619
620 fn delete(&self, id: InvoiceId) -> Result<()> {
621 let mut conn = self.conn()?;
622 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
623
624 let status: String = tx
625 .query_row("SELECT status FROM invoices WHERE id = ?", [id.to_string()], |row| {
626 row.get(0)
627 })
628 .map_err(map_db_error)?;
629
630 if parse_enum::<InvoiceStatus>(&status, "invoice", "status")? != InvoiceStatus::Draft {
631 return Err(CommerceError::ValidationError(
632 "Can only delete draft invoices".to_string(),
633 ));
634 }
635
636 tx.execute("DELETE FROM invoice_items WHERE invoice_id = ?", [id.to_string()])
637 .map_err(map_db_error)?;
638 tx.execute("DELETE FROM invoices WHERE id = ?", [id.to_string()]).map_err(map_db_error)?;
639 tx.commit().map_err(map_db_error)?;
640 Ok(())
641 }
642
643 fn send(&self, id: InvoiceId) -> Result<Invoice> {
644 let conn = self.conn()?;
645 let now = chrono::Utc::now();
646
647 conn.execute(
648 "UPDATE invoices SET status = ?, sent_at = ?, updated_at = ? WHERE id = ?",
649 params![
650 InvoiceStatus::Sent.to_string(),
651 now.to_rfc3339(),
652 now.to_rfc3339(),
653 id.to_string()
654 ],
655 )
656 .map_err(map_db_error)?;
657
658 Self::get_invoice_with_conn(&conn, id)?.ok_or(CommerceError::NotFound)
659 }
660
661 fn mark_viewed(&self, id: InvoiceId) -> Result<Invoice> {
662 let conn = self.conn()?;
663 let now = chrono::Utc::now();
664
665 conn.execute(
666 "UPDATE invoices SET status = CASE WHEN status = 'sent' THEN 'viewed' ELSE status END,
667 viewed_at = COALESCE(viewed_at, ?), updated_at = ? WHERE id = ?",
668 params![now.to_rfc3339(), now.to_rfc3339(), id.to_string()],
669 )
670 .map_err(map_db_error)?;
671
672 Self::get_invoice_with_conn(&conn, id)?.ok_or(CommerceError::NotFound)
673 }
674
675 fn record_payment(&self, id: InvoiceId, payment: RecordInvoicePayment) -> Result<Invoice> {
676 if payment.amount <= Decimal::ZERO {
677 return Err(CommerceError::ValidationError(
678 "Invoice payment amount must be positive".to_string(),
679 ));
680 }
681
682 let now = chrono::Utc::now();
683 let id_str = id.to_string();
684
685 with_immediate_transaction(&self.pool, |tx| {
698 let (total, amount_paid): (String, String) = tx.query_row(
699 "SELECT total, amount_paid FROM invoices WHERE id = ?",
700 [&id_str],
701 |row| Ok((row.get(0)?, row.get(1)?)),
702 )?;
703
704 let total_dec = parse_decimal_row(&total, "invoice", "total")?;
705 let amount_paid_dec = parse_decimal_row(&amount_paid, "invoice", "amount_paid")?;
706
707 let new_amount_paid = money(amount_paid_dec + payment.amount);
708 let new_balance = money(total_dec - new_amount_paid);
709
710 let new_status = if new_balance <= Decimal::ZERO {
711 InvoiceStatus::Paid
712 } else {
713 InvoiceStatus::PartiallyPaid
714 };
715
716 let paid_at = if new_status == InvoiceStatus::Paid { Some(now) } else { None };
717
718 tx.execute(
719 "UPDATE invoices SET amount_paid = ?, balance_due = ?, status = ?,
720 paid_at = COALESCE(?, paid_at), updated_at = ? WHERE id = ?",
721 params![
722 new_amount_paid.to_string(),
723 new_balance.to_string(),
724 new_status.to_string(),
725 paid_at.map(|d| d.to_rfc3339()),
726 now.to_rfc3339(),
727 &id_str,
728 ],
729 )?;
730
731 Self::get_invoice_with_conn(tx, id)
732 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?
733 .ok_or_else(|| {
734 rusqlite::Error::ToSqlConversionFailure(Box::new(CommerceError::NotFound))
735 })
736 })
737 }
738
739 fn void(&self, id: InvoiceId) -> Result<Invoice> {
740 let conn = self.conn()?;
741 let now = chrono::Utc::now();
742
743 conn.execute(
744 "UPDATE invoices SET status = ?, voided_at = ?, updated_at = ? WHERE id = ?",
745 params![
746 InvoiceStatus::Voided.to_string(),
747 now.to_rfc3339(),
748 now.to_rfc3339(),
749 id.to_string()
750 ],
751 )
752 .map_err(map_db_error)?;
753
754 Self::get_invoice_with_conn(&conn, id)?.ok_or(CommerceError::NotFound)
755 }
756
757 fn write_off(&self, id: InvoiceId) -> Result<Invoice> {
758 let conn = self.conn()?;
759 let now = chrono::Utc::now();
760
761 conn.execute(
762 "UPDATE invoices SET status = ?, updated_at = ? WHERE id = ?",
763 params![InvoiceStatus::WrittenOff.to_string(), now.to_rfc3339(), id.to_string()],
764 )
765 .map_err(map_db_error)?;
766
767 Self::get_invoice_with_conn(&conn, id)?.ok_or(CommerceError::NotFound)
768 }
769
770 fn dispute(&self, id: InvoiceId) -> Result<Invoice> {
771 let conn = self.conn()?;
772 let now = chrono::Utc::now();
773
774 conn.execute(
775 "UPDATE invoices SET status = ?, updated_at = ? WHERE id = ?",
776 params![InvoiceStatus::Disputed.to_string(), now.to_rfc3339(), id.to_string()],
777 )
778 .map_err(map_db_error)?;
779
780 Self::get_invoice_with_conn(&conn, id)?.ok_or(CommerceError::NotFound)
781 }
782
783 fn add_item(&self, invoice_id: InvoiceId, item: CreateInvoiceItem) -> Result<InvoiceItem> {
784 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
785 let id = Uuid::new_v4();
786 let now = chrono::Utc::now();
787 let line_total = item.quantity * item.unit_price - item.discount_amount.unwrap_or_default()
788 + item.tax_amount.unwrap_or_default();
789
790 conn.execute(
791 "INSERT INTO invoice_items (id, invoice_id, order_item_id, product_id, sku, description,
792 quantity, unit_of_measure, unit_price, discount_amount, tax_amount, line_total,
793 sort_order, created_at, updated_at)
794 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
795 params![
796 id.to_string(),
797 invoice_id.to_string(),
798 item.order_item_id.map(|id| id.to_string()),
799 item.product_id.map(|id| id.to_string()),
800 item.sku,
801 item.description,
802 item.quantity.to_string(),
803 item.unit_of_measure,
804 item.unit_price.to_string(),
805 item.discount_amount.unwrap_or_default().to_string(),
806 item.tax_amount.unwrap_or_default().to_string(),
807 line_total.to_string(),
808 item.sort_order.unwrap_or(0),
809 now.to_rfc3339(),
810 now.to_rfc3339(),
811 ],
812 ).map_err(map_db_error)?;
813
814 let mut stmt =
815 conn.prepare("SELECT * FROM invoice_items WHERE id = ?").map_err(map_db_error)?;
816 stmt.query_row([id.to_string()], Self::row_to_invoice_item).map_err(map_db_error)
817 }
818
819 fn update_item(&self, item_id: Uuid, item: CreateInvoiceItem) -> Result<InvoiceItem> {
820 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
821 let now = chrono::Utc::now();
822 let line_total = item.quantity * item.unit_price - item.discount_amount.unwrap_or_default()
823 + item.tax_amount.unwrap_or_default();
824
825 conn.execute(
826 "UPDATE invoice_items SET sku = ?, description = ?, quantity = ?, unit_of_measure = ?,
827 unit_price = ?, discount_amount = ?, tax_amount = ?, line_total = ?, sort_order = ?,
828 updated_at = ? WHERE id = ?",
829 params![
830 item.sku,
831 item.description,
832 item.quantity.to_string(),
833 item.unit_of_measure,
834 item.unit_price.to_string(),
835 item.discount_amount.unwrap_or_default().to_string(),
836 item.tax_amount.unwrap_or_default().to_string(),
837 line_total.to_string(),
838 item.sort_order.unwrap_or(0),
839 now.to_rfc3339(),
840 item_id.to_string(),
841 ],
842 )
843 .map_err(map_db_error)?;
844
845 let mut stmt =
846 conn.prepare("SELECT * FROM invoice_items WHERE id = ?").map_err(map_db_error)?;
847 stmt.query_row([item_id.to_string()], Self::row_to_invoice_item).map_err(map_db_error)
848 }
849
850 fn remove_item(&self, item_id: Uuid) -> Result<()> {
851 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
852 conn.execute("DELETE FROM invoice_items WHERE id = ?", [item_id.to_string()])
853 .map_err(map_db_error)?;
854 Ok(())
855 }
856
857 fn get_items(&self, invoice_id: InvoiceId) -> Result<Vec<InvoiceItem>> {
858 self.get_invoice_items(invoice_id)
859 }
860
861 fn recalculate(&self, id: InvoiceId) -> Result<Invoice> {
862 let mut conn = self.conn()?;
863 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
864
865 Self::recalculate_with_conn(&tx, id)?;
866 tx.commit().map_err(map_db_error)?;
867
868 Self::get_invoice_with_conn(&conn, id)?.ok_or(CommerceError::NotFound)
869 }
870
871 fn get_overdue(&self) -> Result<Vec<Invoice>> {
872 self.list(InvoiceFilter { overdue_only: Some(true), ..Default::default() })
873 }
874
875 fn count(&self, filter: InvoiceFilter) -> Result<u64> {
876 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
877
878 let mut sql = "SELECT COUNT(*) FROM invoices WHERE 1=1".to_string();
879 let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
880
881 if let Some(customer_id) = &filter.customer_id {
882 sql.push_str(" AND customer_id = ?");
883 params_vec.push(Box::new(customer_id.to_string()));
884 }
885 if let Some(status) = &filter.status {
886 sql.push_str(" AND status = ?");
887 params_vec.push(Box::new(status.to_string()));
888 }
889 if filter.overdue_only.unwrap_or(false) {
890 sql.push_str(" AND due_date < datetime('now') AND status NOT IN ('paid', 'voided')");
891 }
892
893 let params_refs: Vec<&dyn rusqlite::ToSql> =
894 params_vec.iter().map(std::convert::AsRef::as_ref).collect();
895 let count: i64 =
896 conn.query_row(&sql, params_refs.as_slice(), |row| row.get(0)).map_err(map_db_error)?;
897 Ok(count as u64)
898 }
899
900 fn create_batch(&self, inputs: Vec<CreateInvoice>) -> Result<BatchResult<Invoice>> {
903 validate_batch_size(&inputs)?;
904 let mut result = BatchResult::with_capacity(inputs.len());
905
906 for (index, input) in inputs.into_iter().enumerate() {
907 match self.create(input) {
908 Ok(invoice) => result.record_success(invoice),
909 Err(e) => result.record_failure(index, None, &e),
910 }
911 }
912
913 Ok(result)
914 }
915
916 fn create_batch_atomic(&self, inputs: Vec<CreateInvoice>) -> Result<Vec<Invoice>> {
917 validate_batch_size(&inputs)?;
918 if inputs.is_empty() {
919 return Ok(vec![]);
920 }
921
922 let mut conn = self.conn()?;
923 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
924 let mut results = Vec::with_capacity(inputs.len());
925
926 for input in inputs {
927 let id = InvoiceId::new();
928 let now = chrono::Utc::now();
929 let invoice_number = generate_invoice_number();
930 let invoice_date = input.invoice_date.unwrap_or(now);
931 let due_date = input.due_date.unwrap_or_else(|| {
932 invoice_date + chrono::Duration::days(i64::from(input.days_until_due.unwrap_or(30)))
933 });
934
935 tx.execute(
936 "INSERT INTO invoices (id, invoice_number, customer_id, order_id, status, invoice_type,
937 invoice_date, due_date, payment_terms, currency, billing_name, billing_email,
938 billing_address, billing_city, billing_state, billing_postal_code, billing_country,
939 subtotal, discount_amount, discount_percent, tax_amount, tax_rate, shipping_amount,
940 total, amount_paid, balance_due, po_number, notes, terms, footer, created_at, updated_at)
941 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
942 params![
943 id.to_string(),
944 invoice_number.clone(),
945 input.customer_id.to_string(),
946 input.order_id.map(|id| id.to_string()),
947 InvoiceStatus::Draft.to_string(),
948 input.invoice_type.unwrap_or_default().to_string(),
949 invoice_date.to_rfc3339(),
950 due_date.to_rfc3339(),
951 input.payment_terms.clone(),
952 input.currency.unwrap_or_default(),
953 input.billing_name.clone(),
954 input.billing_email.clone(),
955 input.billing_address.clone(),
956 input.billing_city.clone(),
957 input.billing_state.clone(),
958 input.billing_postal_code.clone(),
959 input.billing_country.clone(),
960 "0",
961 input.discount_amount.unwrap_or_default().to_string(),
962 input.discount_percent.map(|d| d.to_string()),
963 input.tax_amount.unwrap_or_default().to_string(),
964 input.tax_rate.map(|d| d.to_string()),
965 input.shipping_amount.unwrap_or_default().to_string(),
966 "0",
967 "0",
968 "0",
969 input.po_number.clone(),
970 input.notes.clone(),
971 input.terms.clone(),
972 input.footer.clone(),
973 now.to_rfc3339(),
974 now.to_rfc3339(),
975 ],
976 ).map_err(map_db_error)?;
977
978 let mut items = Vec::with_capacity(input.items.len());
980 for (i, item) in input.items.into_iter().enumerate() {
981 let item_id = Uuid::new_v4();
982 let mut item_with_order = item;
983 if item_with_order.sort_order.is_none() {
984 item_with_order.sort_order = Some(i as i32);
985 }
986
987 let line_total = item_with_order.quantity * item_with_order.unit_price
988 - item_with_order.discount_amount.unwrap_or_default()
989 + item_with_order.tax_amount.unwrap_or_default();
990
991 tx.execute(
992 "INSERT INTO invoice_items (id, invoice_id, order_item_id, product_id, sku, description,
993 quantity, unit_of_measure, unit_price, discount_amount, tax_amount, line_total,
994 sort_order, created_at, updated_at)
995 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
996 params![
997 item_id.to_string(),
998 id.to_string(),
999 item_with_order.order_item_id.map(|id| id.to_string()),
1000 item_with_order.product_id.map(|id| id.to_string()),
1001 item_with_order.sku.clone(),
1002 item_with_order.description.clone(),
1003 item_with_order.quantity.to_string(),
1004 item_with_order.unit_of_measure.clone(),
1005 item_with_order.unit_price.to_string(),
1006 item_with_order.discount_amount.unwrap_or_default().to_string(),
1007 item_with_order.tax_amount.unwrap_or_default().to_string(),
1008 line_total.to_string(),
1009 item_with_order.sort_order.unwrap_or(0),
1010 now.to_rfc3339(),
1011 now.to_rfc3339(),
1012 ],
1013 )
1014 .map_err(map_db_error)?;
1015
1016 items.push(InvoiceItem {
1017 id: item_id,
1018 invoice_id: id,
1019 order_item_id: item_with_order.order_item_id,
1020 product_id: item_with_order.product_id,
1021 sku: item_with_order.sku,
1022 description: item_with_order.description,
1023 quantity: item_with_order.quantity,
1024 unit_of_measure: item_with_order.unit_of_measure,
1025 unit_price: item_with_order.unit_price,
1026 discount_amount: item_with_order.discount_amount.unwrap_or_default(),
1027 tax_amount: item_with_order.tax_amount.unwrap_or_default(),
1028 line_total,
1029 sort_order: item_with_order.sort_order.unwrap_or(0),
1030 created_at: now,
1031 updated_at: now,
1032 });
1033 }
1034
1035 Self::recalculate_with_conn(&tx, id)?;
1037
1038 let invoice = tx
1040 .query_row(
1041 "SELECT * FROM invoices WHERE id = ?",
1042 [id.to_string()],
1043 Self::row_to_invoice,
1044 )
1045 .map_err(map_db_error)?;
1046
1047 results.push(Invoice {
1048 id,
1049 invoice_number,
1050 customer_id: input.customer_id,
1051 order_id: input.order_id,
1052 status: InvoiceStatus::Draft,
1053 invoice_type: input.invoice_type.unwrap_or_default(),
1054 invoice_date,
1055 due_date,
1056 payment_terms: input.payment_terms,
1057 currency: input.currency.unwrap_or_default(),
1058 billing_name: input.billing_name,
1059 billing_email: input.billing_email,
1060 billing_address: input.billing_address,
1061 billing_city: input.billing_city,
1062 billing_state: input.billing_state,
1063 billing_postal_code: input.billing_postal_code,
1064 billing_country: input.billing_country,
1065 subtotal: invoice.subtotal,
1066 discount_amount: input.discount_amount.unwrap_or_default(),
1067 discount_percent: input.discount_percent,
1068 tax_amount: input.tax_amount.unwrap_or_default(),
1069 tax_rate: input.tax_rate,
1070 shipping_amount: input.shipping_amount.unwrap_or_default(),
1071 total: invoice.total,
1072 amount_paid: Decimal::ZERO,
1073 balance_due: invoice.balance_due,
1074 po_number: input.po_number,
1075 notes: input.notes,
1076 terms: input.terms,
1077 footer: input.footer,
1078 sent_at: None,
1079 viewed_at: None,
1080 paid_at: None,
1081 voided_at: None,
1082 items,
1083 created_at: now,
1084 updated_at: now,
1085 });
1086 }
1087
1088 tx.commit().map_err(map_db_error)?;
1089 Ok(results)
1090 }
1091
1092 fn update_batch(
1093 &self,
1094 updates: Vec<(InvoiceId, UpdateInvoice)>,
1095 ) -> Result<BatchResult<Invoice>> {
1096 validate_batch_size(&updates)?;
1097 let mut result = BatchResult::with_capacity(updates.len());
1098
1099 for (index, (id, input)) in updates.into_iter().enumerate() {
1100 match self.update(id, input) {
1101 Ok(invoice) => result.record_success(invoice),
1102 Err(e) => result.record_failure(index, Some(id.to_string()), &e),
1103 }
1104 }
1105
1106 Ok(result)
1107 }
1108
1109 fn update_batch_atomic(
1110 &self,
1111 updates: Vec<(InvoiceId, UpdateInvoice)>,
1112 ) -> Result<Vec<Invoice>> {
1113 validate_batch_size(&updates)?;
1114 if updates.is_empty() {
1115 return Ok(vec![]);
1116 }
1117
1118 let mut conn = self.conn()?;
1119 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1120 let mut results = Vec::with_capacity(updates.len());
1121
1122 for (id, input) in updates {
1123 let now = chrono::Utc::now();
1124 let invoice = tx
1125 .query_row(
1126 "SELECT * FROM invoices WHERE id = ?",
1127 [id.to_string()],
1128 Self::row_to_invoice,
1129 )
1130 .map_err(map_db_error)?;
1131
1132 tx.execute(
1133 "UPDATE invoices SET due_date = ?, payment_terms = ?, billing_name = ?, billing_email = ?,
1134 billing_address = ?, billing_city = ?, billing_state = ?, billing_postal_code = ?,
1135 billing_country = ?, discount_amount = ?, discount_percent = ?, tax_amount = ?,
1136 tax_rate = ?, shipping_amount = ?, po_number = ?, notes = ?, terms = ?, footer = ?,
1137 updated_at = ? WHERE id = ?",
1138 params![
1139 input.due_date.map_or_else(|| invoice.due_date.to_rfc3339(), |d| d.to_rfc3339()),
1140 input.payment_terms.or(invoice.payment_terms.clone()),
1141 input.billing_name.or(invoice.billing_name.clone()),
1142 input.billing_email.or(invoice.billing_email.clone()),
1143 input.billing_address.or(invoice.billing_address.clone()),
1144 input.billing_city.or(invoice.billing_city.clone()),
1145 input.billing_state.or(invoice.billing_state.clone()),
1146 input.billing_postal_code.or(invoice.billing_postal_code.clone()),
1147 input.billing_country.or(invoice.billing_country.clone()),
1148 input.discount_amount.unwrap_or(invoice.discount_amount).to_string(),
1149 input.discount_percent.map(|d| d.to_string()).or(invoice.discount_percent.map(|d| d.to_string())),
1150 input.tax_amount.unwrap_or(invoice.tax_amount).to_string(),
1151 input.tax_rate.map(|d| d.to_string()).or(invoice.tax_rate.map(|d| d.to_string())),
1152 input.shipping_amount.unwrap_or(invoice.shipping_amount).to_string(),
1153 input.po_number.or(invoice.po_number.clone()),
1154 input.notes.or(invoice.notes.clone()),
1155 input.terms.or(invoice.terms.clone()),
1156 input.footer.or(invoice.footer.clone()),
1157 now.to_rfc3339(),
1158 id.to_string(),
1159 ],
1160 ).map_err(map_db_error)?;
1161
1162 Self::recalculate_with_conn(&tx, id)?;
1163
1164 let updated_invoice = tx
1165 .query_row(
1166 "SELECT * FROM invoices WHERE id = ?",
1167 [id.to_string()],
1168 Self::row_to_invoice,
1169 )
1170 .map_err(map_db_error)?;
1171
1172 results.push(updated_invoice);
1173 }
1174
1175 tx.commit().map_err(map_db_error)?;
1176
1177 let conn = self.conn()?;
1179 let ids: Vec<InvoiceId> = results.iter().map(|i| i.id).collect();
1180 let mut items_by_id = Self::get_invoice_items_batch(&conn, &ids)?;
1181 for invoice in &mut results {
1182 invoice.items = items_by_id.remove(&invoice.id.to_string()).unwrap_or_default();
1183 }
1184
1185 Ok(results)
1186 }
1187
1188 fn delete_batch(&self, ids: Vec<InvoiceId>) -> Result<BatchResult<Uuid>> {
1189 validate_batch_size(&ids)?;
1190 let mut result = BatchResult::with_capacity(ids.len());
1191
1192 for (index, id) in ids.into_iter().enumerate() {
1193 match self.delete(id) {
1194 Ok(()) => result.record_success(id.into_uuid()),
1195 Err(e) => result.record_failure(index, Some(id.to_string()), &e),
1196 }
1197 }
1198
1199 Ok(result)
1200 }
1201
1202 fn delete_batch_atomic(&self, ids: Vec<InvoiceId>) -> Result<()> {
1203 validate_batch_size(&ids)?;
1204 if ids.is_empty() {
1205 return Ok(());
1206 }
1207
1208 let mut conn = self.conn()?;
1209 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1210
1211 let placeholders = build_in_clause(ids.len());
1213 let raw_ids: Vec<Uuid> = ids.iter().map(|id| id.into_uuid()).collect();
1214 let params = uuid_params(&raw_ids);
1215 let params_refs = params_refs(¶ms);
1216
1217 {
1218 let sql = format!("SELECT id, status FROM invoices WHERE id IN ({placeholders})");
1219 let mut stmt = tx.prepare(&sql).map_err(map_db_error)?;
1220 let rows = stmt
1221 .query_map(params_refs.as_slice(), |row| {
1222 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1223 })
1224 .map_err(map_db_error)?;
1225
1226 for row in rows {
1227 let (id_str, status) = row.map_err(map_db_error)?;
1228 if parse_enum::<InvoiceStatus>(&status, "invoice", "status")?
1229 != InvoiceStatus::Draft
1230 {
1231 return Err(CommerceError::ValidationError(format!(
1232 "Can only delete draft invoices. Invoice {id_str} has status {status}"
1233 )));
1234 }
1235 }
1236 }
1237
1238 let sql = format!("DELETE FROM invoice_items WHERE invoice_id IN ({placeholders})");
1240 tx.execute(&sql, params_refs.as_slice()).map_err(map_db_error)?;
1241
1242 let sql = format!("DELETE FROM invoices WHERE id IN ({placeholders})");
1244 tx.execute(&sql, params_refs.as_slice()).map_err(map_db_error)?;
1245
1246 tx.commit().map_err(map_db_error)?;
1247 Ok(())
1248 }
1249
1250 fn get_batch(&self, ids: Vec<InvoiceId>) -> Result<Vec<Invoice>> {
1251 validate_batch_size(&ids)?;
1252 if ids.is_empty() {
1253 return Ok(vec![]);
1254 }
1255
1256 let conn = self.conn()?;
1257 let placeholders = build_in_clause(ids.len());
1258 let sql = format!("SELECT * FROM invoices WHERE id IN ({placeholders})");
1259
1260 let raw_ids: Vec<Uuid> = ids.iter().map(|id| id.into_uuid()).collect();
1261 let params = uuid_params(&raw_ids);
1262 let params_refs = params_refs(¶ms);
1263
1264 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
1265 let invoices = stmt
1266 .query_map(params_refs.as_slice(), Self::row_to_invoice)
1267 .map_err(map_db_error)?
1268 .collect::<rusqlite::Result<Vec<_>>>()
1269 .map_err(map_db_error)?;
1270
1271 let ids: Vec<InvoiceId> = invoices.iter().map(|i| i.id).collect();
1273 let mut items_by_id = Self::get_invoice_items_batch(&conn, &ids)?;
1274 let mut result = vec![];
1275 for mut invoice in invoices {
1276 invoice.items = items_by_id.remove(&invoice.id.to_string()).unwrap_or_default();
1277 result.push(invoice);
1278 }
1279
1280 Ok(result)
1281 }
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286 use super::*;
1287 use crate::{DatabaseConfig, SqliteDatabase};
1288 use rust_decimal_macros::dec;
1289 use stateset_core::{
1290 CreateInvoice, CreateInvoiceItem, CustomerId, InvoiceFilter, InvoiceRepository,
1291 InvoiceStatus, RecordInvoicePayment,
1292 };
1293
1294 fn fresh_repo() -> SqliteInvoiceRepository {
1295 SqliteDatabase::in_memory().expect("in-memory").invoices()
1296 }
1297
1298 fn make_invoice(repo: &SqliteInvoiceRepository, customer: CustomerId) -> Invoice {
1299 repo.create(CreateInvoice {
1300 customer_id: customer,
1301 order_id: None,
1302 invoice_type: None,
1303 invoice_date: None,
1304 due_date: None,
1305 days_until_due: Some(30),
1306 payment_terms: Some("NET30".into()),
1307 currency: None,
1308 billing_name: Some("Ada Lovelace".into()),
1309 billing_email: Some("ada@example.com".into()),
1310 billing_address: None,
1311 billing_city: None,
1312 billing_state: None,
1313 billing_postal_code: None,
1314 billing_country: None,
1315 discount_amount: None,
1316 discount_percent: None,
1317 tax_amount: None,
1318 tax_rate: None,
1319 shipping_amount: None,
1320 po_number: None,
1321 notes: None,
1322 terms: None,
1323 footer: None,
1324 items: vec![CreateInvoiceItem {
1325 description: "Widget".into(),
1326 quantity: dec!(2),
1327 unit_price: dec!(50),
1328 ..Default::default()
1329 }],
1330 })
1331 .expect("create invoice")
1332 }
1333
1334 #[test]
1335 fn concurrent_payments_are_not_lost() {
1336 use std::sync::{Arc, Barrier};
1341 use std::thread;
1342
1343 let db = Arc::new(SqliteDatabase::new(&DatabaseConfig::in_memory()).unwrap());
1344 let repo = SqliteInvoiceRepository::new(db.pool().clone());
1345 let invoice = make_invoice(&repo, CustomerId::new()); assert_eq!(invoice.balance_due, dec!(100.00));
1347
1348 let thread_count = 10;
1349 let barrier = Arc::new(Barrier::new(thread_count));
1350 let mut handles = Vec::new();
1351 for _ in 0..thread_count {
1352 let db = Arc::clone(&db);
1353 let barrier = Arc::clone(&barrier);
1354 let invoice_id = invoice.id;
1355 handles.push(thread::spawn(move || {
1356 let repo = SqliteInvoiceRepository::new(db.pool().clone());
1357 barrier.wait();
1358 repo.record_payment(
1359 invoice_id,
1360 RecordInvoicePayment {
1361 amount: dec!(10.00),
1362 payment_id: None,
1363 payment_method: None,
1364 reference: None,
1365 notes: None,
1366 },
1367 )
1368 }));
1369 }
1370
1371 let results: Vec<_> = handles.into_iter().map(|h| h.join().expect("thread")).collect();
1372 let successes = results.iter().filter(|r| r.is_ok()).count();
1373 let _ = thread_count;
1376 assert!(
1377 results
1378 .iter()
1379 .all(|r| r.is_ok() || format!("{:?}", r.as_ref().unwrap_err()).contains("locked")),
1380 "every failure must be a transient lock: {results:?}"
1381 );
1382 assert!(successes >= 1, "at least one payment must succeed: {results:?}");
1383
1384 let fetched = repo.get(invoice.id).expect("get").expect("found");
1385 assert_eq!(
1392 fetched.amount_paid,
1393 dec!(10.00) * Decimal::from(successes as u64),
1394 "amount_paid must equal exactly the successful payments (Ok iff committed): {results:?}"
1395 );
1396 assert_eq!(fetched.balance_due, dec!(100.00) - fetched.amount_paid);
1397 let expected_status = if fetched.balance_due <= dec!(0) {
1398 InvoiceStatus::Paid
1399 } else {
1400 InvoiceStatus::PartiallyPaid
1401 };
1402 assert_eq!(fetched.status, expected_status);
1403 }
1404
1405 #[test]
1406 fn record_payment_rejects_nonpositive_amount() {
1407 let repo = fresh_repo();
1411 let invoice = make_invoice(&repo, CustomerId::new()); let pay = |amount| RecordInvoicePayment {
1414 amount,
1415 payment_id: None,
1416 payment_method: None,
1417 reference: None,
1418 notes: None,
1419 };
1420 assert!(repo.record_payment(invoice.id, pay(dec!(0))).is_err(), "zero must be rejected");
1421 assert!(
1422 repo.record_payment(invoice.id, pay(dec!(-10))).is_err(),
1423 "negative must be rejected"
1424 );
1425
1426 let fetched = repo.get(invoice.id).expect("get").expect("found");
1427 assert_eq!(fetched.amount_paid, dec!(0), "rejected payment must not touch amount_paid");
1428 assert_eq!(fetched.balance_due, dec!(100.00));
1429 }
1430
1431 #[test]
1432 fn create_invoice_starts_in_draft_with_items() {
1433 let repo = fresh_repo();
1434 let cust = CustomerId::new();
1435 let inv = make_invoice(&repo, cust);
1436 assert_eq!(inv.customer_id, cust);
1437 assert_eq!(inv.status, InvoiceStatus::Draft);
1438 assert!(!inv.invoice_number.is_empty());
1439
1440 let items = repo.get_items(inv.id).expect("items");
1441 assert_eq!(items.len(), 1);
1442 assert_eq!(items[0].quantity, dec!(2));
1443 }
1444
1445 #[test]
1446 fn get_and_get_by_number_round_trip() {
1447 let repo = fresh_repo();
1448 let inv = make_invoice(&repo, CustomerId::new());
1449 let by_id = repo.get(inv.id).expect("ok").expect("found");
1450 assert_eq!(by_id.id, inv.id);
1451 let by_num = repo.get_by_number(&inv.invoice_number).expect("ok").expect("found");
1452 assert_eq!(by_num.id, inv.id);
1453 assert!(repo.get_by_number("missing").expect("ok").is_none());
1454 }
1455
1456 #[test]
1457 fn send_transitions_to_sent() {
1458 let repo = fresh_repo();
1459 let inv = make_invoice(&repo, CustomerId::new());
1460 let sent = repo.send(inv.id).expect("send");
1461 assert_eq!(sent.status, InvoiceStatus::Sent);
1462 }
1463
1464 #[test]
1465 fn void_transitions_to_voided() {
1466 let repo = fresh_repo();
1467 let inv = make_invoice(&repo, CustomerId::new());
1468 let voided = repo.void(inv.id).expect("void");
1469 assert_eq!(voided.status, InvoiceStatus::Voided);
1470 }
1471
1472 #[test]
1473 fn list_filters_by_customer() {
1474 let repo = fresh_repo();
1475 let cust_a = CustomerId::new();
1476 let cust_b = CustomerId::new();
1477 make_invoice(&repo, cust_a);
1478 make_invoice(&repo, cust_a);
1479 make_invoice(&repo, cust_b);
1480
1481 let for_a = repo
1482 .list(InvoiceFilter { customer_id: Some(cust_a), ..Default::default() })
1483 .expect("list");
1484 assert_eq!(for_a.len(), 2);
1485 assert!(for_a.iter().all(|i| i.customer_id == cust_a));
1486 }
1487
1488 #[test]
1489 fn list_filters_by_status() {
1490 let repo = fresh_repo();
1491 let cust = CustomerId::new();
1492 let draft = make_invoice(&repo, cust);
1493 let to_send = make_invoice(&repo, cust);
1494 repo.send(to_send.id).expect("send");
1495
1496 let drafts = repo
1497 .list(InvoiceFilter { status: Some(InvoiceStatus::Draft), ..Default::default() })
1498 .expect("drafts");
1499 let sents = repo
1500 .list(InvoiceFilter { status: Some(InvoiceStatus::Sent), ..Default::default() })
1501 .expect("sents");
1502 assert!(drafts.iter().any(|i| i.id == draft.id));
1503 assert!(sents.iter().any(|i| i.id == to_send.id));
1504 }
1505
1506 #[test]
1507 fn get_overdue_empty_on_fresh_db() {
1508 let repo = fresh_repo();
1509 assert!(repo.get_overdue().expect("ok").is_empty());
1510 }
1511
1512 #[test]
1513 fn create_batch_returns_per_input_results() {
1514 let repo = fresh_repo();
1515 let cust = CustomerId::new();
1516 let mk = |desc: &str| CreateInvoice {
1517 customer_id: cust,
1518 days_until_due: Some(30),
1519 items: vec![CreateInvoiceItem {
1520 description: desc.into(),
1521 quantity: dec!(1),
1522 unit_price: dec!(10),
1523 ..Default::default()
1524 }],
1525 ..Default::default()
1526 };
1527 let result = repo.create_batch(vec![mk("A"), mk("B"), mk("C")]).expect("batch");
1528 assert_eq!(result.success_count, 3);
1529 assert_eq!(result.failure_count, 0);
1530 }
1531
1532 #[test]
1533 fn get_unknown_invoice_returns_none() {
1534 let repo = fresh_repo();
1535 assert!(repo.get(stateset_core::InvoiceId::new()).expect("ok").is_none());
1536 }
1537
1538 #[test]
1539 fn get_batch_returns_only_existing() {
1540 let repo = fresh_repo();
1541 let cust = CustomerId::new();
1542 let i1 = make_invoice(&repo, cust);
1543 let i2 = make_invoice(&repo, cust);
1544 let stranger = stateset_core::InvoiceId::new();
1545 let fetched = repo.get_batch(vec![i1.id, i2.id, stranger]).expect("ok");
1546 assert_eq!(fetched.len(), 2);
1547 }
1548}