Skip to main content

stateset_db/sqlite/
backorder.rs

1//! SQLite implementation of backorder repository
2
3use 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(), // quantity_remaining starts same as ordered
248                    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        // Order by priority (critical first) then by created_at
358        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        // Release any allocations
389        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        // The status/quantity read, the guards, the fulfillment INSERT and the
424        // backorder UPDATE all run inside ONE `IMMEDIATE` transaction, so
425        // concurrent fulfillments serialize (no lost update / over-fulfill) and a
426        // failed UPDATE can't leave an orphaned fulfillment row.
427        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            // A cancelled or already-fulfilled backorder cannot be fulfilled.
445            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            // Cannot fulfill more units than remain.
451            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        // Update backorder status to allocated
544        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        // Get pending backorders for this SKU ordered by priority
637        let _backorders = self.list_backorders(BackorderFilter {
638            sku: Some(sku.to_string()),
639            status: Some(BackorderStatus::Pending),
640            ..Default::default()
641        })?;
642
643        // Note: In a real implementation, we would check inventory availability
644        // and create allocations. For now, return empty as this requires integration
645        // with the inventory module.
646        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}