1use chrono::Utc;
4use r2d2::Pool;
5use r2d2_sqlite::SqliteConnectionManager;
6use rust_decimal::Decimal;
7use stateset_core::{
8 AllocateBackorder, AllocationStatus, Backorder, BackorderAllocation, BackorderFilter,
9 BackorderFulfillment, BackorderRepository, BackorderStatus, BackorderSummary, CommerceError,
10 CreateBackorder, FulfillBackorder, Result, SkuBackorderSummary, UpdateBackorder,
11 generate_backorder_number,
12};
13use uuid::Uuid;
14
15use super::{
16 map_db_error, parse_datetime_opt, parse_datetime_opt_row, parse_datetime_row,
17 parse_decimal_row, parse_enum_row, parse_uuid_opt_row, parse_uuid_row, sum_decimal_query,
18 with_immediate_transaction,
19};
20
21fn row_to_backorder_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Backorder> {
22 Ok(Backorder {
23 id: parse_uuid_row(&row.get::<_, String>(0)?, "backorder", "id")?,
24 backorder_number: row.get(1)?,
25 order_id: parse_uuid_row(&row.get::<_, String>(2)?, "backorder", "order_id")?,
26 order_line_id: parse_uuid_opt_row(
27 row.get::<_, Option<String>>(3)?,
28 "backorder",
29 "order_line_id",
30 )?,
31 customer_id: parse_uuid_row(&row.get::<_, String>(4)?, "backorder", "customer_id")?,
32 sku: row.get(5)?,
33 quantity_ordered: parse_decimal_row(
34 &row.get::<_, String>(6)?,
35 "backorder",
36 "quantity_ordered",
37 )?,
38 quantity_fulfilled: parse_decimal_row(
39 &row.get::<_, String>(7)?,
40 "backorder",
41 "quantity_fulfilled",
42 )?,
43 quantity_remaining: parse_decimal_row(
44 &row.get::<_, String>(8)?,
45 "backorder",
46 "quantity_remaining",
47 )?,
48 status: parse_enum_row(&row.get::<_, String>(9)?, "backorder", "status")?,
49 priority: parse_enum_row(&row.get::<_, String>(10)?, "backorder", "priority")?,
50 expected_date: parse_datetime_opt_row(
51 row.get::<_, Option<String>>(11)?,
52 "backorder",
53 "expected_date",
54 )?,
55 promised_date: parse_datetime_opt_row(
56 row.get::<_, Option<String>>(12)?,
57 "backorder",
58 "promised_date",
59 )?,
60 source_location_id: row.get(13)?,
61 notes: row.get(14)?,
62 created_at: parse_datetime_row(&row.get::<_, String>(15)?, "backorder", "created_at")?,
63 updated_at: parse_datetime_row(&row.get::<_, String>(16)?, "backorder", "updated_at")?,
64 })
65}
66
67#[derive(Debug)]
68pub struct SqliteBackorderRepository {
69 pool: Pool<SqliteConnectionManager>,
70}
71
72impl SqliteBackorderRepository {
73 #[must_use]
74 pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
75 Self { pool }
76 }
77
78 fn row_to_backorder(&self, row: &rusqlite::Row<'_>) -> rusqlite::Result<Backorder> {
79 row_to_backorder_row(row)
80 }
81
82 fn row_to_fulfillment(
83 &self,
84 row: &rusqlite::Row<'_>,
85 ) -> rusqlite::Result<BackorderFulfillment> {
86 Ok(BackorderFulfillment {
87 id: parse_uuid_row(&row.get::<_, String>(0)?, "backorder_fulfillment", "id")?,
88 backorder_id: parse_uuid_row(
89 &row.get::<_, String>(1)?,
90 "backorder_fulfillment",
91 "backorder_id",
92 )?,
93 quantity: parse_decimal_row(
94 &row.get::<_, String>(2)?,
95 "backorder_fulfillment",
96 "quantity",
97 )?,
98 source_type: parse_enum_row(
99 &row.get::<_, String>(3)?,
100 "backorder_fulfillment",
101 "source_type",
102 )?,
103 source_id: parse_uuid_opt_row(
104 row.get::<_, Option<String>>(4)?,
105 "backorder_fulfillment",
106 "source_id",
107 )?,
108 notes: row.get(5)?,
109 fulfilled_at: parse_datetime_row(
110 &row.get::<_, String>(6)?,
111 "backorder_fulfillment",
112 "fulfilled_at",
113 )?,
114 fulfilled_by: row.get(7)?,
115 })
116 }
117
118 fn row_to_allocation(&self, row: &rusqlite::Row<'_>) -> rusqlite::Result<BackorderAllocation> {
119 Ok(BackorderAllocation {
120 id: parse_uuid_row(&row.get::<_, String>(0)?, "backorder_allocation", "id")?,
121 backorder_id: parse_uuid_row(
122 &row.get::<_, String>(1)?,
123 "backorder_allocation",
124 "backorder_id",
125 )?,
126 sku: row.get(2)?,
127 quantity: parse_decimal_row(
128 &row.get::<_, String>(3)?,
129 "backorder_allocation",
130 "quantity",
131 )?,
132 location_id: row.get(4)?,
133 lot_id: parse_uuid_opt_row(
134 row.get::<_, Option<String>>(5)?,
135 "backorder_allocation",
136 "lot_id",
137 )?,
138 status: parse_enum_row(&row.get::<_, String>(6)?, "backorder_allocation", "status")?,
139 allocated_at: parse_datetime_row(
140 &row.get::<_, String>(7)?,
141 "backorder_allocation",
142 "allocated_at",
143 )?,
144 expires_at: parse_datetime_opt_row(
145 row.get::<_, Option<String>>(8)?,
146 "backorder_allocation",
147 "expires_at",
148 )?,
149 })
150 }
151}
152
153pub(crate) fn create_backorder_in_tx(
154 tx: &rusqlite::Transaction<'_>,
155 input: &CreateBackorder,
156) -> std::result::Result<Backorder, rusqlite::Error> {
157 let id = Uuid::new_v4();
158 let now = Utc::now();
159 let backorder_number = generate_backorder_number();
160 let priority = input.priority.unwrap_or_default();
161
162 tx.execute(
163 "INSERT INTO backorders (id, backorder_number, order_id, order_line_id, customer_id,
164 sku, quantity_ordered, quantity_fulfilled, quantity_remaining, status, priority,
165 expected_date, promised_date, source_location_id, notes, created_at, updated_at)
166 VALUES (?, ?, ?, ?, ?, ?, ?, '0', ?, 'pending', ?, ?, ?, ?, ?, ?, ?)",
167 rusqlite::params![
168 id.to_string(),
169 &backorder_number,
170 input.order_id.to_string(),
171 input.order_line_id.map(|id| id.to_string()),
172 input.customer_id.to_string(),
173 &input.sku,
174 input.quantity.to_string(),
175 input.quantity.to_string(),
176 priority.to_string(),
177 input.expected_date.map(|d| d.to_rfc3339()),
178 input.promised_date.map(|d| d.to_rfc3339()),
179 input.source_location_id,
180 input.notes,
181 now.to_rfc3339(),
182 now.to_rfc3339(),
183 ],
184 )?;
185
186 let row = tx.query_row(
187 "SELECT id, backorder_number, order_id, order_line_id, customer_id, sku,
188 quantity_ordered, quantity_fulfilled, quantity_remaining, status, priority,
189 expected_date, promised_date, source_location_id, notes, created_at, updated_at
190 FROM backorders WHERE id = ?",
191 [id.to_string()],
192 row_to_backorder_row,
193 );
194
195 match row {
196 Ok(bo) => Ok(bo),
197 Err(rusqlite::Error::QueryReturnedNoRows) => {
198 Err(rusqlite::Error::ToSqlConversionFailure(Box::new(CommerceError::NotFound)))
199 }
200 Err(e) => Err(e),
201 }
202}
203
204pub(crate) fn cancel_backorders_for_order_in_tx(
205 tx: &rusqlite::Transaction<'_>,
206 order_id: Uuid,
207) -> std::result::Result<(), rusqlite::Error> {
208 let now = Utc::now();
209
210 tx.execute(
211 "UPDATE backorders SET status = 'cancelled', updated_at = ? WHERE order_id = ?",
212 [now.to_rfc3339(), order_id.to_string()],
213 )?;
214
215 tx.execute(
216 "UPDATE backorder_allocations SET status = 'released'
217 WHERE backorder_id IN (SELECT id FROM backorders WHERE order_id = ?)
218 AND status = 'reserved'",
219 [order_id.to_string()],
220 )?;
221
222 Ok(())
223}
224
225impl BackorderRepository for SqliteBackorderRepository {
226 fn create_backorder(&self, input: CreateBackorder) -> Result<Backorder> {
227 let id = Uuid::new_v4();
228 let now = Utc::now();
229 let backorder_number = generate_backorder_number();
230 let priority = input.priority.unwrap_or_default();
231
232 {
233 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
234 conn.execute(
235 "INSERT INTO backorders (id, backorder_number, order_id, order_line_id, customer_id,
236 sku, quantity_ordered, quantity_fulfilled, quantity_remaining, status, priority,
237 expected_date, promised_date, source_location_id, notes, created_at, updated_at)
238 VALUES (?, ?, ?, ?, ?, ?, ?, '0', ?, 'pending', ?, ?, ?, ?, ?, ?, ?)",
239 rusqlite::params![
240 id.to_string(),
241 &backorder_number,
242 input.order_id.to_string(),
243 input.order_line_id.map(|id| id.to_string()),
244 input.customer_id.to_string(),
245 &input.sku,
246 input.quantity.to_string(),
247 input.quantity.to_string(), priority.to_string(),
249 input.expected_date.map(|d| d.to_rfc3339()),
250 input.promised_date.map(|d| d.to_rfc3339()),
251 input.source_location_id,
252 input.notes,
253 now.to_rfc3339(),
254 now.to_rfc3339(),
255 ],
256 ).map_err(map_db_error)?;
257 }
258
259 self.get_backorder(id)?.ok_or(CommerceError::NotFound)
260 }
261
262 fn get_backorder(&self, id: Uuid) -> Result<Option<Backorder>> {
263 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
264 let result = conn.query_row(
265 "SELECT id, backorder_number, order_id, order_line_id, customer_id, sku,
266 quantity_ordered, quantity_fulfilled, quantity_remaining, status, priority,
267 expected_date, promised_date, source_location_id, notes, created_at, updated_at
268 FROM backorders WHERE id = ?",
269 [id.to_string()],
270 |row| self.row_to_backorder(row),
271 );
272
273 match result {
274 Ok(bo) => Ok(Some(bo)),
275 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
276 Err(e) => Err(map_db_error(e)),
277 }
278 }
279
280 fn get_backorder_by_number(&self, number: &str) -> Result<Option<Backorder>> {
281 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
282 let result = conn.query_row(
283 "SELECT id, backorder_number, order_id, order_line_id, customer_id, sku,
284 quantity_ordered, quantity_fulfilled, quantity_remaining, status, priority,
285 expected_date, promised_date, source_location_id, notes, created_at, updated_at
286 FROM backorders WHERE backorder_number = ?",
287 [number],
288 |row| self.row_to_backorder(row),
289 );
290
291 match result {
292 Ok(bo) => Ok(Some(bo)),
293 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
294 Err(e) => Err(map_db_error(e)),
295 }
296 }
297
298 fn update_backorder(&self, id: Uuid, input: UpdateBackorder) -> Result<Backorder> {
299 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
300 let now = Utc::now();
301
302 conn.execute(
303 "UPDATE backorders SET
304 priority = COALESCE(?, priority),
305 expected_date = COALESCE(?, expected_date),
306 promised_date = COALESCE(?, promised_date),
307 source_location_id = COALESCE(?, source_location_id),
308 notes = COALESCE(?, notes),
309 updated_at = ?
310 WHERE id = ?",
311 rusqlite::params![
312 input.priority.map(|p| p.to_string()),
313 input.expected_date.map(|d| d.to_rfc3339()),
314 input.promised_date.map(|d| d.to_rfc3339()),
315 input.source_location_id,
316 input.notes,
317 now.to_rfc3339(),
318 id.to_string(),
319 ],
320 )
321 .map_err(map_db_error)?;
322
323 self.get_backorder(id)?.ok_or(CommerceError::NotFound)
324 }
325
326 fn list_backorders(&self, filter: BackorderFilter) -> Result<Vec<Backorder>> {
327 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
328 let mut sql = String::from(
329 "SELECT id, backorder_number, order_id, order_line_id, customer_id, sku,
330 quantity_ordered, quantity_fulfilled, quantity_remaining, status, priority,
331 expected_date, promised_date, source_location_id, notes, created_at, updated_at
332 FROM backorders WHERE 1=1",
333 );
334 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
335
336 if let Some(ref order_id) = filter.order_id {
337 sql.push_str(" AND order_id = ?");
338 params.push(Box::new(order_id.to_string()));
339 }
340 if let Some(ref customer_id) = filter.customer_id {
341 sql.push_str(" AND customer_id = ?");
342 params.push(Box::new(customer_id.to_string()));
343 }
344 if let Some(ref sku) = filter.sku {
345 sql.push_str(" AND sku = ?");
346 params.push(Box::new(sku.clone()));
347 }
348 if let Some(ref status) = filter.status {
349 sql.push_str(" AND status = ?");
350 params.push(Box::new(status.to_string()));
351 }
352 if let Some(ref priority) = filter.priority {
353 sql.push_str(" AND priority = ?");
354 params.push(Box::new(priority.to_string()));
355 }
356
357 sql.push_str(" ORDER BY CASE priority WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'normal' THEN 3 ELSE 4 END, created_at ASC");
359
360 if let Some(limit) = filter.limit {
361 sql.push_str(&format!(" LIMIT {limit}"));
362 }
363
364 let param_refs: Vec<&dyn rusqlite::ToSql> =
365 params.iter().map(std::convert::AsRef::as_ref).collect();
366 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
367 let rows = stmt
368 .query_map(param_refs.as_slice(), |row| self.row_to_backorder(row))
369 .map_err(map_db_error)?;
370
371 let mut backorders = Vec::new();
372 for row in rows {
373 backorders.push(row.map_err(map_db_error)?);
374 }
375 Ok(backorders)
376 }
377
378 fn cancel_backorder(&self, id: Uuid) -> Result<Backorder> {
379 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
380 let now = Utc::now();
381
382 conn.execute(
383 "UPDATE backorders SET status = 'cancelled', updated_at = ? WHERE id = ?",
384 [now.to_rfc3339(), id.to_string()],
385 )
386 .map_err(map_db_error)?;
387
388 conn.execute(
390 "UPDATE backorder_allocations SET status = 'released' WHERE backorder_id = ? AND status = 'reserved'",
391 [id.to_string()],
392 ).map_err(map_db_error)?;
393
394 self.get_backorder(id)?.ok_or(CommerceError::NotFound)
395 }
396
397 fn get_backorders_for_order(&self, order_id: Uuid) -> Result<Vec<Backorder>> {
398 self.list_backorders(BackorderFilter { order_id: Some(order_id), ..Default::default() })
399 }
400
401 fn get_backorders_for_customer(&self, customer_id: Uuid) -> Result<Vec<Backorder>> {
402 self.list_backorders(BackorderFilter {
403 customer_id: Some(customer_id),
404 ..Default::default()
405 })
406 }
407
408 fn get_backorders_for_sku(&self, sku: &str) -> Result<Vec<Backorder>> {
409 self.list_backorders(BackorderFilter { sku: Some(sku.to_string()), ..Default::default() })
410 }
411
412 fn fulfill_backorder(&self, input: FulfillBackorder) -> Result<Backorder> {
413 if input.quantity <= Decimal::ZERO {
414 return Err(CommerceError::ValidationError(
415 "Fulfillment quantity must be greater than zero".to_string(),
416 ));
417 }
418
419 let now = Utc::now();
420 let fulfillment_id = Uuid::new_v4();
421 let backorder_id_str = input.backorder_id.to_string();
422
423 with_immediate_transaction(&self.pool, |tx| {
428 let (status_str, remaining_str, fulfilled_str): (String, String, String) = tx
429 .query_row(
430 "SELECT status, quantity_remaining, quantity_fulfilled FROM backorders WHERE id = ?",
431 [&backorder_id_str],
432 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
433 )
434 .map_err(|e| match e {
435 rusqlite::Error::QueryReturnedNoRows => {
436 rusqlite::Error::ToSqlConversionFailure(Box::new(CommerceError::NotFound))
437 }
438 other => other,
439 })?;
440 let status: BackorderStatus = parse_enum_row(&status_str, "backorder", "status")?;
441 let remaining = parse_decimal_row(&remaining_str, "backorder", "quantity_remaining")?;
442 let fulfilled = parse_decimal_row(&fulfilled_str, "backorder", "quantity_fulfilled")?;
443
444 if matches!(status, BackorderStatus::Cancelled | BackorderStatus::Fulfilled) {
446 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
447 CommerceError::ValidationError("Backorder cannot be fulfilled".to_string()),
448 )));
449 }
450 if input.quantity > remaining {
452 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
453 CommerceError::ValidationError(format!(
454 "Cannot fulfill {} - only {} remaining",
455 input.quantity, remaining
456 )),
457 )));
458 }
459
460 let new_fulfilled = fulfilled + input.quantity;
461 let new_remaining = remaining - input.quantity;
462 let new_status = if new_remaining <= Decimal::ZERO {
463 BackorderStatus::Fulfilled
464 } else {
465 BackorderStatus::PartiallyFulfilled
466 };
467
468 tx.execute(
469 "INSERT INTO backorder_fulfillments (id, backorder_id, quantity, source_type,
470 source_id, notes, fulfilled_at, fulfilled_by)
471 VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
472 rusqlite::params![
473 fulfillment_id.to_string(),
474 backorder_id_str,
475 input.quantity.to_string(),
476 input.source_type.to_string(),
477 input.source_id.map(|id| id.to_string()),
478 input.notes.as_deref(),
479 now.to_rfc3339(),
480 input.fulfilled_by.as_deref(),
481 ],
482 )?;
483
484 tx.execute(
485 "UPDATE backorders SET quantity_fulfilled = ?, quantity_remaining = ?, status = ?, updated_at = ?
486 WHERE id = ?",
487 rusqlite::params![
488 new_fulfilled.to_string(),
489 new_remaining.to_string(),
490 new_status.to_string(),
491 now.to_rfc3339(),
492 backorder_id_str,
493 ],
494 )?;
495 Ok(())
496 })?;
497
498 self.get_backorder(input.backorder_id)?.ok_or(CommerceError::NotFound)
499 }
500
501 fn get_fulfillment_history(&self, backorder_id: Uuid) -> Result<Vec<BackorderFulfillment>> {
502 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
503 let mut stmt = conn.prepare(
504 "SELECT id, backorder_id, quantity, source_type, source_id, notes, fulfilled_at, fulfilled_by
505 FROM backorder_fulfillments WHERE backorder_id = ? ORDER BY fulfilled_at"
506 ).map_err(map_db_error)?;
507
508 let rows = stmt
509 .query_map([backorder_id.to_string()], |row| self.row_to_fulfillment(row))
510 .map_err(map_db_error)?;
511
512 let mut fulfillments = Vec::new();
513 for row in rows {
514 fulfillments.push(row.map_err(map_db_error)?);
515 }
516 Ok(fulfillments)
517 }
518
519 fn allocate_backorder(&self, input: AllocateBackorder) -> Result<BackorderAllocation> {
520 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
521 let id = Uuid::new_v4();
522 let now = Utc::now();
523
524 let backorder = self.get_backorder(input.backorder_id)?.ok_or(CommerceError::NotFound)?;
525
526 conn.execute(
527 "INSERT INTO backorder_allocations (id, backorder_id, sku, quantity, location_id,
528 lot_id, status, allocated_at, expires_at)
529 VALUES (?, ?, ?, ?, ?, ?, 'reserved', ?, ?)",
530 rusqlite::params![
531 id.to_string(),
532 input.backorder_id.to_string(),
533 &backorder.sku,
534 input.quantity.to_string(),
535 input.location_id,
536 input.lot_id.map(|id| id.to_string()),
537 now.to_rfc3339(),
538 input.expires_at.map(|d| d.to_rfc3339()),
539 ],
540 )
541 .map_err(map_db_error)?;
542
543 conn.execute(
545 "UPDATE backorders SET status = 'allocated', updated_at = ?
546 WHERE id = ? AND status = 'pending'",
547 [now.to_rfc3339(), input.backorder_id.to_string()],
548 )
549 .map_err(map_db_error)?;
550
551 Ok(BackorderAllocation {
552 id,
553 backorder_id: input.backorder_id,
554 sku: backorder.sku,
555 quantity: input.quantity,
556 location_id: input.location_id,
557 lot_id: input.lot_id,
558 status: AllocationStatus::Reserved,
559 allocated_at: now,
560 expires_at: input.expires_at,
561 })
562 }
563
564 fn get_allocations(&self, backorder_id: Uuid) -> Result<Vec<BackorderAllocation>> {
565 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
566 let mut stmt = conn.prepare(
567 "SELECT id, backorder_id, sku, quantity, location_id, lot_id, status, allocated_at, expires_at
568 FROM backorder_allocations WHERE backorder_id = ? AND status = 'reserved'"
569 ).map_err(map_db_error)?;
570
571 let rows = stmt
572 .query_map([backorder_id.to_string()], |row| self.row_to_allocation(row))
573 .map_err(map_db_error)?;
574
575 let mut allocations = Vec::new();
576 for row in rows {
577 allocations.push(row.map_err(map_db_error)?);
578 }
579 Ok(allocations)
580 }
581
582 fn release_allocation(&self, allocation_id: Uuid) -> Result<BackorderAllocation> {
583 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
584
585 conn.execute(
586 "UPDATE backorder_allocations SET status = 'released' WHERE id = ?",
587 [allocation_id.to_string()],
588 )
589 .map_err(map_db_error)?;
590
591 let result = conn.query_row(
592 "SELECT id, backorder_id, sku, quantity, location_id, lot_id, status, allocated_at, expires_at
593 FROM backorder_allocations WHERE id = ?",
594 [allocation_id.to_string()],
595 |row| self.row_to_allocation(row),
596 ).map_err(map_db_error)?;
597
598 Ok(result)
599 }
600
601 fn confirm_allocation(&self, allocation_id: Uuid) -> Result<BackorderAllocation> {
602 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
603
604 conn.execute(
605 "UPDATE backorder_allocations SET status = 'confirmed' WHERE id = ?",
606 [allocation_id.to_string()],
607 )
608 .map_err(map_db_error)?;
609
610 let result = conn.query_row(
611 "SELECT id, backorder_id, sku, quantity, location_id, lot_id, status, allocated_at, expires_at
612 FROM backorder_allocations WHERE id = ?",
613 [allocation_id.to_string()],
614 |row| self.row_to_allocation(row),
615 ).map_err(map_db_error)?;
616
617 Ok(result)
618 }
619
620 fn expire_allocations(&self) -> Result<u32> {
621 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
622 let now = Utc::now();
623
624 let count = conn
625 .execute(
626 "UPDATE backorder_allocations SET status = 'expired'
627 WHERE status = 'reserved' AND expires_at IS NOT NULL AND expires_at < ?",
628 [now.to_rfc3339()],
629 )
630 .map_err(map_db_error)?;
631
632 Ok(count as u32)
633 }
634
635 fn auto_allocate_inventory(&self, sku: &str) -> Result<Vec<BackorderAllocation>> {
636 let _backorders = self.list_backorders(BackorderFilter {
638 sku: Some(sku.to_string()),
639 status: Some(BackorderStatus::Pending),
640 ..Default::default()
641 })?;
642
643 Ok(Vec::new())
647 }
648
649 fn get_summary(&self) -> Result<BackorderSummary> {
650 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
651 let now = Utc::now();
652
653 let (total, pending, allocated, critical): (i32, i32, i32, i32) = conn
654 .query_row(
655 "SELECT
656 COUNT(*),
657 SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END),
658 SUM(CASE WHEN status = 'allocated' THEN 1 ELSE 0 END),
659 SUM(CASE WHEN priority = 'critical' THEN 1 ELSE 0 END)
660 FROM backorders WHERE status NOT IN ('fulfilled', 'cancelled')",
661 [],
662 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
663 )
664 .map_err(map_db_error)?;
665
666 let overdue: i32 = conn
667 .query_row(
668 "SELECT COUNT(*) FROM backorders
669 WHERE status NOT IN ('fulfilled', 'cancelled')
670 AND expected_date IS NOT NULL AND expected_date < ?",
671 [now.to_rfc3339()],
672 |row| row.get(0),
673 )
674 .map_err(map_db_error)?;
675
676 let total_quantity = sum_decimal_query(
677 &conn,
678 "SELECT quantity_remaining FROM backorders WHERE status NOT IN ('fulfilled', 'cancelled')",
679 &[],
680 "backorders",
681 "quantity_remaining",
682 )?;
683
684 Ok(BackorderSummary {
685 total_backorders: total,
686 total_quantity,
687 pending_count: pending,
688 allocated_count: allocated,
689 critical_count: critical,
690 overdue_count: overdue,
691 })
692 }
693
694 fn get_sku_summary(&self, sku: &str) -> Result<Option<SkuBackorderSummary>> {
695 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
696
697 let (count, oldest_date_raw, earliest_expected_raw): (i32, Option<String>, Option<String>) =
698 conn.query_row(
699 "SELECT COUNT(*), MIN(created_at), MIN(expected_date)
700 FROM backorders
701 WHERE sku = ? AND status NOT IN ('fulfilled', 'cancelled')",
702 [sku],
703 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
704 )
705 .map_err(map_db_error)?;
706
707 if count == 0 {
708 return Ok(None);
709 }
710
711 let sku_param = sku.to_string();
712 let sku_params: [&dyn rusqlite::ToSql; 1] = [&sku_param];
713 let total_quantity = sum_decimal_query(
714 &conn,
715 "SELECT quantity_remaining FROM backorders WHERE sku = ? AND status NOT IN ('fulfilled', 'cancelled')",
716 &sku_params,
717 "backorders",
718 "quantity_remaining",
719 )?;
720 let oldest_date =
721 parse_datetime_opt(oldest_date_raw, "sku_backorder_summary", "oldest_date")?;
722 let earliest_expected = parse_datetime_opt(
723 earliest_expected_raw,
724 "sku_backorder_summary",
725 "earliest_expected",
726 )?;
727
728 Ok(Some(SkuBackorderSummary {
729 sku: sku.to_string(),
730 total_quantity,
731 backorder_count: count,
732 oldest_date,
733 earliest_expected,
734 }))
735 }
736
737 fn get_overdue_backorders(&self) -> Result<Vec<Backorder>> {
738 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
739 let now = Utc::now();
740
741 let mut stmt = conn
742 .prepare(
743 "SELECT id, backorder_number, order_id, order_line_id, customer_id, sku,
744 quantity_ordered, quantity_fulfilled, quantity_remaining, status, priority,
745 expected_date, promised_date, source_location_id, notes, created_at, updated_at
746 FROM backorders
747 WHERE status NOT IN ('fulfilled', 'cancelled')
748 AND expected_date IS NOT NULL AND expected_date < ?
749 ORDER BY expected_date",
750 )
751 .map_err(map_db_error)?;
752
753 let rows = stmt
754 .query_map([now.to_rfc3339()], |row| self.row_to_backorder(row))
755 .map_err(map_db_error)?;
756
757 let mut backorders = Vec::new();
758 for row in rows {
759 backorders.push(row.map_err(map_db_error)?);
760 }
761 Ok(backorders)
762 }
763
764 fn count_pending(&self) -> Result<u64> {
765 let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
766 let count: i64 = conn
767 .query_row("SELECT COUNT(*) FROM backorders WHERE status = 'pending'", [], |row| {
768 row.get(0)
769 })
770 .map_err(map_db_error)?;
771 Ok(count as u64)
772 }
773}