Skip to main content

stateset_db/sqlite/
serials.rs

1//! SQLite implementation of Serial Number Repository
2
3use chrono::Utc;
4use r2d2::Pool;
5use r2d2_sqlite::SqliteConnectionManager;
6use rusqlite::{Row, params};
7use stateset_core::CommerceError;
8use stateset_core::{
9    BatchResult, ChangeSerialStatus, CreateSerialNumber, CreateSerialNumbersBulk, LotRepository,
10    MoveSerial, ReserveSerialNumber, SerialEventType, SerialFilter, SerialHistory,
11    SerialHistoryFilter, SerialLookupResult, SerialNumber, SerialRepository, SerialReservation,
12    SerialStatus, SerialValidation, TransferSerialOwnership, UpdateSerialNumber, WarrantyId,
13    WarrantyLookupStatus, WarrantyRepository,
14};
15use uuid::Uuid;
16
17use super::{
18    SqliteLotRepository, SqliteWarrantyRepository, build_in_clause, map_db_error, params_refs,
19    parse_datetime_opt_row, parse_datetime_row, parse_enum_row, parse_json_opt_row,
20    parse_uuid_opt_row, parse_uuid_row, string_params, uuid_params,
21};
22
23/// SQLite implementation of `SerialRepository`
24#[derive(Debug)]
25pub struct SqliteSerialRepository {
26    pool: Pool<SqliteConnectionManager>,
27}
28
29impl SqliteSerialRepository {
30    #[must_use]
31    pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
32        Self { pool }
33    }
34
35    /// Build the shared `WHERE` conditions (and their bound params) for serial
36    /// queries, so `list` and `count` filter identically — a divergence between
37    /// them was exactly how `count` came to ignore most filters.
38    fn serial_filter_conditions(
39        filter: &SerialFilter,
40    ) -> (Vec<String>, Vec<Box<dyn rusqlite::ToSql>>) {
41        let mut conditions = Vec::new();
42        let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
43
44        if let Some(serial) = &filter.serial {
45            conditions.push("serial = ?".to_string());
46            params.push(Box::new(serial.clone()));
47        }
48        if let Some(prefix) = &filter.serial_prefix {
49            conditions.push("serial LIKE ?".to_string());
50            params.push(Box::new(format!("{prefix}%")));
51        }
52        if let Some(sku) = &filter.sku {
53            conditions.push("sku = ?".to_string());
54            params.push(Box::new(sku.clone()));
55        }
56        if let Some(status) = &filter.status {
57            conditions.push("status = ?".to_string());
58            params.push(Box::new(status.to_string()));
59        }
60        if let Some(statuses) = &filter.statuses {
61            let placeholders = build_in_clause(statuses.len());
62            conditions.push(format!("status IN ({placeholders})"));
63            for s in statuses {
64                params.push(Box::new(s.to_string()));
65            }
66        }
67        if let Some(lot_id) = &filter.lot_id {
68            conditions.push("lot_id = ?".to_string());
69            params.push(Box::new(lot_id.to_string()));
70        }
71        if let Some(lot_number) = &filter.lot_number {
72            conditions.push("lot_number = ?".to_string());
73            params.push(Box::new(lot_number.clone()));
74        }
75        if let Some(loc_id) = filter.location_id {
76            conditions.push("current_location_id = ?".to_string());
77            params.push(Box::new(loc_id));
78        }
79        if let Some(owner_id) = &filter.owner_id {
80            conditions.push("current_owner_id = ?".to_string());
81            params.push(Box::new(owner_id.to_string()));
82        }
83        if let Some(owner_type) = &filter.owner_type {
84            conditions.push("current_owner_type = ?".to_string());
85            params.push(Box::new(owner_type.clone()));
86        }
87        if let Some(warranty_id) = &filter.warranty_id {
88            conditions.push("warranty_id = ?".to_string());
89            params.push(Box::new(warranty_id.to_string()));
90        }
91        if let Some(has_warranty) = filter.has_warranty {
92            if has_warranty {
93                conditions.push("warranty_id IS NOT NULL".to_string());
94            } else {
95                conditions.push("warranty_id IS NULL".to_string());
96            }
97        }
98        if let Some(after) = &filter.manufactured_after {
99            conditions.push("manufactured_at >= ?".to_string());
100            params.push(Box::new(after.to_rfc3339()));
101        }
102        if let Some(before) = &filter.manufactured_before {
103            conditions.push("manufactured_at <= ?".to_string());
104            params.push(Box::new(before.to_rfc3339()));
105        }
106        if let Some(after) = &filter.sold_after {
107            conditions.push("sold_at >= ?".to_string());
108            params.push(Box::new(after.to_rfc3339()));
109        }
110        if let Some(before) = &filter.sold_before {
111            conditions.push("sold_at <= ?".to_string());
112            params.push(Box::new(before.to_rfc3339()));
113        }
114
115        (conditions, params)
116    }
117
118    fn map_serial_row(row: &Row<'_>) -> Result<SerialNumber, rusqlite::Error> {
119        let status_str: String = row.get("status")?;
120        let attributes_str: Option<String> = row.get("attributes")?;
121
122        Ok(SerialNumber {
123            id: parse_uuid_row(&row.get::<_, String>("id")?, "serial", "id")?,
124            serial: row.get("serial")?,
125            sku: row.get("sku")?,
126            status: parse_enum_row(&status_str, "serial", "status")?,
127            lot_id: parse_uuid_opt_row(
128                row.get::<_, Option<String>>("lot_id")?,
129                "serial",
130                "lot_id",
131            )?,
132            lot_number: row.get("lot_number")?,
133            current_location_id: row.get("current_location_id")?,
134            current_owner_id: parse_uuid_opt_row(
135                row.get::<_, Option<String>>("current_owner_id")?,
136                "serial",
137                "current_owner_id",
138            )?,
139            current_owner_type: row.get("current_owner_type")?,
140            warranty_id: parse_uuid_opt_row(
141                row.get::<_, Option<String>>("warranty_id")?,
142                "serial",
143                "warranty_id",
144            )?,
145            manufactured_at: parse_datetime_opt_row(
146                row.get::<_, Option<String>>("manufactured_at")?,
147                "serial",
148                "manufactured_at",
149            )?,
150            received_at: parse_datetime_opt_row(
151                row.get::<_, Option<String>>("received_at")?,
152                "serial",
153                "received_at",
154            )?,
155            sold_at: parse_datetime_opt_row(
156                row.get::<_, Option<String>>("sold_at")?,
157                "serial",
158                "sold_at",
159            )?,
160            activated_at: parse_datetime_opt_row(
161                row.get::<_, Option<String>>("activated_at")?,
162                "serial",
163                "activated_at",
164            )?,
165            last_service_at: parse_datetime_opt_row(
166                row.get::<_, Option<String>>("last_service_at")?,
167                "serial",
168                "last_service_at",
169            )?,
170            notes: row.get("notes")?,
171            attributes: parse_json_opt_row(attributes_str, "serial", "attributes")?
172                .unwrap_or(serde_json::Value::Null),
173            created_at: parse_datetime_row(
174                &row.get::<_, String>("created_at")?,
175                "serial",
176                "created_at",
177            )?,
178            updated_at: parse_datetime_row(
179                &row.get::<_, String>("updated_at")?,
180                "serial",
181                "updated_at",
182            )?,
183        })
184    }
185
186    fn map_history_row(row: &Row<'_>) -> Result<SerialHistory, rusqlite::Error> {
187        let event_type_str: String = row.get("event_type")?;
188        let from_status_str: String = row.get("from_status")?;
189        let to_status_str: String = row.get("to_status")?;
190
191        Ok(SerialHistory {
192            id: parse_uuid_row(&row.get::<_, String>("id")?, "serial_history", "id")?,
193            serial_id: parse_uuid_row(
194                &row.get::<_, String>("serial_id")?,
195                "serial_history",
196                "serial_id",
197            )?,
198            event_type: parse_enum_row(&event_type_str, "serial_history", "event_type")?,
199            reference_type: row.get("reference_type")?,
200            reference_id: parse_uuid_opt_row(
201                row.get::<_, Option<String>>("reference_id")?,
202                "serial_history",
203                "reference_id",
204            )?,
205            from_status: parse_enum_row(&from_status_str, "serial_history", "from_status")?,
206            to_status: parse_enum_row(&to_status_str, "serial_history", "to_status")?,
207            from_location_id: row.get("from_location_id")?,
208            to_location_id: row.get("to_location_id")?,
209            from_owner_id: parse_uuid_opt_row(
210                row.get::<_, Option<String>>("from_owner_id")?,
211                "serial_history",
212                "from_owner_id",
213            )?,
214            to_owner_id: parse_uuid_opt_row(
215                row.get::<_, Option<String>>("to_owner_id")?,
216                "serial_history",
217                "to_owner_id",
218            )?,
219            performed_by: row.get("performed_by")?,
220            notes: row.get("notes")?,
221            created_at: parse_datetime_row(
222                &row.get::<_, String>("created_at")?,
223                "serial_history",
224                "created_at",
225            )?,
226        })
227    }
228
229    fn map_reservation_row(row: &Row<'_>) -> Result<SerialReservation, rusqlite::Error> {
230        Ok(SerialReservation {
231            id: parse_uuid_row(&row.get::<_, String>("id")?, "serial_reservation", "id")?,
232            serial_id: parse_uuid_row(
233                &row.get::<_, String>("serial_id")?,
234                "serial_reservation",
235                "serial_id",
236            )?,
237            reference_type: row.get("reference_type")?,
238            reference_id: parse_uuid_row(
239                &row.get::<_, String>("reference_id")?,
240                "serial_reservation",
241                "reference_id",
242            )?,
243            reserved_by: row.get("reserved_by")?,
244            reserved_at: parse_datetime_row(
245                &row.get::<_, String>("reserved_at")?,
246                "serial_reservation",
247                "reserved_at",
248            )?,
249            expires_at: parse_datetime_opt_row(
250                row.get::<_, Option<String>>("expires_at")?,
251                "serial_reservation",
252                "expires_at",
253            )?,
254            confirmed_at: parse_datetime_opt_row(
255                row.get::<_, Option<String>>("confirmed_at")?,
256                "serial_reservation",
257                "confirmed_at",
258            )?,
259            released_at: parse_datetime_opt_row(
260                row.get::<_, Option<String>>("released_at")?,
261                "serial_reservation",
262                "released_at",
263            )?,
264        })
265    }
266
267    #[allow(clippy::too_many_arguments, dead_code)]
268    fn record_history(
269        &self,
270        conn: &rusqlite::Connection,
271        serial: &SerialNumber,
272        event_type: SerialEventType,
273        from_status: SerialStatus,
274        to_status: SerialStatus,
275        reference_type: Option<&str>,
276        reference_id: Option<Uuid>,
277        from_location_id: Option<i32>,
278        to_location_id: Option<i32>,
279        from_owner_id: Option<Uuid>,
280        to_owner_id: Option<Uuid>,
281        performed_by: Option<&str>,
282        notes: Option<&str>,
283    ) -> Result<(), rusqlite::Error> {
284        let id = Uuid::new_v4();
285        let now = Utc::now().to_rfc3339();
286
287        conn.execute(
288            "INSERT INTO serial_history (
289                id, serial_id, event_type, reference_type, reference_id,
290                from_status, to_status, from_location_id, to_location_id,
291                from_owner_id, to_owner_id, performed_by, notes, created_at
292            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
293            params![
294                id.to_string(),
295                serial.id.to_string(),
296                event_type.to_string(),
297                reference_type,
298                reference_id.map(|id| id.to_string()),
299                from_status.to_string(),
300                to_status.to_string(),
301                from_location_id,
302                to_location_id,
303                from_owner_id.map(|id| id.to_string()),
304                to_owner_id.map(|id| id.to_string()),
305                performed_by,
306                notes,
307                now,
308            ],
309        )?;
310
311        Ok(())
312    }
313
314    fn generate_serial(&self, prefix: Option<&str>) -> String {
315        let unique_part = Uuid::new_v4().to_string().replace('-', "").to_uppercase();
316        let short = &unique_part[..12];
317        match prefix {
318            Some(p) => format!("{p}-{short}"),
319            None => format!("SN-{short}"),
320        }
321    }
322}
323
324impl SerialRepository for SqliteSerialRepository {
325    fn create(&self, input: CreateSerialNumber) -> stateset_core::Result<SerialNumber> {
326        let id = Uuid::new_v4();
327        let now = Utc::now().to_rfc3339();
328        let serial = input.serial.unwrap_or_else(|| self.generate_serial(None));
329        let attributes = input.attributes.unwrap_or(serde_json::Value::Null);
330
331        {
332            let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
333            conn.execute(
334                "INSERT INTO serial_numbers (
335                    id, serial, sku, status, lot_id, lot_number, current_location_id,
336                    manufactured_at, notes, attributes, created_at, updated_at
337                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
338                params![
339                    id.to_string(),
340                    serial,
341                    input.sku,
342                    SerialStatus::Available.to_string(),
343                    input.lot_id.map(|id| id.to_string()),
344                    input.lot_number,
345                    input.location_id,
346                    input.manufactured_at.map(|dt| dt.to_rfc3339()),
347                    input.notes,
348                    serde_json::to_string(&attributes).ok(),
349                    now,
350                    now,
351                ],
352            )
353            .map_err(map_db_error)?;
354        }
355
356        self.get(id)?.ok_or(CommerceError::NotFound)
357    }
358
359    fn create_bulk(
360        &self,
361        input: CreateSerialNumbersBulk,
362    ) -> stateset_core::Result<Vec<SerialNumber>> {
363        let mut conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
364        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
365
366        let mut serials = Vec::with_capacity(input.quantity as usize);
367        let now = Utc::now().to_rfc3339();
368
369        for i in 0..input.quantity {
370            let id = Uuid::new_v4();
371            let serial_number = match &input.prefix {
372                Some(prefix) => format!("{}-{:06}", prefix, i + 1),
373                None => self.generate_serial(None),
374            };
375
376            tx.execute(
377                "INSERT INTO serial_numbers (
378                    id, serial, sku, status, lot_id, lot_number, current_location_id,
379                    manufactured_at, created_at, updated_at
380                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
381                params![
382                    id.to_string(),
383                    serial_number,
384                    input.sku,
385                    SerialStatus::Available.to_string(),
386                    input.lot_id.map(|id| id.to_string()),
387                    input.lot_number,
388                    input.location_id,
389                    input.manufactured_at.map(|dt| dt.to_rfc3339()),
390                    now,
391                    now,
392                ],
393            )
394            .map_err(map_db_error)?;
395
396            // Record creation history
397            let history_id = Uuid::new_v4();
398            tx.execute(
399                "INSERT INTO serial_history (
400                    id, serial_id, event_type, from_status, to_status, created_at
401                ) VALUES (?, ?, ?, ?, ?, ?)",
402                params![
403                    history_id.to_string(),
404                    id.to_string(),
405                    SerialEventType::Created.to_string(),
406                    SerialStatus::Available.to_string(),
407                    SerialStatus::Available.to_string(),
408                    now,
409                ],
410            )
411            .map_err(map_db_error)?;
412
413            serials.push(SerialNumber {
414                id,
415                serial: serial_number,
416                sku: input.sku.clone(),
417                status: SerialStatus::Available,
418                lot_id: input.lot_id,
419                lot_number: input.lot_number.clone(),
420                current_location_id: input.location_id,
421                current_owner_id: None,
422                current_owner_type: None,
423                warranty_id: None,
424                manufactured_at: input.manufactured_at,
425                received_at: None,
426                sold_at: None,
427                activated_at: None,
428                last_service_at: None,
429                notes: None,
430                attributes: serde_json::Value::Null,
431                created_at: Utc::now(),
432                updated_at: Utc::now(),
433            });
434        }
435
436        tx.commit().map_err(map_db_error)?;
437        Ok(serials)
438    }
439
440    fn get(&self, id: Uuid) -> stateset_core::Result<Option<SerialNumber>> {
441        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
442
443        let result = conn.query_row(
444            "SELECT * FROM serial_numbers WHERE id = ?",
445            params![id.to_string()],
446            Self::map_serial_row,
447        );
448
449        match result {
450            Ok(serial) => Ok(Some(serial)),
451            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
452            Err(e) => Err(map_db_error(e)),
453        }
454    }
455
456    fn get_by_serial(&self, serial: &str) -> stateset_core::Result<Option<SerialNumber>> {
457        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
458
459        let result = conn.query_row(
460            "SELECT * FROM serial_numbers WHERE serial = ?",
461            params![serial],
462            Self::map_serial_row,
463        );
464
465        match result {
466            Ok(serial) => Ok(Some(serial)),
467            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
468            Err(e) => Err(map_db_error(e)),
469        }
470    }
471
472    fn update(&self, id: Uuid, input: UpdateSerialNumber) -> stateset_core::Result<SerialNumber> {
473        let now = Utc::now().to_rfc3339();
474
475        let mut updates = vec!["updated_at = ?".to_string()];
476        let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(now)];
477
478        if let Some(status) = &input.status {
479            updates.push("status = ?".to_string());
480            params.push(Box::new(status.to_string()));
481        }
482        if let Some(loc) = input.location_id {
483            updates.push("current_location_id = ?".to_string());
484            params.push(Box::new(loc));
485        }
486        if let Some(lot_id) = &input.lot_id {
487            updates.push("lot_id = ?".to_string());
488            params.push(Box::new(lot_id.to_string()));
489        }
490        if let Some(warranty_id) = &input.warranty_id {
491            updates.push("warranty_id = ?".to_string());
492            params.push(Box::new(warranty_id.to_string()));
493        }
494        if let Some(notes) = &input.notes {
495            updates.push("notes = ?".to_string());
496            params.push(Box::new(notes.clone()));
497        }
498        if let Some(attrs) = &input.attributes {
499            updates.push("attributes = ?".to_string());
500            params.push(Box::new(serde_json::to_string(attrs).unwrap_or_default()));
501        }
502
503        params.push(Box::new(id.to_string()));
504
505        let sql = format!("UPDATE serial_numbers SET {} WHERE id = ?", updates.join(", "));
506
507        {
508            let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
509            conn.execute(
510                &sql,
511                rusqlite::params_from_iter(params.iter().map(std::convert::AsRef::as_ref)),
512            )
513            .map_err(map_db_error)?;
514        }
515
516        self.get(id)?.ok_or(CommerceError::NotFound)
517    }
518
519    fn list(&self, filter: SerialFilter) -> stateset_core::Result<Vec<SerialNumber>> {
520        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
521
522        let (conditions, mut params) = Self::serial_filter_conditions(&filter);
523
524        let where_clause = if conditions.is_empty() {
525            String::new()
526        } else {
527            format!("WHERE {}", conditions.join(" AND "))
528        };
529
530        let limit = filter.limit.unwrap_or(100);
531        let offset = filter.offset.unwrap_or(0);
532
533        let sql = format!(
534            "SELECT * FROM serial_numbers {where_clause} ORDER BY created_at DESC LIMIT ? OFFSET ?"
535        );
536
537        params.push(Box::new(i64::from(limit)));
538        params.push(Box::new(i64::from(offset)));
539
540        let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
541        let rows = stmt
542            .query_map(
543                rusqlite::params_from_iter(params.iter().map(std::convert::AsRef::as_ref)),
544                Self::map_serial_row,
545            )
546            .map_err(map_db_error)?;
547
548        let mut serials = Vec::new();
549        for row in rows {
550            serials.push(row.map_err(map_db_error)?);
551        }
552
553        Ok(serials)
554    }
555
556    fn delete(&self, id: Uuid) -> stateset_core::Result<()> {
557        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
558
559        // Check if serial can be deleted (only if Available and never used)
560        let serial = self.get(id)?.ok_or(CommerceError::NotFound)?;
561        if serial.status != SerialStatus::Available {
562            return Err(CommerceError::ValidationError(
563                "Can only delete serials with 'available' status".to_string(),
564            ));
565        }
566
567        // Check if there's any history beyond creation
568        let history_count: i64 = conn
569            .query_row(
570                "SELECT COUNT(*) FROM serial_history WHERE serial_id = ? AND event_type != ?",
571                params![id.to_string(), SerialEventType::Created.to_string()],
572                |row| row.get(0),
573            )
574            .map_err(map_db_error)?;
575
576        if history_count > 0 {
577            return Err(CommerceError::ValidationError(
578                "Cannot delete serial with transaction history".to_string(),
579            ));
580        }
581
582        // Delete history first
583        conn.execute("DELETE FROM serial_history WHERE serial_id = ?", params![id.to_string()])
584            .map_err(map_db_error)?;
585
586        // Delete reservations
587        conn.execute(
588            "DELETE FROM serial_reservations WHERE serial_id = ?",
589            params![id.to_string()],
590        )
591        .map_err(map_db_error)?;
592
593        // Delete serial
594        conn.execute("DELETE FROM serial_numbers WHERE id = ?", params![id.to_string()])
595            .map_err(map_db_error)?;
596
597        Ok(())
598    }
599
600    fn change_status(&self, input: ChangeSerialStatus) -> stateset_core::Result<SerialNumber> {
601        {
602            let mut conn =
603                self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
604            let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
605            let now = Utc::now().to_rfc3339();
606
607            // Get current serial
608            let serial: SerialNumber = tx
609                .query_row(
610                    "SELECT * FROM serial_numbers WHERE id = ?",
611                    params![input.serial_id.to_string()],
612                    Self::map_serial_row,
613                )
614                .map_err(map_db_error)?;
615
616            let old_status = serial.status;
617
618            // Update serial
619            tx.execute(
620                "UPDATE serial_numbers SET
621                    status = ?,
622                    current_location_id = COALESCE(?, current_location_id),
623                    current_owner_id = COALESCE(?, current_owner_id),
624                    current_owner_type = COALESCE(?, current_owner_type),
625                    updated_at = ?
626                WHERE id = ?",
627                params![
628                    input.new_status.to_string(),
629                    input.location_id,
630                    input.owner_id.map(|id| id.to_string()),
631                    input.owner_type,
632                    now,
633                    input.serial_id.to_string(),
634                ],
635            )
636            .map_err(map_db_error)?;
637
638            // Record history
639            let history_id = Uuid::new_v4();
640            tx.execute(
641                "INSERT INTO serial_history (
642                    id, serial_id, event_type, reference_type, reference_id,
643                    from_status, to_status, from_location_id, to_location_id,
644                    from_owner_id, to_owner_id, performed_by, notes, created_at
645                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
646                params![
647                    history_id.to_string(),
648                    input.serial_id.to_string(),
649                    SerialEventType::StatusChanged.to_string(),
650                    input.reference_type,
651                    input.reference_id.map(|id| id.to_string()),
652                    old_status.to_string(),
653                    input.new_status.to_string(),
654                    serial.current_location_id,
655                    input.location_id,
656                    serial.current_owner_id.map(|id| id.to_string()),
657                    input.owner_id.map(|id| id.to_string()),
658                    input.performed_by,
659                    input.notes,
660                    now,
661                ],
662            )
663            .map_err(map_db_error)?;
664
665            tx.commit().map_err(map_db_error)?;
666        }
667        self.get(input.serial_id)?.ok_or(CommerceError::NotFound)
668    }
669
670    fn reserve(&self, input: ReserveSerialNumber) -> stateset_core::Result<SerialReservation> {
671        let mut conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
672        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
673        let now = Utc::now();
674        let now_str = now.to_rfc3339();
675
676        // Get and validate serial
677        let serial: SerialNumber = tx
678            .query_row(
679                "SELECT * FROM serial_numbers WHERE id = ?",
680                params![input.serial_id.to_string()],
681                Self::map_serial_row,
682            )
683            .map_err(map_db_error)?;
684
685        if serial.status != SerialStatus::Available {
686            return Err(CommerceError::ValidationError(format!(
687                "Serial is not available for reservation, current status: {}",
688                serial.status
689            )));
690        }
691
692        // Check for existing active reservations
693        let existing: i64 = tx
694            .query_row(
695                "SELECT COUNT(*) FROM serial_reservations
696             WHERE serial_id = ? AND released_at IS NULL AND confirmed_at IS NULL
697             AND (expires_at IS NULL OR expires_at > ?)",
698                params![input.serial_id.to_string(), now_str],
699                |row| row.get(0),
700            )
701            .map_err(map_db_error)?;
702
703        if existing > 0 {
704            return Err(CommerceError::ValidationError(
705                "Serial already has an active reservation".to_string(),
706            ));
707        }
708
709        let id = Uuid::new_v4();
710        let expires_at = input.expires_in_seconds.map(|secs| now + chrono::Duration::seconds(secs));
711
712        // Create reservation
713        tx.execute(
714            "INSERT INTO serial_reservations (
715                id, serial_id, reference_type, reference_id, reserved_by,
716                reserved_at, expires_at
717            ) VALUES (?, ?, ?, ?, ?, ?, ?)",
718            params![
719                id.to_string(),
720                input.serial_id.to_string(),
721                input.reference_type,
722                input.reference_id.to_string(),
723                input.reserved_by,
724                now_str,
725                expires_at.map(|dt| dt.to_rfc3339()),
726            ],
727        )
728        .map_err(map_db_error)?;
729
730        // Update serial status
731        tx.execute(
732            "UPDATE serial_numbers SET status = ?, updated_at = ? WHERE id = ?",
733            params![SerialStatus::Reserved.to_string(), now_str, input.serial_id.to_string(),],
734        )
735        .map_err(map_db_error)?;
736
737        // Record history
738        let history_id = Uuid::new_v4();
739        tx.execute(
740            "INSERT INTO serial_history (
741                id, serial_id, event_type, reference_type, reference_id,
742                from_status, to_status, performed_by, created_at
743            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
744            params![
745                history_id.to_string(),
746                input.serial_id.to_string(),
747                SerialEventType::Reserved.to_string(),
748                input.reference_type,
749                input.reference_id.to_string(),
750                SerialStatus::Available.to_string(),
751                SerialStatus::Reserved.to_string(),
752                input.reserved_by,
753                now_str,
754            ],
755        )
756        .map_err(map_db_error)?;
757
758        tx.commit().map_err(map_db_error)?;
759
760        Ok(SerialReservation {
761            id,
762            serial_id: input.serial_id,
763            reference_type: input.reference_type,
764            reference_id: input.reference_id,
765            reserved_by: input.reserved_by,
766            reserved_at: now,
767            expires_at,
768            confirmed_at: None,
769            released_at: None,
770        })
771    }
772
773    fn release_reservation(&self, reservation_id: Uuid) -> stateset_core::Result<()> {
774        let mut conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
775        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
776        let now = Utc::now().to_rfc3339();
777
778        // Get reservation
779        let reservation: SerialReservation = tx
780            .query_row(
781                "SELECT * FROM serial_reservations WHERE id = ?",
782                params![reservation_id.to_string()],
783                Self::map_reservation_row,
784            )
785            .map_err(map_db_error)?;
786
787        if reservation.released_at.is_some() || reservation.confirmed_at.is_some() {
788            return Err(CommerceError::ValidationError(
789                "Reservation is already released or confirmed".to_string(),
790            ));
791        }
792
793        // Release reservation
794        tx.execute(
795            "UPDATE serial_reservations SET released_at = ? WHERE id = ?",
796            params![now, reservation_id.to_string()],
797        )
798        .map_err(map_db_error)?;
799
800        // Update serial status back to available
801        tx.execute(
802            "UPDATE serial_numbers SET status = ?, updated_at = ? WHERE id = ?",
803            params![SerialStatus::Available.to_string(), now, reservation.serial_id.to_string(),],
804        )
805        .map_err(map_db_error)?;
806
807        // Record history
808        let history_id = Uuid::new_v4();
809        tx.execute(
810            "INSERT INTO serial_history (
811                id, serial_id, event_type, reference_type, reference_id,
812                from_status, to_status, created_at
813            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
814            params![
815                history_id.to_string(),
816                reservation.serial_id.to_string(),
817                SerialEventType::Released.to_string(),
818                reservation.reference_type,
819                reservation.reference_id.to_string(),
820                SerialStatus::Reserved.to_string(),
821                SerialStatus::Available.to_string(),
822                now,
823            ],
824        )
825        .map_err(map_db_error)?;
826
827        tx.commit().map_err(map_db_error)?;
828        Ok(())
829    }
830
831    fn confirm_reservation(&self, reservation_id: Uuid) -> stateset_core::Result<()> {
832        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
833        let now = Utc::now().to_rfc3339();
834
835        // Get reservation
836        let reservation: SerialReservation = conn
837            .query_row(
838                "SELECT * FROM serial_reservations WHERE id = ?",
839                params![reservation_id.to_string()],
840                Self::map_reservation_row,
841            )
842            .map_err(map_db_error)?;
843
844        if reservation.released_at.is_some() {
845            return Err(CommerceError::ValidationError(
846                "Reservation has been released".to_string(),
847            ));
848        }
849
850        if reservation.confirmed_at.is_some() {
851            return Ok(()); // Already confirmed
852        }
853
854        conn.execute(
855            "UPDATE serial_reservations SET confirmed_at = ? WHERE id = ?",
856            params![now, reservation_id.to_string()],
857        )
858        .map_err(map_db_error)?;
859
860        Ok(())
861    }
862
863    fn move_serial(&self, input: MoveSerial) -> stateset_core::Result<SerialNumber> {
864        {
865            let mut conn =
866                self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
867            let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
868            let now = Utc::now().to_rfc3339();
869
870            // Get current serial
871            let serial: SerialNumber = tx
872                .query_row(
873                    "SELECT * FROM serial_numbers WHERE id = ?",
874                    params![input.serial_id.to_string()],
875                    Self::map_serial_row,
876                )
877                .map_err(map_db_error)?;
878
879            let from_location = serial.current_location_id;
880
881            // Update location
882            tx.execute(
883                "UPDATE serial_numbers SET current_location_id = ?, updated_at = ? WHERE id = ?",
884                params![input.to_location_id, now, input.serial_id.to_string()],
885            )
886            .map_err(map_db_error)?;
887
888            // Record history
889            let history_id = Uuid::new_v4();
890            tx.execute(
891                "INSERT INTO serial_history (
892                    id, serial_id, event_type, from_status, to_status,
893                    from_location_id, to_location_id, performed_by, notes, created_at
894                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
895                params![
896                    history_id.to_string(),
897                    input.serial_id.to_string(),
898                    SerialEventType::LocationChanged.to_string(),
899                    serial.status.to_string(),
900                    serial.status.to_string(),
901                    from_location,
902                    input.to_location_id,
903                    input.performed_by,
904                    input.notes,
905                    now,
906                ],
907            )
908            .map_err(map_db_error)?;
909
910            tx.commit().map_err(map_db_error)?;
911        }
912        self.get(input.serial_id)?.ok_or(CommerceError::NotFound)
913    }
914
915    fn transfer_ownership(
916        &self,
917        input: TransferSerialOwnership,
918    ) -> stateset_core::Result<SerialNumber> {
919        {
920            let mut conn =
921                self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
922            let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
923            let now = Utc::now().to_rfc3339();
924
925            // Get current serial
926            let serial: SerialNumber = tx
927                .query_row(
928                    "SELECT * FROM serial_numbers WHERE id = ?",
929                    params![input.serial_id.to_string()],
930                    Self::map_serial_row,
931                )
932                .map_err(map_db_error)?;
933
934            // Update ownership
935            tx.execute(
936                "UPDATE serial_numbers SET
937                    current_owner_id = ?,
938                    current_owner_type = ?,
939                    status = ?,
940                    updated_at = ?
941                WHERE id = ?",
942                params![
943                    input.new_owner_id.to_string(),
944                    input.new_owner_type,
945                    SerialStatus::Transferred.to_string(),
946                    now,
947                    input.serial_id.to_string(),
948                ],
949            )
950            .map_err(map_db_error)?;
951
952            // Record history
953            let history_id = Uuid::new_v4();
954            tx.execute(
955                "INSERT INTO serial_history (
956                    id, serial_id, event_type, reference_type, reference_id,
957                    from_status, to_status, from_owner_id, to_owner_id, notes, created_at
958                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
959                params![
960                    history_id.to_string(),
961                    input.serial_id.to_string(),
962                    SerialEventType::Transferred.to_string(),
963                    input.reference_type,
964                    input.reference_id.map(|id| id.to_string()),
965                    serial.status.to_string(),
966                    SerialStatus::Transferred.to_string(),
967                    serial.current_owner_id.map(|id| id.to_string()),
968                    input.new_owner_id.to_string(),
969                    input.notes,
970                    now,
971                ],
972            )
973            .map_err(map_db_error)?;
974
975            tx.commit().map_err(map_db_error)?;
976        }
977        self.get(input.serial_id)?.ok_or(CommerceError::NotFound)
978    }
979
980    fn mark_sold(
981        &self,
982        id: Uuid,
983        customer_id: Uuid,
984        order_id: Option<Uuid>,
985    ) -> stateset_core::Result<SerialNumber> {
986        {
987            let mut conn =
988                self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
989            let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
990            let now = Utc::now();
991            let now_str = now.to_rfc3339();
992
993            // Get current serial
994            let serial: SerialNumber = tx
995                .query_row(
996                    "SELECT * FROM serial_numbers WHERE id = ?",
997                    params![id.to_string()],
998                    Self::map_serial_row,
999                )
1000                .map_err(map_db_error)?;
1001
1002            // Update serial
1003            tx.execute(
1004                "UPDATE serial_numbers SET
1005                    status = ?,
1006                    current_owner_id = ?,
1007                    current_owner_type = 'customer',
1008                    sold_at = ?,
1009                    updated_at = ?
1010                WHERE id = ?",
1011                params![
1012                    SerialStatus::Sold.to_string(),
1013                    customer_id.to_string(),
1014                    now_str,
1015                    now_str,
1016                    id.to_string(),
1017                ],
1018            )
1019            .map_err(map_db_error)?;
1020
1021            // Record history
1022            let history_id = Uuid::new_v4();
1023            tx.execute(
1024                "INSERT INTO serial_history (
1025                    id, serial_id, event_type, reference_type, reference_id,
1026                    from_status, to_status, to_owner_id, created_at
1027                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
1028                params![
1029                    history_id.to_string(),
1030                    id.to_string(),
1031                    SerialEventType::Sold.to_string(),
1032                    order_id.map(|_| "order"),
1033                    order_id.map(|id| id.to_string()),
1034                    serial.status.to_string(),
1035                    SerialStatus::Sold.to_string(),
1036                    customer_id.to_string(),
1037                    now_str,
1038                ],
1039            )
1040            .map_err(map_db_error)?;
1041
1042            tx.commit().map_err(map_db_error)?;
1043        }
1044        self.get(id)?.ok_or(CommerceError::NotFound)
1045    }
1046
1047    fn mark_shipped(&self, id: Uuid, shipment_id: Uuid) -> stateset_core::Result<SerialNumber> {
1048        let mut conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1049        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1050        let now = Utc::now().to_rfc3339();
1051
1052        // Get current serial
1053        let serial: SerialNumber = tx
1054            .query_row(
1055                "SELECT * FROM serial_numbers WHERE id = ?",
1056                params![id.to_string()],
1057                Self::map_serial_row,
1058            )
1059            .map_err(map_db_error)?;
1060
1061        // Update serial
1062        tx.execute(
1063            "UPDATE serial_numbers SET status = ?, updated_at = ? WHERE id = ?",
1064            params![SerialStatus::Shipped.to_string(), now, id.to_string()],
1065        )
1066        .map_err(map_db_error)?;
1067
1068        // Record history
1069        let history_id = Uuid::new_v4();
1070        tx.execute(
1071            "INSERT INTO serial_history (
1072                id, serial_id, event_type, reference_type, reference_id,
1073                from_status, to_status, created_at
1074            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
1075            params![
1076                history_id.to_string(),
1077                id.to_string(),
1078                SerialEventType::Shipped.to_string(),
1079                "shipment",
1080                shipment_id.to_string(),
1081                serial.status.to_string(),
1082                SerialStatus::Shipped.to_string(),
1083                now,
1084            ],
1085        )
1086        .map_err(map_db_error)?;
1087
1088        tx.commit().map_err(map_db_error)?;
1089        self.get(id)?.ok_or(CommerceError::NotFound)
1090    }
1091
1092    fn mark_returned(&self, id: Uuid, return_id: Uuid) -> stateset_core::Result<SerialNumber> {
1093        let mut conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1094        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1095        let now = Utc::now().to_rfc3339();
1096
1097        // Get current serial
1098        let serial: SerialNumber = tx
1099            .query_row(
1100                "SELECT * FROM serial_numbers WHERE id = ?",
1101                params![id.to_string()],
1102                Self::map_serial_row,
1103            )
1104            .map_err(map_db_error)?;
1105
1106        // Update serial
1107        tx.execute(
1108            "UPDATE serial_numbers SET
1109                status = ?,
1110                current_owner_id = NULL,
1111                current_owner_type = NULL,
1112                updated_at = ?
1113            WHERE id = ?",
1114            params![SerialStatus::Returned.to_string(), now, id.to_string()],
1115        )
1116        .map_err(map_db_error)?;
1117
1118        // Record history
1119        let history_id = Uuid::new_v4();
1120        tx.execute(
1121            "INSERT INTO serial_history (
1122                id, serial_id, event_type, reference_type, reference_id,
1123                from_status, to_status, from_owner_id, created_at
1124            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
1125            params![
1126                history_id.to_string(),
1127                id.to_string(),
1128                SerialEventType::Returned.to_string(),
1129                "return",
1130                return_id.to_string(),
1131                serial.status.to_string(),
1132                SerialStatus::Returned.to_string(),
1133                serial.current_owner_id.map(|id| id.to_string()),
1134                now,
1135            ],
1136        )
1137        .map_err(map_db_error)?;
1138
1139        tx.commit().map_err(map_db_error)?;
1140        self.get(id)?.ok_or(CommerceError::NotFound)
1141    }
1142
1143    fn activate(&self, id: Uuid) -> stateset_core::Result<SerialNumber> {
1144        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1145        let now = Utc::now().to_rfc3339();
1146
1147        conn.execute(
1148            "UPDATE serial_numbers SET activated_at = ?, updated_at = ? WHERE id = ? AND activated_at IS NULL",
1149            params![now, now, id.to_string()],
1150        ).map_err(map_db_error)?;
1151
1152        // Record history
1153        if let Some(serial) = self.get(id)? {
1154            let history_id = Uuid::new_v4();
1155            conn.execute(
1156                "INSERT INTO serial_history (
1157                    id, serial_id, event_type, from_status, to_status, created_at
1158                ) VALUES (?, ?, ?, ?, ?, ?)",
1159                params![
1160                    history_id.to_string(),
1161                    id.to_string(),
1162                    SerialEventType::Activated.to_string(),
1163                    serial.status.to_string(),
1164                    serial.status.to_string(),
1165                    now,
1166                ],
1167            )
1168            .map_err(map_db_error)?;
1169        }
1170
1171        self.get(id)?.ok_or(CommerceError::NotFound)
1172    }
1173
1174    fn quarantine(&self, id: Uuid, reason: &str) -> stateset_core::Result<SerialNumber> {
1175        let mut conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1176        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1177        let now = Utc::now().to_rfc3339();
1178
1179        // Get current serial
1180        let serial: SerialNumber = tx
1181            .query_row(
1182                "SELECT * FROM serial_numbers WHERE id = ?",
1183                params![id.to_string()],
1184                Self::map_serial_row,
1185            )
1186            .map_err(map_db_error)?;
1187
1188        // Update serial
1189        tx.execute(
1190            "UPDATE serial_numbers SET status = ?, updated_at = ? WHERE id = ?",
1191            params![SerialStatus::Quarantined.to_string(), now, id.to_string()],
1192        )
1193        .map_err(map_db_error)?;
1194
1195        // Record history
1196        let history_id = Uuid::new_v4();
1197        tx.execute(
1198            "INSERT INTO serial_history (
1199                id, serial_id, event_type, from_status, to_status, notes, created_at
1200            ) VALUES (?, ?, ?, ?, ?, ?, ?)",
1201            params![
1202                history_id.to_string(),
1203                id.to_string(),
1204                SerialEventType::Quarantined.to_string(),
1205                serial.status.to_string(),
1206                SerialStatus::Quarantined.to_string(),
1207                reason,
1208                now,
1209            ],
1210        )
1211        .map_err(map_db_error)?;
1212
1213        tx.commit().map_err(map_db_error)?;
1214        self.get(id)?.ok_or(CommerceError::NotFound)
1215    }
1216
1217    fn release_quarantine(&self, id: Uuid) -> stateset_core::Result<SerialNumber> {
1218        let mut conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1219        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1220        let now = Utc::now().to_rfc3339();
1221
1222        // Get current serial
1223        let serial: SerialNumber = tx
1224            .query_row(
1225                "SELECT * FROM serial_numbers WHERE id = ?",
1226                params![id.to_string()],
1227                Self::map_serial_row,
1228            )
1229            .map_err(map_db_error)?;
1230
1231        if serial.status != SerialStatus::Quarantined {
1232            return Err(CommerceError::ValidationError("Serial is not quarantined".to_string()));
1233        }
1234
1235        // Update serial
1236        tx.execute(
1237            "UPDATE serial_numbers SET status = ?, updated_at = ? WHERE id = ?",
1238            params![SerialStatus::Available.to_string(), now, id.to_string()],
1239        )
1240        .map_err(map_db_error)?;
1241
1242        // Record history
1243        let history_id = Uuid::new_v4();
1244        tx.execute(
1245            "INSERT INTO serial_history (
1246                id, serial_id, event_type, from_status, to_status, created_at
1247            ) VALUES (?, ?, ?, ?, ?, ?)",
1248            params![
1249                history_id.to_string(),
1250                id.to_string(),
1251                SerialEventType::QuarantineReleased.to_string(),
1252                SerialStatus::Quarantined.to_string(),
1253                SerialStatus::Available.to_string(),
1254                now,
1255            ],
1256        )
1257        .map_err(map_db_error)?;
1258
1259        tx.commit().map_err(map_db_error)?;
1260        self.get(id)?.ok_or(CommerceError::NotFound)
1261    }
1262
1263    fn scrap(&self, id: Uuid, reason: &str) -> stateset_core::Result<SerialNumber> {
1264        let mut conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1265        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1266        let now = Utc::now().to_rfc3339();
1267
1268        // Get current serial
1269        let serial: SerialNumber = tx
1270            .query_row(
1271                "SELECT * FROM serial_numbers WHERE id = ?",
1272                params![id.to_string()],
1273                Self::map_serial_row,
1274            )
1275            .map_err(map_db_error)?;
1276
1277        if !serial.can_scrap() {
1278            return Err(CommerceError::ValidationError(
1279                "Serial cannot be scrapped in current state".to_string(),
1280            ));
1281        }
1282
1283        // Update serial
1284        tx.execute(
1285            "UPDATE serial_numbers SET status = ?, updated_at = ? WHERE id = ?",
1286            params![SerialStatus::Scrapped.to_string(), now, id.to_string()],
1287        )
1288        .map_err(map_db_error)?;
1289
1290        // Record history
1291        let history_id = Uuid::new_v4();
1292        tx.execute(
1293            "INSERT INTO serial_history (
1294                id, serial_id, event_type, from_status, to_status, notes, created_at
1295            ) VALUES (?, ?, ?, ?, ?, ?, ?)",
1296            params![
1297                history_id.to_string(),
1298                id.to_string(),
1299                SerialEventType::Scrapped.to_string(),
1300                serial.status.to_string(),
1301                SerialStatus::Scrapped.to_string(),
1302                reason,
1303                now,
1304            ],
1305        )
1306        .map_err(map_db_error)?;
1307
1308        tx.commit().map_err(map_db_error)?;
1309        self.get(id)?.ok_or(CommerceError::NotFound)
1310    }
1311
1312    fn get_history(
1313        &self,
1314        serial_id: Uuid,
1315        filter: SerialHistoryFilter,
1316    ) -> stateset_core::Result<Vec<SerialHistory>> {
1317        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1318
1319        let mut conditions = vec!["serial_id = ?".to_string()];
1320        let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(serial_id.to_string())];
1321
1322        if let Some(event_type) = &filter.event_type {
1323            conditions.push("event_type = ?".to_string());
1324            params.push(Box::new(event_type.to_string()));
1325        }
1326        if let Some(ref_type) = &filter.reference_type {
1327            conditions.push("reference_type = ?".to_string());
1328            params.push(Box::new(ref_type.clone()));
1329        }
1330        if let Some(from_date) = &filter.from_date {
1331            conditions.push("created_at >= ?".to_string());
1332            params.push(Box::new(from_date.to_rfc3339()));
1333        }
1334        if let Some(to_date) = &filter.to_date {
1335            conditions.push("created_at <= ?".to_string());
1336            params.push(Box::new(to_date.to_rfc3339()));
1337        }
1338
1339        let limit = filter.limit.unwrap_or(100);
1340        let offset = filter.offset.unwrap_or(0);
1341
1342        let sql = format!(
1343            "SELECT * FROM serial_history WHERE {} ORDER BY created_at DESC LIMIT ? OFFSET ?",
1344            conditions.join(" AND ")
1345        );
1346
1347        params.push(Box::new(i64::from(limit)));
1348        params.push(Box::new(i64::from(offset)));
1349
1350        let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
1351        let rows = stmt
1352            .query_map(
1353                rusqlite::params_from_iter(params.iter().map(std::convert::AsRef::as_ref)),
1354                Self::map_history_row,
1355            )
1356            .map_err(map_db_error)?;
1357
1358        let mut history = Vec::new();
1359        for row in rows {
1360            history.push(row.map_err(map_db_error)?);
1361        }
1362
1363        Ok(history)
1364    }
1365
1366    fn lookup(&self, serial: &str) -> stateset_core::Result<Option<SerialLookupResult>> {
1367        let serial_number = match self.get_by_serial(serial)? {
1368            Some(s) => s,
1369            None => return Ok(None),
1370        };
1371
1372        // Get recent history
1373        let recent_history = self.get_history(
1374            serial_number.id,
1375            SerialHistoryFilter { limit: Some(10), ..Default::default() },
1376        )?;
1377
1378        let product_name = {
1379            let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1380            let result: rusqlite::Result<String> = conn.query_row(
1381                "SELECT COALESCE(p.name, v.name)
1382                 FROM product_variants v
1383                 LEFT JOIN products p ON p.id = v.product_id
1384                 WHERE v.sku = ?",
1385                [serial_number.sku.as_str()],
1386                |row| row.get(0),
1387            );
1388            match result {
1389                Ok(name) => Some(name),
1390                Err(rusqlite::Error::QueryReturnedNoRows) => None,
1391                Err(e) => return Err(map_db_error(e)),
1392            }
1393        };
1394
1395        let lot = {
1396            let lot_repo = SqliteLotRepository::new(self.pool.clone());
1397            match (serial_number.lot_id, serial_number.lot_number.as_deref()) {
1398                (Some(lot_id), lot_number) => match lot_repo.get(lot_id)? {
1399                    Some(lot) => Some(lot),
1400                    None => match lot_number {
1401                        Some(number) => lot_repo.get_by_number(number)?,
1402                        None => None,
1403                    },
1404                },
1405                (None, Some(lot_number)) => lot_repo.get_by_number(lot_number)?,
1406                (None, None) => None,
1407            }
1408        };
1409
1410        let warranty_status = if let Some(warranty_id) = serial_number.warranty_id {
1411            let warranty_repo = SqliteWarrantyRepository::new(self.pool.clone());
1412            match warranty_repo.get(WarrantyId::from(warranty_id))? {
1413                Some(warranty) => Some(WarrantyLookupStatus {
1414                    warranty_id,
1415                    is_active: warranty.is_valid(),
1416                    expires_at: warranty.end_date,
1417                    coverage_type: Some(warranty.warranty_type.to_string()),
1418                }),
1419                None => None,
1420            }
1421        } else {
1422            None
1423        };
1424
1425        Ok(Some(SerialLookupResult {
1426            serial: serial_number,
1427            product_name,
1428            lot,
1429            warranty_status,
1430            recent_history,
1431        }))
1432    }
1433
1434    fn validate(&self, serial: &str) -> stateset_core::Result<SerialValidation> {
1435        match self.get_by_serial(serial)? {
1436            Some(s) => Ok(SerialValidation {
1437                is_valid: true,
1438                serial_id: Some(s.id),
1439                status: Some(s.status),
1440                sku: Some(s.sku),
1441                error_message: None,
1442            }),
1443            None => Ok(SerialValidation {
1444                is_valid: false,
1445                serial_id: None,
1446                status: None,
1447                sku: None,
1448                error_message: Some("Serial number not found".to_string()),
1449            }),
1450        }
1451    }
1452
1453    fn get_available_for_sku(
1454        &self,
1455        sku: &str,
1456        limit: u32,
1457    ) -> stateset_core::Result<Vec<SerialNumber>> {
1458        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1459
1460        // Allocate the OLDEST available serial first (FIFO), matching Postgres.
1461        // This deliberately orders ASC, unlike `list`'s newest-first (DESC) view.
1462        let mut stmt = conn
1463            .prepare(
1464                "SELECT * FROM serial_numbers WHERE sku = ? AND status = ? \
1465                 ORDER BY created_at ASC LIMIT ?",
1466            )
1467            .map_err(map_db_error)?;
1468        let rows = stmt
1469            .query_map(
1470                params![sku, SerialStatus::Available.to_string(), i64::from(limit)],
1471                Self::map_serial_row,
1472            )
1473            .map_err(map_db_error)?;
1474
1475        let mut serials = Vec::new();
1476        for row in rows {
1477            serials.push(row.map_err(map_db_error)?);
1478        }
1479        Ok(serials)
1480    }
1481
1482    fn get_for_lot(&self, lot_id: Uuid) -> stateset_core::Result<Vec<SerialNumber>> {
1483        self.list(SerialFilter { lot_id: Some(lot_id), ..Default::default() })
1484    }
1485
1486    fn get_for_customer(&self, customer_id: Uuid) -> stateset_core::Result<Vec<SerialNumber>> {
1487        self.list(SerialFilter {
1488            owner_id: Some(customer_id),
1489            owner_type: Some("customer".to_string()),
1490            ..Default::default()
1491        })
1492    }
1493
1494    fn count(&self, filter: SerialFilter) -> stateset_core::Result<u64> {
1495        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1496
1497        // Reuse the same conditions as `list` so the two never diverge.
1498        let (conditions, params) = Self::serial_filter_conditions(&filter);
1499
1500        let where_clause = if conditions.is_empty() {
1501            String::new()
1502        } else {
1503            format!("WHERE {}", conditions.join(" AND "))
1504        };
1505
1506        let sql = format!("SELECT COUNT(*) FROM serial_numbers {where_clause}");
1507
1508        let count: i64 = conn
1509            .query_row(
1510                &sql,
1511                rusqlite::params_from_iter(params.iter().map(std::convert::AsRef::as_ref)),
1512                |row| row.get(0),
1513            )
1514            .map_err(map_db_error)?;
1515
1516        Ok(count as u64)
1517    }
1518
1519    fn create_batch(
1520        &self,
1521        inputs: Vec<CreateSerialNumber>,
1522    ) -> stateset_core::Result<BatchResult<SerialNumber>> {
1523        let mut result = BatchResult::with_capacity(inputs.len());
1524
1525        for (idx, input) in inputs.into_iter().enumerate() {
1526            match self.create(input) {
1527                Ok(serial) => result.record_success(serial),
1528                Err(e) => result.record_failure(idx, None, &e),
1529            }
1530        }
1531
1532        Ok(result)
1533    }
1534
1535    fn get_batch(&self, ids: Vec<Uuid>) -> stateset_core::Result<Vec<SerialNumber>> {
1536        if ids.is_empty() {
1537            return Ok(Vec::new());
1538        }
1539
1540        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1541        let placeholders = build_in_clause(ids.len());
1542        let params = uuid_params(&ids);
1543        let params_ref = params_refs(&params);
1544
1545        let sql = format!("SELECT * FROM serial_numbers WHERE id IN ({placeholders})");
1546
1547        let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
1548        let rows = stmt
1549            .query_map(rusqlite::params_from_iter(params_ref), Self::map_serial_row)
1550            .map_err(map_db_error)?;
1551
1552        let mut serials = Vec::new();
1553        for row in rows {
1554            serials.push(row.map_err(map_db_error)?);
1555        }
1556
1557        Ok(serials)
1558    }
1559
1560    fn get_batch_by_serial(
1561        &self,
1562        serials: Vec<String>,
1563    ) -> stateset_core::Result<Vec<SerialNumber>> {
1564        if serials.is_empty() {
1565            return Ok(Vec::new());
1566        }
1567
1568        let conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1569        let placeholders = build_in_clause(serials.len());
1570        let params = string_params(&serials);
1571        let params_ref = params_refs(&params);
1572
1573        let sql = format!("SELECT * FROM serial_numbers WHERE serial IN ({placeholders})");
1574
1575        let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
1576        let rows = stmt
1577            .query_map(rusqlite::params_from_iter(params_ref), Self::map_serial_row)
1578            .map_err(map_db_error)?;
1579
1580        let mut result = Vec::new();
1581        for row in rows {
1582            result.push(row.map_err(map_db_error)?);
1583        }
1584
1585        Ok(result)
1586    }
1587}
1588
1589#[cfg(test)]
1590mod tests {
1591    use super::*;
1592    use crate::SqliteDatabase;
1593    use stateset_core::{
1594        ChangeSerialStatus, CreateSerialNumber, CreateSerialNumbersBulk, ReserveSerialNumber,
1595        SerialFilter, SerialRepository, SerialStatus, UpdateSerialNumber,
1596    };
1597
1598    fn fresh_repo() -> SqliteSerialRepository {
1599        SqliteDatabase::in_memory().expect("in-memory").serials()
1600    }
1601
1602    fn make_serial(repo: &SqliteSerialRepository, sku: &str, serial: &str) -> SerialNumber {
1603        repo.create(CreateSerialNumber {
1604            serial: Some(serial.into()),
1605            sku: sku.into(),
1606            lot_id: None,
1607            lot_number: Some("LOT-1".into()),
1608            location_id: Some(1),
1609            manufactured_at: None,
1610            notes: None,
1611            attributes: None,
1612        })
1613        .expect("create")
1614    }
1615
1616    #[test]
1617    fn create_with_explicit_serial_starts_available() {
1618        let repo = fresh_repo();
1619        let s = make_serial(&repo, "SKU-A", "S-001");
1620        assert_eq!(s.serial, "S-001");
1621        assert_eq!(s.sku, "SKU-A");
1622        assert_eq!(s.status, SerialStatus::Available);
1623    }
1624
1625    #[test]
1626    fn delete_rejects_missing_and_non_available_serials() {
1627        let repo = fresh_repo();
1628
1629        // A non-existent serial is NotFound, not a silent success.
1630        let err = repo.delete(Uuid::new_v4()).expect_err("missing serial must error");
1631        assert!(matches!(err, CommerceError::NotFound), "got {err:?}");
1632
1633        // A non-Available serial cannot be deleted, and must survive the attempt.
1634        let s = make_serial(&repo, "SKU-DEL", "S-DEL-1");
1635        repo.update(
1636            s.id,
1637            UpdateSerialNumber { status: Some(SerialStatus::Sold), ..Default::default() },
1638        )
1639        .expect("update to sold");
1640        let err = repo.delete(s.id).expect_err("a sold serial must not be deletable");
1641        assert!(matches!(err, CommerceError::ValidationError(_)), "got {err:?}");
1642        assert!(
1643            repo.get(s.id).expect("get").is_some(),
1644            "a rejected delete must not remove the serial"
1645        );
1646    }
1647
1648    #[test]
1649    fn count_matches_list_for_all_filters() {
1650        let repo = fresh_repo();
1651        make_serial(&repo, "SKU", "S-1");
1652        make_serial(&repo, "SKU", "S-2");
1653
1654        // Every predicate `list` honors, `count` must honor too — `count(f)` must
1655        // equal `list(f).len()` for the same filter (they previously diverged
1656        // because `count` only applied sku/status/lot_id).
1657        let filters = [
1658            SerialFilter { serial: Some("S-1".into()), ..Default::default() },
1659            SerialFilter { serial_prefix: Some("S-1".into()), ..Default::default() },
1660            SerialFilter { statuses: Some(vec![SerialStatus::Sold]), ..Default::default() },
1661            SerialFilter { sku: Some("SKU".into()), ..Default::default() },
1662        ];
1663        for filter in filters {
1664            let listed = repo.list(filter.clone()).expect("list").len() as u64;
1665            let counted = repo.count(filter).expect("count");
1666            assert_eq!(counted, listed, "count must match list for the same filter");
1667        }
1668
1669        // Sanity: the distinguishing filters select the right subset.
1670        assert_eq!(
1671            repo.count(SerialFilter { serial: Some("S-1".into()), ..Default::default() }).unwrap(),
1672            1
1673        );
1674        assert_eq!(
1675            repo.count(SerialFilter {
1676                statuses: Some(vec![SerialStatus::Sold]),
1677                ..Default::default()
1678            })
1679            .unwrap(),
1680            0
1681        );
1682        assert_eq!(
1683            repo.count(SerialFilter { sku: Some("SKU".into()), ..Default::default() }).unwrap(),
1684            2
1685        );
1686    }
1687
1688    #[test]
1689    fn get_available_for_sku_allocates_oldest_first_fifo() {
1690        let repo = fresh_repo();
1691        let oldest = make_serial(&repo, "SKU-F", "S-OLD");
1692        let newest = make_serial(&repo, "SKU-F", "S-NEW");
1693
1694        // FIFO: the oldest available serial is allocated first (matching Postgres,
1695        // which orders by created_at ASC). SQLite used to inherit list's DESC order
1696        // and hand out the newest unit.
1697        let one = repo.get_available_for_sku("SKU-F", 1).expect("available");
1698        assert_eq!(one.len(), 1);
1699        assert_eq!(one[0].id, oldest.id, "must allocate the oldest serial first (FIFO)");
1700
1701        // The full set is ordered oldest → newest.
1702        let all = repo.get_available_for_sku("SKU-F", 10).expect("all");
1703        assert_eq!(
1704            all.iter().map(|s| s.id).collect::<Vec<_>>(),
1705            vec![oldest.id, newest.id],
1706            "available serials must be FIFO-ordered"
1707        );
1708    }
1709
1710    #[test]
1711    fn create_with_no_serial_generates_unique_one() {
1712        let repo = fresh_repo();
1713        let s1 = repo
1714            .create(CreateSerialNumber {
1715                serial: None,
1716                sku: "SKU-X".into(),
1717                lot_id: None,
1718                lot_number: None,
1719                location_id: None,
1720                manufactured_at: None,
1721                notes: None,
1722                attributes: None,
1723            })
1724            .expect("c1");
1725        let s2 = repo
1726            .create(CreateSerialNumber {
1727                serial: None,
1728                sku: "SKU-X".into(),
1729                lot_id: None,
1730                lot_number: None,
1731                location_id: None,
1732                manufactured_at: None,
1733                notes: None,
1734                attributes: None,
1735            })
1736            .expect("c2");
1737        assert_ne!(s1.serial, s2.serial);
1738    }
1739
1740    #[test]
1741    fn get_and_get_by_serial_round_trip() {
1742        let repo = fresh_repo();
1743        let s = make_serial(&repo, "SKU-RT", "S-RT-1");
1744        let by_id = repo.get(s.id).expect("ok").expect("found");
1745        assert_eq!(by_id.id, s.id);
1746        let by_serial = repo.get_by_serial("S-RT-1").expect("ok").expect("found");
1747        assert_eq!(by_serial.id, s.id);
1748        assert!(repo.get_by_serial("missing").expect("ok").is_none());
1749    }
1750
1751    #[test]
1752    fn create_bulk_creates_n_serials_with_prefix() {
1753        let repo = fresh_repo();
1754        let serials = repo
1755            .create_bulk(CreateSerialNumbersBulk {
1756                sku: "SKU-BULK".into(),
1757                quantity: 5,
1758                prefix: Some("BLK".into()),
1759                lot_id: None,
1760                lot_number: None,
1761                location_id: Some(1),
1762                manufactured_at: None,
1763            })
1764            .expect("bulk");
1765        assert_eq!(serials.len(), 5);
1766        for (i, s) in serials.iter().enumerate() {
1767            assert_eq!(s.serial, format!("BLK-{:06}", i + 1));
1768            assert_eq!(s.status, SerialStatus::Available);
1769        }
1770    }
1771
1772    #[test]
1773    fn list_filters_by_sku() {
1774        let repo = fresh_repo();
1775        make_serial(&repo, "SKU-L1", "S-L1-1");
1776        make_serial(&repo, "SKU-L1", "S-L1-2");
1777        make_serial(&repo, "SKU-L2", "S-L2-1");
1778
1779        let l1 = repo
1780            .list(SerialFilter { sku: Some("SKU-L1".into()), ..Default::default() })
1781            .expect("list");
1782        assert_eq!(l1.len(), 2);
1783    }
1784
1785    #[test]
1786    fn list_filters_by_status() {
1787        let repo = fresh_repo();
1788        let s_available = make_serial(&repo, "SKU-S", "S-AV");
1789        let s_to_reserve = make_serial(&repo, "SKU-S", "S-TR");
1790        repo.change_status(ChangeSerialStatus {
1791            serial_id: s_to_reserve.id,
1792            new_status: SerialStatus::Reserved,
1793            ..Default::default()
1794        })
1795        .expect("change status");
1796
1797        let available = repo
1798            .list(SerialFilter { status: Some(SerialStatus::Available), ..Default::default() })
1799            .expect("list available");
1800        let reserved = repo
1801            .list(SerialFilter { status: Some(SerialStatus::Reserved), ..Default::default() })
1802            .expect("list reserved");
1803        let av_ids: Vec<_> = available.iter().map(|s| s.id).collect();
1804        let res_ids: Vec<_> = reserved.iter().map(|s| s.id).collect();
1805        assert!(av_ids.contains(&s_available.id));
1806        assert!(res_ids.contains(&s_to_reserve.id));
1807    }
1808
1809    #[test]
1810    fn change_status_transitions() {
1811        let repo = fresh_repo();
1812        let s = make_serial(&repo, "SKU-T", "S-T-1");
1813        let updated = repo
1814            .change_status(ChangeSerialStatus {
1815                serial_id: s.id,
1816                new_status: SerialStatus::Sold,
1817                performed_by: Some("alice".into()),
1818                ..Default::default()
1819            })
1820            .expect("change");
1821        assert_eq!(updated.status, SerialStatus::Sold);
1822    }
1823
1824    #[test]
1825    fn reserve_serial_creates_reservation_and_changes_status() {
1826        let repo = fresh_repo();
1827        let s = make_serial(&repo, "SKU-R", "S-R-1");
1828        let order_id = Uuid::new_v4();
1829        let res = repo
1830            .reserve(ReserveSerialNumber {
1831                serial_id: s.id,
1832                reference_type: "order".into(),
1833                reference_id: order_id,
1834                reserved_by: Some("alice".into()),
1835                expires_in_seconds: Some(60),
1836            })
1837            .expect("reserve");
1838        assert_eq!(res.serial_id, s.id);
1839        let after = repo.get(s.id).expect("ok").expect("found");
1840        assert_eq!(after.status, SerialStatus::Reserved);
1841
1842        repo.release_reservation(res.id).expect("release");
1843        let after_release = repo.get(s.id).expect("ok").expect("found");
1844        assert_eq!(after_release.status, SerialStatus::Available);
1845    }
1846
1847    #[test]
1848    fn quarantine_marks_quarantined_then_release() {
1849        let repo = fresh_repo();
1850        let s = make_serial(&repo, "SKU-Q", "S-Q-1");
1851        let q = repo.quarantine(s.id, "qc fail").expect("quarantine");
1852        assert_eq!(q.status, SerialStatus::Quarantined);
1853        let r = repo.release_quarantine(s.id).expect("release");
1854        assert_eq!(r.status, SerialStatus::Available);
1855    }
1856
1857    #[test]
1858    fn get_for_customer_filters_by_owner() {
1859        let repo = fresh_repo();
1860        let s = make_serial(&repo, "SKU-O", "S-O-1");
1861        let cust = Uuid::new_v4();
1862        repo.change_status(ChangeSerialStatus {
1863            serial_id: s.id,
1864            new_status: SerialStatus::Sold,
1865            owner_id: Some(cust),
1866            owner_type: Some("customer".into()),
1867            ..Default::default()
1868        })
1869        .expect("change");
1870
1871        let owned = repo.get_for_customer(cust).expect("ok");
1872        let ids: Vec<_> = owned.iter().map(|s| s.id).collect();
1873        assert!(ids.contains(&s.id));
1874
1875        let none = repo.get_for_customer(Uuid::new_v4()).expect("ok");
1876        assert!(none.is_empty());
1877    }
1878
1879    #[test]
1880    fn get_available_for_sku_returns_only_available() {
1881        let repo = fresh_repo();
1882        make_serial(&repo, "SKU-AV", "S-AV-1");
1883        let s2 = make_serial(&repo, "SKU-AV", "S-AV-2");
1884        repo.change_status(ChangeSerialStatus {
1885            serial_id: s2.id,
1886            new_status: SerialStatus::Sold,
1887            ..Default::default()
1888        })
1889        .expect("change");
1890
1891        let available = repo.get_available_for_sku("SKU-AV", 10).expect("ok");
1892        assert_eq!(available.len(), 1);
1893        assert_eq!(available[0].status, SerialStatus::Available);
1894    }
1895
1896    #[test]
1897    fn get_for_unknown_id_returns_none() {
1898        let repo = fresh_repo();
1899        assert!(repo.get(Uuid::new_v4()).expect("ok").is_none());
1900    }
1901
1902    #[test]
1903    fn get_batch_returns_only_existing() {
1904        let repo = fresh_repo();
1905        let s1 = make_serial(&repo, "SKU-B", "S-B-1");
1906        let s2 = make_serial(&repo, "SKU-B", "S-B-2");
1907        let stranger = Uuid::new_v4();
1908
1909        let batch = repo.get_batch(vec![s1.id, s2.id, stranger]).expect("batch");
1910        assert_eq!(batch.len(), 2);
1911    }
1912
1913    #[test]
1914    fn create_batch_returns_per_input_results() {
1915        let repo = fresh_repo();
1916        let result = repo
1917            .create_batch(vec![
1918                CreateSerialNumber {
1919                    serial: Some("S-CB-1".into()),
1920                    sku: "SKU-CB".into(),
1921                    lot_id: None,
1922                    lot_number: None,
1923                    location_id: None,
1924                    manufactured_at: None,
1925                    notes: None,
1926                    attributes: None,
1927                },
1928                CreateSerialNumber {
1929                    serial: Some("S-CB-2".into()),
1930                    sku: "SKU-CB".into(),
1931                    lot_id: None,
1932                    lot_number: None,
1933                    location_id: None,
1934                    manufactured_at: None,
1935                    notes: None,
1936                    attributes: None,
1937                },
1938            ])
1939            .expect("batch");
1940        assert_eq!(result.success_count, 2);
1941        assert_eq!(result.failure_count, 0);
1942    }
1943}