Skip to main content

stateset_db/sqlite/
orders.rs

1//! SQLite order repository implementation
2
3use super::{
4    backorder::{cancel_backorders_for_order_in_tx, create_backorder_in_tx},
5    build_in_clause,
6    inventory::{ReservationConfirmOutcome, SqliteInventoryRepository},
7    map_db_error, params_refs, parse_datetime_row, parse_decimal_row, parse_enum, parse_enum_row,
8    parse_json_opt_row, parse_uuid_row, sum_decimal_query, uuid_params, with_immediate_transaction,
9};
10use chrono::Utc;
11use r2d2::Pool;
12use r2d2_sqlite::SqliteConnectionManager;
13use rust_decimal::Decimal;
14use stateset_core::{
15    Address, BatchResult, CommerceError, CreateBackorder, CreateOrder, CreateOrderItem, CustomerId,
16    FulfillmentStatus, Order, OrderFilter, OrderId, OrderItem, OrderItemId, OrderRepository,
17    OrderStatus, PaymentStatus, ProductId, ReserveInventory, Result, UpdateOrder,
18    validate_batch_size, validate_currency_code, validate_postal_code, validate_price,
19    validate_required_text, validate_required_uuid, validate_sku,
20};
21use uuid::Uuid;
22
23/// SQLite implementation of `OrderRepository`
24#[derive(Debug)]
25pub struct SqliteOrderRepository {
26    pool: Pool<SqliteConnectionManager>,
27}
28
29impl SqliteOrderRepository {
30    #[must_use]
31    pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
32        Self { pool }
33    }
34
35    fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
36        self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))
37    }
38
39    fn generate_order_number() -> String {
40        use std::sync::atomic::{AtomicU64, Ordering};
41        static COUNTER: AtomicU64 = AtomicU64::new(0);
42        let now = Utc::now();
43        let timestamp = now.timestamp();
44        let nanos = now.timestamp_subsec_nanos();
45        // Monotonic counter is cheaper than Uuid::new_v4() and still unique per process
46        let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
47        format!("ORD-{}-{:06}-{:08X}", timestamp, nanos / 1000, seq as u32)
48    }
49
50    fn row_to_order(row: &rusqlite::Row<'_>) -> rusqlite::Result<Order> {
51        let shipping_addr: Option<Address> = parse_json_opt_row(
52            row.get::<_, Option<String>>("shipping_address")?,
53            "order",
54            "shipping_address",
55        )?;
56        let billing_addr: Option<Address> = parse_json_opt_row(
57            row.get::<_, Option<String>>("billing_address")?,
58            "order",
59            "billing_address",
60        )?;
61
62        Ok(Order {
63            id: OrderId::from(parse_uuid_row(&row.get::<_, String>("id")?, "order", "id")?),
64            order_number: row.get("order_number")?,
65            customer_id: CustomerId::from(parse_uuid_row(
66                &row.get::<_, String>("customer_id")?,
67                "order",
68                "customer_id",
69            )?),
70            status: parse_enum_row(&row.get::<_, String>("status")?, "order", "status")?,
71            order_date: parse_datetime_row(
72                &row.get::<_, String>("order_date")?,
73                "order",
74                "order_date",
75            )?,
76            total_amount: parse_decimal_row(
77                &row.get::<_, String>("total_amount")?,
78                "order",
79                "total_amount",
80            )?,
81            currency: row.get("currency")?,
82            payment_status: parse_enum_row(
83                &row.get::<_, String>("payment_status")?,
84                "order",
85                "payment_status",
86            )?,
87            fulfillment_status: parse_enum_row(
88                &row.get::<_, String>("fulfillment_status")?,
89                "order",
90                "fulfillment_status",
91            )?,
92            payment_method: row.get("payment_method")?,
93            shipping_method: row.get("shipping_method")?,
94            tracking_number: row.get("tracking_number")?,
95            notes: row.get("notes")?,
96            shipping_address: shipping_addr,
97            billing_address: billing_addr,
98            items: vec![], // Loaded separately
99            version: row.get::<_, Option<i32>>("version")?.unwrap_or(1),
100            created_at: parse_datetime_row(
101                &row.get::<_, String>("created_at")?,
102                "order",
103                "created_at",
104            )?,
105            updated_at: parse_datetime_row(
106                &row.get::<_, String>("updated_at")?,
107                "order",
108                "updated_at",
109            )?,
110        })
111    }
112
113    fn validate_order_item_input(item: &CreateOrderItem) -> Result<()> {
114        validate_required_uuid("order_item.product_id", item.product_id.into_uuid())?;
115        if let Some(variant_id) = item.variant_id {
116            validate_required_uuid("order_item.variant_id", variant_id)?;
117        }
118        validate_sku(&item.sku)?;
119        validate_required_text("order_item.name", &item.name, 255)?;
120
121        if item.quantity <= 0 {
122            return Err(CommerceError::InvalidInput {
123                field: "order_item.quantity".to_string(),
124                message: "must be greater than zero".into(),
125            });
126        }
127
128        validate_price(item.unit_price)?;
129        if let Some(discount) = item.discount {
130            validate_price(discount)?;
131        }
132        if let Some(tax) = item.tax_amount {
133            validate_price(tax)?;
134        }
135
136        let subtotal = item.unit_price * Decimal::from(item.quantity);
137        let discount = item.discount.unwrap_or_default();
138        let tax = item.tax_amount.unwrap_or_default();
139        if discount > subtotal {
140            return Err(CommerceError::ValidationError(
141                "Order item discount cannot exceed subtotal".into(),
142            ));
143        }
144
145        let total = subtotal - discount + tax;
146        if total < Decimal::ZERO {
147            return Err(CommerceError::ValidationError(
148                "Order item total cannot be negative".into(),
149            ));
150        }
151
152        Ok(())
153    }
154
155    fn validate_address_input(address: &Address, field_prefix: &str) -> Result<()> {
156        validate_required_text(&format!("{field_prefix}.line1"), &address.line1, 255)?;
157        validate_required_text(&format!("{field_prefix}.city"), &address.city, 255)?;
158        validate_postal_code(&address.postal_code)?;
159        validate_required_text(&format!("{field_prefix}.country"), &address.country, 64)?;
160
161        if let Some(line2) = &address.line2 {
162            validate_required_text(&format!("{field_prefix}.line2"), line2, 255)?;
163        }
164        if let Some(state) = &address.state {
165            validate_required_text(&format!("{field_prefix}.state"), state, 64)?;
166        }
167
168        Ok(())
169    }
170
171    fn validate_order_input(input: &CreateOrder) -> Result<()> {
172        validate_required_uuid("order.customer_id", input.customer_id.into_uuid())?;
173
174        if let Some(ref currency) = input.currency {
175            validate_currency_code(currency.as_str())?;
176        }
177
178        if input.items.is_empty() {
179            return Err(CommerceError::ValidationError("Order must have at least one item".into()));
180        }
181
182        for item in &input.items {
183            Self::validate_order_item_input(item)?;
184        }
185
186        if let Some(address) = &input.shipping_address {
187            Self::validate_address_input(address, "order.shipping_address")?;
188        }
189        if let Some(address) = &input.billing_address {
190            Self::validate_address_input(address, "order.billing_address")?;
191        }
192
193        Ok(())
194    }
195
196    fn load_order_items_with_conn(
197        conn: &rusqlite::Connection,
198        order_id: OrderId,
199    ) -> Result<Vec<OrderItem>> {
200        let mut stmt = conn
201            .prepare(
202                "SELECT id, order_id, product_id, variant_id, sku, name, quantity,
203                        unit_price, discount, tax_amount, total
204                 FROM order_items WHERE order_id = ?",
205            )
206            .map_err(map_db_error)?;
207
208        let items = stmt
209            .query_map([order_id.to_string()], Self::row_to_order_item)
210            .map_err(map_db_error)?
211            .collect::<rusqlite::Result<Vec<_>>>()
212            .map_err(map_db_error)?;
213
214        Ok(items)
215    }
216
217    fn row_to_order_item(row: &rusqlite::Row<'_>) -> rusqlite::Result<OrderItem> {
218        Ok(OrderItem {
219            id: OrderItemId::from(parse_uuid_row(
220                &row.get::<_, String>("id")?,
221                "order_item",
222                "id",
223            )?),
224            order_id: OrderId::from(parse_uuid_row(
225                &row.get::<_, String>("order_id")?,
226                "order_item",
227                "order_id",
228            )?),
229            product_id: ProductId::from(parse_uuid_row(
230                &row.get::<_, String>("product_id")?,
231                "order_item",
232                "product_id",
233            )?),
234            variant_id: row.get::<_, Option<String>>("variant_id")?.and_then(|s| s.parse().ok()),
235            sku: row.get("sku")?,
236            name: row.get("name")?,
237            quantity: row.get("quantity")?,
238            unit_price: parse_decimal_row(
239                &row.get::<_, String>("unit_price")?,
240                "order_item",
241                "unit_price",
242            )?,
243            discount: parse_decimal_row(
244                &row.get::<_, String>("discount")?,
245                "order_item",
246                "discount",
247            )?,
248            tax_amount: parse_decimal_row(
249                &row.get::<_, String>("tax_amount")?,
250                "order_item",
251                "tax_amount",
252            )?,
253            total: parse_decimal_row(&row.get::<_, String>("total")?, "order_item", "total")?,
254        })
255    }
256
257    fn load_order_items_batch(
258        conn: &rusqlite::Connection,
259        ids: &[OrderId],
260    ) -> Result<std::collections::HashMap<OrderId, Vec<OrderItem>>> {
261        let mut map: std::collections::HashMap<OrderId, Vec<OrderItem>> =
262            std::collections::HashMap::with_capacity(ids.len());
263        for chunk in ids.chunks(500) {
264            let placeholders = build_in_clause(chunk.len());
265            let sql = format!(
266                "SELECT id, order_id, product_id, variant_id, sku, name, quantity,
267                        unit_price, discount, tax_amount, total
268                 FROM order_items WHERE order_id IN ({placeholders})"
269            );
270            let id_strs: Vec<String> = chunk.iter().map(ToString::to_string).collect();
271            let param_refs: Vec<&dyn rusqlite::ToSql> =
272                id_strs.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
273            let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
274            let rows = stmt
275                .query_map(param_refs.as_slice(), Self::row_to_order_item)
276                .map_err(map_db_error)?;
277            for row in rows {
278                let item = row.map_err(map_db_error)?;
279                map.entry(item.order_id).or_default().push(item);
280            }
281        }
282        Ok(map)
283    }
284
285    fn get_by_cart_id_in_conn(
286        conn: &rusqlite::Connection,
287        cart_id: Uuid,
288    ) -> std::result::Result<Option<Order>, rusqlite::Error> {
289        let result = conn.query_row(
290            "SELECT * FROM orders WHERE cart_id = ?",
291            [cart_id.to_string()],
292            Self::row_to_order,
293        );
294
295        match result {
296            Ok(mut order) => {
297                order.items = Self::load_order_items_with_conn(conn, order.id)
298                    .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
299                Ok(Some(order))
300            }
301            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
302            Err(e) => Err(e),
303        }
304    }
305
306    pub fn get_by_cart_id(&self, cart_id: Uuid) -> Result<Option<Order>> {
307        let conn = self.conn()?;
308        Self::get_by_cart_id_in_conn(&conn, cart_id).map_err(map_db_error)
309    }
310
311    fn create_internal_in_tx(
312        tx: &rusqlite::Transaction<'_>,
313        cart_id: Option<Uuid>,
314        idempotent_by_cart_id: bool,
315        input: &CreateOrder,
316    ) -> std::result::Result<Order, rusqlite::Error> {
317        let id = OrderId::new();
318        let order_number = Self::generate_order_number();
319        let now = Utc::now();
320        let currency = input.currency.unwrap_or_default();
321
322        // Pre-compute strings used multiple times to avoid repeated allocation
323        let id_str = id.to_string();
324        let customer_id_str = input.customer_id.to_string();
325        let now_str = now.to_rfc3339();
326
327        // Order total is the sum of the per-line money totals. Each line is
328        // rounded to the currency minor unit by `OrderItem::calculate_total`
329        // (the same helper used to store `order_items.total`), so the order
330        // total foots exactly to its line items and matches `update_order_total`
331        // and the Postgres backend.
332        let total: Decimal = input
333            .items
334            .iter()
335            .map(|item| {
336                OrderItem::calculate_total(
337                    item.quantity,
338                    item.unit_price,
339                    item.discount.unwrap_or_default(),
340                    item.tax_amount.unwrap_or_default(),
341                )
342            })
343            .sum();
344        let total_str = total.to_string();
345
346        let shipping_address_json = input
347            .shipping_address
348            .as_ref()
349            .map(|address| {
350                serde_json::to_string(address).map_err(|error| {
351                    rusqlite::Error::ToSqlConversionFailure(Box::new(CommerceError::DatabaseError(
352                        format!("Failed to serialize order.shipping_address: {error}"),
353                    )))
354                })
355            })
356            .transpose()?;
357        let billing_address_json = input
358            .billing_address
359            .as_ref()
360            .map(|address| {
361                serde_json::to_string(address).map_err(|error| {
362                    rusqlite::Error::ToSqlConversionFailure(Box::new(CommerceError::DatabaseError(
363                        format!("Failed to serialize order.billing_address: {error}"),
364                    )))
365                })
366            })
367            .transpose()?;
368
369        let cart_id_str = if idempotent_by_cart_id {
370            Some(
371                cart_id
372                    .ok_or_else(|| {
373                        rusqlite::Error::ToSqlConversionFailure(Box::new(
374                            CommerceError::ValidationError(
375                                "cart_id is required for cart checkout".into(),
376                            ),
377                        ))
378                    })?
379                    .to_string(),
380            )
381        } else {
382            None
383        };
384
385        let inserted = if idempotent_by_cart_id {
386            let cart_id_str = cart_id_str.as_deref().ok_or_else(|| {
387                rusqlite::Error::ToSqlConversionFailure(Box::new(CommerceError::DatabaseError(
388                    "cart_id was required but missing (internal error)".into(),
389                )))
390            })?;
391            let rows_affected = tx.execute(
392                "INSERT OR IGNORE INTO orders (id, order_number, customer_id, status, order_date, total_amount,
393                                 currency, payment_status, fulfillment_status, payment_method,
394                                 shipping_method, notes, shipping_address, billing_address,
395                                 cart_id, created_at, updated_at)
396             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
397                rusqlite::params![
398                    &id_str,
399                    &order_number,
400                    &customer_id_str,
401                    "pending",
402                    &now_str,
403                    &total_str,
404                    &currency,
405                    "pending",
406                    "unfulfilled",
407                    &input.payment_method,
408                    &input.shipping_method,
409                    &input.notes,
410                    &shipping_address_json,
411                    &billing_address_json,
412                    cart_id_str,
413                    &now_str,
414                    &now_str,
415                ],
416            )?;
417
418            rows_affected > 0
419        } else {
420            tx.prepare_cached(
421                "INSERT INTO orders (id, order_number, customer_id, status, order_date, total_amount,
422                                 currency, payment_status, fulfillment_status, payment_method,
423                                 shipping_method, notes, shipping_address, billing_address,
424                                 created_at, updated_at)
425             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
426            )?.execute(
427                rusqlite::params![
428                    &id_str,
429                    &order_number,
430                    &customer_id_str,
431                    "pending",
432                    &now_str,
433                    &total_str,
434                    &currency,
435                    "pending",
436                    "unfulfilled",
437                    &input.payment_method,
438                    &input.shipping_method,
439                    &input.notes,
440                    &shipping_address_json,
441                    &billing_address_json,
442                    &now_str,
443                    &now_str,
444                ],
445            )?;
446
447            true
448        };
449
450        if !inserted {
451            let cart_id = cart_id.ok_or_else(|| {
452                rusqlite::Error::ToSqlConversionFailure(Box::new(CommerceError::DatabaseError(
453                    "cart_id was required but missing (internal error)".into(),
454                )))
455            })?;
456            let existing = Self::get_by_cart_id_in_conn(tx, cart_id)?;
457            return existing.ok_or_else(|| {
458                rusqlite::Error::ToSqlConversionFailure(Box::new(CommerceError::DatabaseError(
459                    "Order exists for cart_id but could not be loaded".into(),
460                )))
461            });
462        }
463
464        let mut items = Vec::with_capacity(input.items.len());
465        {
466            let mut stmt = tx.prepare_cached(
467                "INSERT INTO order_items (id, order_id, product_id, variant_id, sku, name,
468                                          quantity, unit_price, discount, tax_amount, total)
469                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
470            )?;
471            for item in &input.items {
472                let item_id = OrderItemId::new();
473                let item_total = OrderItem::calculate_total(
474                    item.quantity,
475                    item.unit_price,
476                    item.discount.unwrap_or_default(),
477                    item.tax_amount.unwrap_or_default(),
478                );
479
480                stmt.execute(rusqlite::params![
481                    item_id.to_string(),
482                    &id_str,
483                    item.product_id.to_string(),
484                    item.variant_id.map(|variant_id| variant_id.to_string()),
485                    &item.sku,
486                    &item.name,
487                    item.quantity,
488                    item.unit_price.to_string(),
489                    item.discount.unwrap_or_default().to_string(),
490                    item.tax_amount.unwrap_or_default().to_string(),
491                    item_total.to_string(),
492                ])?;
493
494                items.push(OrderItem {
495                    id: item_id,
496                    order_id: id,
497                    product_id: item.product_id,
498                    variant_id: item.variant_id,
499                    sku: item.sku.clone(),
500                    name: item.name.clone(),
501                    quantity: item.quantity,
502                    unit_price: item.unit_price,
503                    discount: item.discount.unwrap_or_default(),
504                    tax_amount: item.tax_amount.unwrap_or_default(),
505                    total: item_total,
506                });
507            }
508        }
509
510        let reference_id = &id_str;
511        let mut inv_lookup = tx.prepare_cached("SELECT id FROM inventory_items WHERE sku = ?")?;
512        for item in &items {
513            if item.quantity <= 0 {
514                continue;
515            }
516
517            let item_row = inv_lookup.query_row([&item.sku], |row| row.get::<_, i64>(0));
518
519            let item_id = match item_row {
520                Ok(item_id) => item_id,
521                Err(rusqlite::Error::QueryReturnedNoRows) => continue,
522                Err(e) => return Err(e),
523            };
524
525            let available = sum_decimal_query(
526                tx,
527                "SELECT quantity_available FROM inventory_balances WHERE item_id = ?",
528                &[&item_id],
529                "inventory_balance",
530                "quantity_available",
531            )
532            .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
533
534            let requested = Decimal::from(item.quantity);
535            let reserve_qty =
536                if available > Decimal::ZERO { requested.min(available) } else { Decimal::ZERO };
537
538            let mut reserved = Decimal::ZERO;
539            if reserve_qty > Decimal::ZERO {
540                let reserve_input = ReserveInventory {
541                    sku: item.sku.clone(),
542                    location_id: None,
543                    quantity: reserve_qty,
544                    reference_type: "order".to_string(),
545                    reference_id: reference_id.clone(),
546                    expires_in_seconds: None,
547                };
548
549                match SqliteInventoryRepository::reserve_in_tx(tx, &reserve_input) {
550                    Ok(_reservation) => {
551                        reserved = reserve_qty;
552                    }
553                    Err(err) => {
554                        let commerce_err = map_db_error(err);
555                        if matches!(commerce_err, CommerceError::InsufficientStock { .. }) {
556                            reserved = Decimal::ZERO;
557                        } else {
558                            return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
559                                commerce_err,
560                            )));
561                        }
562                    }
563                }
564            }
565
566            let remaining = requested - reserved;
567            if remaining > Decimal::ZERO {
568                let backorder_input = CreateBackorder {
569                    order_id: id.into_uuid(),
570                    order_line_id: Some(item.id.into_uuid()),
571                    customer_id: input.customer_id.into_uuid(),
572                    sku: item.sku.clone(),
573                    quantity: remaining,
574                    priority: None,
575                    expected_date: None,
576                    promised_date: None,
577                    source_location_id: None,
578                    notes: Some("Auto backorder: insufficient stock".to_string()),
579                };
580                create_backorder_in_tx(tx, &backorder_input)?;
581            }
582        }
583
584        Ok(Order {
585            id,
586            order_number,
587            customer_id: input.customer_id,
588            status: OrderStatus::Pending,
589            order_date: now,
590            total_amount: total,
591            currency,
592            payment_status: PaymentStatus::Pending,
593            fulfillment_status: FulfillmentStatus::Unfulfilled,
594            payment_method: input.payment_method.clone(),
595            shipping_method: input.shipping_method.clone(),
596            tracking_number: None,
597            notes: input.notes.clone(),
598            shipping_address: input.shipping_address.clone(),
599            billing_address: input.billing_address.clone(),
600            items,
601            version: 1,
602            created_at: now,
603            updated_at: now,
604        })
605    }
606
607    pub(crate) fn create_from_cart_in_tx(
608        tx: &rusqlite::Transaction<'_>,
609        cart_id: Uuid,
610        input: &CreateOrder,
611    ) -> std::result::Result<Order, rusqlite::Error> {
612        Self::validate_order_input(input)
613            .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
614
615        if let Some(existing) = Self::get_by_cart_id_in_conn(tx, cart_id)? {
616            return Ok(existing);
617        }
618
619        Self::create_internal_in_tx(tx, Some(cart_id), true, input)
620    }
621
622    pub fn create_from_cart(&self, cart_id: Uuid, input: CreateOrder) -> Result<Order> {
623        if let Some(existing) = self.get_by_cart_id(cart_id)? {
624            return Ok(existing);
625        }
626
627        self.create_internal(Some(cart_id), true, input)
628    }
629
630    fn create_internal(
631        &self,
632        cart_id: Option<Uuid>,
633        idempotent_by_cart_id: bool,
634        input: CreateOrder,
635    ) -> Result<Order> {
636        Self::validate_order_input(&input)?;
637
638        with_immediate_transaction(&self.pool, |tx| {
639            Self::create_internal_in_tx(tx, cart_id, idempotent_by_cart_id, &input)
640        })
641    }
642}
643
644impl OrderRepository for SqliteOrderRepository {
645    fn create(&self, input: CreateOrder) -> Result<Order> {
646        self.create_internal(None, false, input)
647    }
648
649    fn get(&self, id: OrderId) -> Result<Option<Order>> {
650        let conn = self.conn()?;
651        let result = conn.query_row(
652            "SELECT * FROM orders WHERE id = ?",
653            [id.to_string()],
654            Self::row_to_order,
655        );
656
657        match result {
658            Ok(mut order) => {
659                order.items = Self::load_order_items_with_conn(&conn, id)?;
660                Ok(Some(order))
661            }
662            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
663            Err(e) => Err(map_db_error(e)),
664        }
665    }
666
667    fn get_by_number(&self, order_number: &str) -> Result<Option<Order>> {
668        let conn = self.conn()?;
669        let result = conn.query_row(
670            "SELECT * FROM orders WHERE order_number = ?",
671            [order_number],
672            Self::row_to_order,
673        );
674
675        match result {
676            Ok(mut order) => {
677                order.items = Self::load_order_items_with_conn(&conn, order.id)?;
678                Ok(Some(order))
679            }
680            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
681            Err(e) => Err(map_db_error(e)),
682        }
683    }
684
685    fn update(&self, id: OrderId, input: UpdateOrder) -> Result<Order> {
686        if let Some(address) = &input.shipping_address {
687            Self::validate_address_input(address, "order.shipping_address")?;
688        }
689        if let Some(address) = &input.billing_address {
690            Self::validate_address_input(address, "order.billing_address")?;
691        }
692
693        let shipping_address_json = input
694            .shipping_address
695            .as_ref()
696            .map(|a| {
697                serde_json::to_string(a).map_err(|e| {
698                    CommerceError::DatabaseError(format!(
699                        "Failed to serialize order.shipping_address: {e}"
700                    ))
701                })
702            })
703            .transpose()?;
704        let billing_address_json = input
705            .billing_address
706            .as_ref()
707            .map(|a| {
708                serde_json::to_string(a).map_err(|e| {
709                    CommerceError::DatabaseError(format!(
710                        "Failed to serialize order.billing_address: {e}"
711                    ))
712                })
713            })
714            .transpose()?;
715
716        struct UpdateOutcome {
717            order: Order,
718            post_commit_error: Option<CommerceError>,
719        }
720
721        let outcome = with_immediate_transaction(&self.pool, |tx| {
722            let now = Utc::now();
723            let (current_version, current_status_raw, current_payment_status_raw): (
724                i32,
725                String,
726                String,
727            ) = tx
728                .query_row(
729                    "SELECT version, status, payment_status FROM orders WHERE id = ?",
730                    [id.to_string()],
731                    |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
732                )
733                .map_err(|e| match e {
734                    rusqlite::Error::QueryReturnedNoRows => {
735                        rusqlite::Error::ToSqlConversionFailure(Box::new(
736                            CommerceError::OrderNotFound(id.into_uuid()),
737                        ))
738                    }
739                    e => e,
740                })?;
741
742            let current_status: OrderStatus = parse_enum(&current_status_raw, "order", "status")
743                .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
744            let current_payment_status: PaymentStatus =
745                parse_enum(&current_payment_status_raw, "order", "payment_status")
746                    .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
747
748            let mut reservation_expired: Option<Uuid> = None;
749
750            if let Some(status) = input.status {
751                if !current_status.can_transition_to(status) {
752                    if status == OrderStatus::Cancelled {
753                        return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
754                            CommerceError::OrderCannotBeCancelled(current_status.to_string()),
755                        )));
756                    }
757
758                    return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
759                        CommerceError::InvalidOrderStatusTransition {
760                            from: current_status.to_string(),
761                            to: status.to_string(),
762                        },
763                    )));
764                }
765
766                if status == OrderStatus::Refunded {
767                    let effective_payment_status =
768                        input.payment_status.unwrap_or(current_payment_status);
769                    if !matches!(
770                        effective_payment_status,
771                        PaymentStatus::Paid
772                            | PaymentStatus::PartiallyPaid
773                            | PaymentStatus::Refunded
774                            | PaymentStatus::PartiallyRefunded
775                    ) {
776                        return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
777                            CommerceError::OrderCannotBeRefunded(
778                                effective_payment_status.to_string(),
779                            ),
780                        )));
781                    }
782                }
783
784                if status == OrderStatus::Shipped {
785                    let reservation_ids =
786                        SqliteInventoryRepository::list_reservation_ids_by_reference_in_tx(
787                            tx,
788                            "order",
789                            &id.to_string(),
790                        )?;
791                    for reservation_id in &reservation_ids {
792                        if SqliteInventoryRepository::expire_reservation_if_needed_in_tx(
793                            tx,
794                            *reservation_id,
795                            now,
796                        )? && reservation_expired.is_none()
797                        {
798                            reservation_expired = Some(*reservation_id);
799                        }
800                    }
801                    if reservation_expired.is_none() {
802                        for reservation_id in reservation_ids {
803                            match SqliteInventoryRepository::confirm_reservation_in_tx_with_now(
804                                tx,
805                                reservation_id,
806                                now,
807                            )? {
808                                ReservationConfirmOutcome::Confirmed => {}
809                                ReservationConfirmOutcome::Expired => {
810                                    if reservation_expired.is_none() {
811                                        reservation_expired = Some(reservation_id);
812                                    }
813                                    break;
814                                }
815                            }
816                        }
817                    }
818                }
819            }
820
821            if let Some(expired_id) = reservation_expired {
822                let result = tx.query_row(
823                    "SELECT * FROM orders WHERE id = ?",
824                    [id.to_string()],
825                    Self::row_to_order,
826                );
827
828                let mut order = match result {
829                    Ok(order) => order,
830                    Err(rusqlite::Error::QueryReturnedNoRows) => {
831                        return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
832                            CommerceError::OrderNotFound(id.into_uuid()),
833                        )));
834                    }
835                    Err(e) => return Err(e),
836                };
837
838                order.items = Self::load_order_items_with_conn(tx, id)
839                    .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
840
841                return Ok(UpdateOutcome {
842                    order,
843                    post_commit_error: Some(CommerceError::ReservationExpired(expired_id)),
844                });
845            }
846
847            // Build dynamic update
848            let mut updates = vec!["updated_at = ?"];
849            let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(now.to_rfc3339())];
850
851            if let Some(status) = &input.status {
852                updates.push("status = ?");
853                params.push(Box::new(status.to_string()));
854            }
855            if let Some(payment_status) = &input.payment_status {
856                updates.push("payment_status = ?");
857                params.push(Box::new(payment_status.to_string()));
858            }
859            if let Some(fulfillment_status) = &input.fulfillment_status {
860                updates.push("fulfillment_status = ?");
861                params.push(Box::new(fulfillment_status.to_string()));
862            }
863            if let Some(tracking) = &input.tracking_number {
864                updates.push("tracking_number = ?");
865                params.push(Box::new(tracking.clone()));
866            }
867            if let Some(notes) = &input.notes {
868                updates.push("notes = ?");
869                params.push(Box::new(notes.clone()));
870            }
871            if let Some(addr_json) = &shipping_address_json {
872                updates.push("shipping_address = ?");
873                params.push(Box::new(addr_json.clone()));
874            }
875            if let Some(addr_json) = &billing_address_json {
876                updates.push("billing_address = ?");
877                params.push(Box::new(addr_json.clone()));
878            }
879
880            updates.push("version = version + 1");
881            params.push(Box::new(id.to_string()));
882            params.push(Box::new(current_version));
883
884            let sql =
885                format!("UPDATE orders SET {} WHERE id = ? AND version = ?", updates.join(", "));
886
887            let params_refs: Vec<&dyn rusqlite::ToSql> =
888                params.iter().map(std::convert::AsRef::as_ref).collect();
889            let rows_affected = tx.execute(&sql, params_refs.as_slice())?;
890            if rows_affected == 0 {
891                return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
892                    CommerceError::VersionConflict {
893                        entity: "order".to_string(),
894                        id: id.to_string(),
895                        expected_version: current_version,
896                    },
897                )));
898            }
899
900            if matches!(input.status, Some(OrderStatus::Cancelled)) {
901                let reservation_ids =
902                    SqliteInventoryRepository::list_reservation_ids_by_reference_in_tx(
903                        tx,
904                        "order",
905                        &id.to_string(),
906                    )?;
907                for reservation_id in reservation_ids {
908                    SqliteInventoryRepository::release_reservation_in_tx(tx, reservation_id)?;
909                }
910                cancel_backorders_for_order_in_tx(tx, id.into_uuid())?;
911            }
912
913            let result = tx.query_row(
914                "SELECT * FROM orders WHERE id = ?",
915                [id.to_string()],
916                Self::row_to_order,
917            );
918
919            let mut order = match result {
920                Ok(order) => order,
921                Err(rusqlite::Error::QueryReturnedNoRows) => {
922                    return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
923                        CommerceError::OrderNotFound(id.into_uuid()),
924                    )));
925                }
926                Err(e) => return Err(e),
927            };
928
929            order.items = Self::load_order_items_with_conn(tx, id)
930                .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
931
932            Ok(UpdateOutcome { order, post_commit_error: None })
933        })?;
934
935        if let Some(err) = outcome.post_commit_error {
936            return Err(err);
937        }
938
939        Ok(outcome.order)
940    }
941
942    fn list(&self, filter: OrderFilter) -> Result<Vec<Order>> {
943        let conn = self.conn()?;
944        let mut sql = "SELECT * FROM orders WHERE 1=1".to_string();
945        let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![];
946
947        if let Some(customer_id) = &filter.customer_id {
948            sql.push_str(" AND customer_id = ?");
949            params.push(Box::new(customer_id.to_string()));
950        }
951        if let Some(status) = &filter.status {
952            sql.push_str(" AND status = ?");
953            params.push(Box::new(status.to_string()));
954        }
955        if let Some(payment_status) = &filter.payment_status {
956            sql.push_str(" AND payment_status = ?");
957            params.push(Box::new(payment_status.to_string()));
958        }
959        if let Some(fulfillment_status) = &filter.fulfillment_status {
960            sql.push_str(" AND fulfillment_status = ?");
961            params.push(Box::new(fulfillment_status.to_string()));
962        }
963        if let Some(from) = &filter.from_date {
964            sql.push_str(" AND order_date >= ?");
965            params.push(Box::new(from.to_rfc3339()));
966        }
967        if let Some(to) = &filter.to_date {
968            sql.push_str(" AND order_date <= ?");
969            params.push(Box::new(to.to_rfc3339()));
970        }
971
972        // Keyset cursor: (order_date, id) for stable DESC ordering
973        if let Some((cursor_date, cursor_id)) = &filter.after_cursor {
974            sql.push_str(" AND (order_date < ? OR (order_date = ? AND id < ?))");
975            params.push(Box::new(cursor_date.clone()));
976            params.push(Box::new(cursor_date.clone()));
977            params.push(Box::new(cursor_id.clone()));
978        }
979
980        sql.push_str(" ORDER BY order_date DESC, id DESC");
981
982        // Offset pagination applies only in non-cursor mode; the helper emits
983        // `LIMIT -1 OFFSET n` when an offset is set without a limit (SQLite rejects
984        // a bare OFFSET).
985        let offset = if filter.after_cursor.is_none() { filter.offset } else { None };
986        crate::sqlite::append_limit_offset(&mut sql, filter.limit, offset);
987
988        let params_refs: Vec<&dyn rusqlite::ToSql> =
989            params.iter().map(std::convert::AsRef::as_ref).collect();
990        let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
991
992        let orders = stmt
993            .query_map(params_refs.as_slice(), Self::row_to_order)
994            .map_err(map_db_error)?
995            .collect::<rusqlite::Result<Vec<_>>>()
996            .map_err(map_db_error)?;
997
998        // Load items for all orders in one batched query on the same connection
999        let ids: Vec<OrderId> = orders.iter().map(|o| o.id).collect();
1000        let mut items_by_id = Self::load_order_items_batch(&conn, &ids)?;
1001        let mut result = vec![];
1002        for mut order in orders {
1003            order.items = items_by_id.remove(&order.id).unwrap_or_default();
1004            result.push(order);
1005        }
1006
1007        Ok(result)
1008    }
1009
1010    fn delete(&self, id: OrderId) -> Result<()> {
1011        with_immediate_transaction(&self.pool, |tx| {
1012            tx.execute("DELETE FROM order_items WHERE order_id = ?", [id.to_string()])?;
1013            tx.execute("DELETE FROM orders WHERE id = ?", [id.to_string()])?;
1014            Ok(())
1015        })
1016    }
1017
1018    fn add_item(&self, order_id: OrderId, item: CreateOrderItem) -> Result<OrderItem> {
1019        validate_required_uuid("order.id", order_id.into_uuid())?;
1020        Self::validate_order_item_input(&item)?;
1021
1022        let item_id = OrderItemId::new();
1023        let item_total = OrderItem::calculate_total(
1024            item.quantity,
1025            item.unit_price,
1026            item.discount.unwrap_or_default(),
1027            item.tax_amount.unwrap_or_default(),
1028        );
1029
1030        with_immediate_transaction(&self.pool, |tx| {
1031            tx.execute(
1032                "INSERT INTO order_items (id, order_id, product_id, variant_id, sku, name,
1033                                          quantity, unit_price, discount, tax_amount, total)
1034                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1035                rusqlite::params![
1036                    item_id.to_string(),
1037                    order_id.to_string(),
1038                    item.product_id.to_string(),
1039                    item.variant_id.map(|v| v.to_string()),
1040                    item.sku,
1041                    item.name,
1042                    item.quantity,
1043                    item.unit_price.to_string(),
1044                    item.discount.unwrap_or_default().to_string(),
1045                    item.tax_amount.unwrap_or_default().to_string(),
1046                    item_total.to_string(),
1047                ],
1048            )?;
1049
1050            // Update order total
1051            self.update_order_total(tx, order_id)
1052                .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
1053            Ok(())
1054        })?;
1055
1056        Ok(OrderItem {
1057            id: item_id,
1058            order_id,
1059            product_id: item.product_id,
1060            variant_id: item.variant_id,
1061            sku: item.sku,
1062            name: item.name,
1063            quantity: item.quantity,
1064            unit_price: item.unit_price,
1065            discount: item.discount.unwrap_or_default(),
1066            tax_amount: item.tax_amount.unwrap_or_default(),
1067            total: item_total,
1068        })
1069    }
1070
1071    fn remove_item(&self, order_id: OrderId, item_id: OrderItemId) -> Result<()> {
1072        with_immediate_transaction(&self.pool, |tx| {
1073            tx.execute(
1074                "DELETE FROM order_items WHERE id = ? AND order_id = ?",
1075                [item_id.to_string(), order_id.to_string()],
1076            )?;
1077
1078            self.update_order_total(tx, order_id)
1079                .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
1080            Ok(())
1081        })
1082    }
1083
1084    fn count(&self, filter: OrderFilter) -> Result<u64> {
1085        let conn = self.conn()?;
1086        let mut sql = "SELECT COUNT(*) FROM orders WHERE 1=1".to_string();
1087        let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![];
1088
1089        if let Some(customer_id) = &filter.customer_id {
1090            sql.push_str(" AND customer_id = ?");
1091            params.push(Box::new(customer_id.to_string()));
1092        }
1093        if let Some(status) = &filter.status {
1094            sql.push_str(" AND status = ?");
1095            params.push(Box::new(status.to_string()));
1096        }
1097        if let Some(payment_status) = &filter.payment_status {
1098            sql.push_str(" AND payment_status = ?");
1099            params.push(Box::new(payment_status.to_string()));
1100        }
1101        if let Some(fulfillment_status) = &filter.fulfillment_status {
1102            sql.push_str(" AND fulfillment_status = ?");
1103            params.push(Box::new(fulfillment_status.to_string()));
1104        }
1105        if let Some(from) = &filter.from_date {
1106            sql.push_str(" AND order_date >= ?");
1107            params.push(Box::new(from.to_rfc3339()));
1108        }
1109        if let Some(to) = &filter.to_date {
1110            sql.push_str(" AND order_date <= ?");
1111            params.push(Box::new(to.to_rfc3339()));
1112        }
1113
1114        let params_refs: Vec<&dyn rusqlite::ToSql> =
1115            params.iter().map(std::convert::AsRef::as_ref).collect();
1116        let count: i64 =
1117            conn.query_row(&sql, params_refs.as_slice(), |row| row.get(0)).map_err(map_db_error)?;
1118
1119        Ok(count as u64)
1120    }
1121
1122    // === Batch Operations ===
1123
1124    fn create_batch(&self, inputs: Vec<CreateOrder>) -> Result<BatchResult<Order>> {
1125        validate_batch_size(&inputs)?;
1126        let mut result = BatchResult::with_capacity(inputs.len());
1127
1128        // Use a single connection for the entire batch to avoid pool churn.
1129        // Each order still gets its own transaction for partial-success semantics.
1130        let mut conn = self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))?;
1131
1132        for (index, input) in inputs.into_iter().enumerate() {
1133            Self::validate_order_input(&input)?;
1134            let tx_result =
1135                conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate);
1136            match tx_result {
1137                Ok(tx) => {
1138                    match Self::create_internal_in_tx(&tx, None, false, &input) {
1139                        Ok(order) => {
1140                            if let Err(e) = tx.commit() {
1141                                result.record_failure(index, None, &map_db_error(e));
1142                            } else {
1143                                result.record_success(order);
1144                            }
1145                        }
1146                        Err(e) => {
1147                            // Transaction auto-rolls back on drop
1148                            result.record_failure(index, None, &map_db_error(e));
1149                        }
1150                    }
1151                }
1152                Err(e) => result.record_failure(index, None, &map_db_error(e)),
1153            }
1154        }
1155
1156        Ok(result)
1157    }
1158
1159    fn create_batch_atomic(&self, inputs: Vec<CreateOrder>) -> Result<Vec<Order>> {
1160        validate_batch_size(&inputs)?;
1161        if inputs.is_empty() {
1162            return Ok(vec![]);
1163        }
1164
1165        let mut conn = self.conn()?;
1166        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1167        let mut results = Vec::with_capacity(inputs.len());
1168
1169        for input in inputs {
1170            Self::validate_order_input(&input)?;
1171
1172            let id = OrderId::new();
1173            let order_number = Self::generate_order_number();
1174            let now = Utc::now();
1175            let currency = input.currency.unwrap_or_default();
1176
1177            let total: Decimal = input
1178                .items
1179                .iter()
1180                .map(|item| {
1181                    OrderItem::calculate_total(
1182                        item.quantity,
1183                        item.unit_price,
1184                        item.discount.unwrap_or_default(),
1185                        item.tax_amount.unwrap_or_default(),
1186                    )
1187                })
1188                .sum();
1189
1190            let shipping_address_json = input
1191                .shipping_address
1192                .as_ref()
1193                .map(|a| {
1194                    serde_json::to_string(a).map_err(|e| {
1195                        CommerceError::DatabaseError(format!(
1196                            "Failed to serialize order.shipping_address: {e}"
1197                        ))
1198                    })
1199                })
1200                .transpose()?;
1201            let billing_address_json = input
1202                .billing_address
1203                .as_ref()
1204                .map(|a| {
1205                    serde_json::to_string(a).map_err(|e| {
1206                        CommerceError::DatabaseError(format!(
1207                            "Failed to serialize order.billing_address: {e}"
1208                        ))
1209                    })
1210                })
1211                .transpose()?;
1212
1213            tx.execute(
1214                "INSERT INTO orders (id, order_number, customer_id, status, order_date, total_amount,
1215                                     currency, payment_status, fulfillment_status, payment_method,
1216                                     shipping_method, notes, shipping_address, billing_address,
1217                                     created_at, updated_at)
1218                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1219                rusqlite::params![
1220                    id.to_string(),
1221                    &order_number,
1222                    input.customer_id.to_string(),
1223                    "pending",
1224                    now.to_rfc3339(),
1225                    total.to_string(),
1226                    &currency,
1227                    "pending",
1228                    "unfulfilled",
1229                    &input.payment_method,
1230                    &input.shipping_method,
1231                    &input.notes,
1232                    &shipping_address_json,
1233                    &billing_address_json,
1234                    now.to_rfc3339(),
1235                    now.to_rfc3339(),
1236                ],
1237            )
1238            .map_err(map_db_error)?;
1239
1240            let mut items = Vec::with_capacity(input.items.len());
1241            for item in &input.items {
1242                let item_id = OrderItemId::new();
1243                let item_total = OrderItem::calculate_total(
1244                    item.quantity,
1245                    item.unit_price,
1246                    item.discount.unwrap_or_default(),
1247                    item.tax_amount.unwrap_or_default(),
1248                );
1249
1250                tx.execute(
1251                    "INSERT INTO order_items (id, order_id, product_id, variant_id, sku, name,
1252                                              quantity, unit_price, discount, tax_amount, total)
1253                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1254                    rusqlite::params![
1255                        item_id.to_string(),
1256                        id.to_string(),
1257                        item.product_id.to_string(),
1258                        item.variant_id.map(|v| v.to_string()),
1259                        &item.sku,
1260                        &item.name,
1261                        item.quantity,
1262                        item.unit_price.to_string(),
1263                        item.discount.unwrap_or_default().to_string(),
1264                        item.tax_amount.unwrap_or_default().to_string(),
1265                        item_total.to_string(),
1266                    ],
1267                )
1268                .map_err(map_db_error)?;
1269
1270                items.push(OrderItem {
1271                    id: item_id,
1272                    order_id: id,
1273                    product_id: item.product_id,
1274                    variant_id: item.variant_id,
1275                    sku: item.sku.clone(),
1276                    name: item.name.clone(),
1277                    quantity: item.quantity,
1278                    unit_price: item.unit_price,
1279                    discount: item.discount.unwrap_or_default(),
1280                    tax_amount: item.tax_amount.unwrap_or_default(),
1281                    total: item_total,
1282                });
1283            }
1284
1285            results.push(Order {
1286                id,
1287                order_number,
1288                customer_id: input.customer_id,
1289                status: OrderStatus::Pending,
1290                order_date: now,
1291                total_amount: total,
1292                currency,
1293                payment_status: PaymentStatus::Pending,
1294                fulfillment_status: FulfillmentStatus::Unfulfilled,
1295                payment_method: input.payment_method,
1296                shipping_method: input.shipping_method,
1297                tracking_number: None,
1298                notes: input.notes,
1299                shipping_address: input.shipping_address,
1300                billing_address: input.billing_address,
1301                items,
1302                version: 1,
1303                created_at: now,
1304                updated_at: now,
1305            });
1306        }
1307
1308        tx.commit().map_err(map_db_error)?;
1309        Ok(results)
1310    }
1311
1312    fn update_batch(&self, updates: Vec<(OrderId, UpdateOrder)>) -> Result<BatchResult<Order>> {
1313        validate_batch_size(&updates)?;
1314        let mut result = BatchResult::with_capacity(updates.len());
1315
1316        for (index, (id, input)) in updates.into_iter().enumerate() {
1317            match self.update(id, input) {
1318                Ok(order) => result.record_success(order),
1319                Err(e) => result.record_failure(index, Some(id.to_string()), &e),
1320            }
1321        }
1322
1323        Ok(result)
1324    }
1325
1326    fn update_batch_atomic(&self, updates: Vec<(OrderId, UpdateOrder)>) -> Result<Vec<Order>> {
1327        validate_batch_size(&updates)?;
1328        if updates.is_empty() {
1329            return Ok(vec![]);
1330        }
1331
1332        // For atomic updates, we use a transaction and fail on any error
1333        let mut conn = self.conn()?;
1334        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1335        let mut results = Vec::with_capacity(updates.len());
1336
1337        for (id, input) in updates {
1338            let now = Utc::now();
1339            let (current_version, current_status_raw, current_payment_status_raw): (
1340                i32,
1341                String,
1342                String,
1343            ) = tx
1344                .query_row(
1345                    "SELECT version, status, payment_status FROM orders WHERE id = ?",
1346                    [id.to_string()],
1347                    |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1348                )
1349                .map_err(|e| match e {
1350                    rusqlite::Error::QueryReturnedNoRows => {
1351                        CommerceError::OrderNotFound(id.into_uuid())
1352                    }
1353                    e => map_db_error(e),
1354                })?;
1355            let current_status: OrderStatus = parse_enum(&current_status_raw, "order", "status")?;
1356            let current_payment_status: PaymentStatus =
1357                parse_enum(&current_payment_status_raw, "order", "payment_status")?;
1358
1359            let mut update_parts = vec!["updated_at = ?"];
1360            let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(now.to_rfc3339())];
1361
1362            if let Some(status) = &input.status {
1363                let next_status = *status;
1364                if !current_status.can_transition_to(next_status) {
1365                    if next_status == OrderStatus::Cancelled {
1366                        return Err(CommerceError::OrderCannotBeCancelled(
1367                            current_status.to_string(),
1368                        ));
1369                    }
1370
1371                    return Err(CommerceError::InvalidOrderStatusTransition {
1372                        from: current_status.to_string(),
1373                        to: next_status.to_string(),
1374                    });
1375                }
1376
1377                if next_status == OrderStatus::Refunded {
1378                    let effective_payment_status =
1379                        input.payment_status.unwrap_or(current_payment_status);
1380                    if !matches!(
1381                        effective_payment_status,
1382                        PaymentStatus::Paid
1383                            | PaymentStatus::PartiallyPaid
1384                            | PaymentStatus::Refunded
1385                            | PaymentStatus::PartiallyRefunded
1386                    ) {
1387                        return Err(CommerceError::OrderCannotBeRefunded(
1388                            effective_payment_status.to_string(),
1389                        ));
1390                    }
1391                }
1392
1393                update_parts.push("status = ?");
1394                params.push(Box::new(status.to_string()));
1395            }
1396            if let Some(payment_status) = &input.payment_status {
1397                update_parts.push("payment_status = ?");
1398                params.push(Box::new(payment_status.to_string()));
1399            }
1400            if let Some(fulfillment_status) = &input.fulfillment_status {
1401                update_parts.push("fulfillment_status = ?");
1402                params.push(Box::new(fulfillment_status.to_string()));
1403            }
1404            if let Some(tracking) = &input.tracking_number {
1405                update_parts.push("tracking_number = ?");
1406                params.push(Box::new(tracking.clone()));
1407            }
1408            if let Some(notes) = &input.notes {
1409                update_parts.push("notes = ?");
1410                params.push(Box::new(notes.clone()));
1411            }
1412            if let Some(addr) = &input.shipping_address {
1413                Self::validate_address_input(addr, "order.shipping_address")?;
1414                update_parts.push("shipping_address = ?");
1415                let address_json = serde_json::to_string(addr).map_err(|e| {
1416                    CommerceError::DatabaseError(format!(
1417                        "Failed to serialize order.shipping_address: {e}"
1418                    ))
1419                })?;
1420                params.push(Box::new(address_json));
1421            }
1422            if let Some(addr) = &input.billing_address {
1423                Self::validate_address_input(addr, "order.billing_address")?;
1424                update_parts.push("billing_address = ?");
1425                let address_json = serde_json::to_string(addr).map_err(|e| {
1426                    CommerceError::DatabaseError(format!(
1427                        "Failed to serialize order.billing_address: {e}"
1428                    ))
1429                })?;
1430                params.push(Box::new(address_json));
1431            }
1432
1433            update_parts.push("version = version + 1");
1434            params.push(Box::new(id.to_string()));
1435            params.push(Box::new(current_version));
1436
1437            let sql = format!(
1438                "UPDATE orders SET {} WHERE id = ? AND version = ?",
1439                update_parts.join(", ")
1440            );
1441
1442            let params_refs: Vec<&dyn rusqlite::ToSql> =
1443                params.iter().map(std::convert::AsRef::as_ref).collect();
1444            let rows_affected = tx.execute(&sql, params_refs.as_slice()).map_err(map_db_error)?;
1445            if rows_affected == 0 {
1446                return Err(CommerceError::VersionConflict {
1447                    entity: "order".to_string(),
1448                    id: id.to_string(),
1449                    expected_version: current_version,
1450                });
1451            }
1452
1453            let order = tx
1454                .query_row(
1455                    "SELECT * FROM orders WHERE id = ?",
1456                    [id.to_string()],
1457                    Self::row_to_order,
1458                )
1459                .map_err(map_db_error)?;
1460
1461            results.push(order);
1462        }
1463
1464        tx.commit().map_err(map_db_error)?;
1465
1466        // Load items for all orders in one batched query
1467        let conn = self.conn()?;
1468        let ids: Vec<OrderId> = results.iter().map(|o| o.id).collect();
1469        let mut items_by_id = Self::load_order_items_batch(&conn, &ids)?;
1470        for order in &mut results {
1471            order.items = items_by_id.remove(&order.id).unwrap_or_default();
1472        }
1473
1474        Ok(results)
1475    }
1476
1477    fn delete_batch(&self, ids: Vec<OrderId>) -> Result<BatchResult<OrderId>> {
1478        validate_batch_size(&ids)?;
1479        let mut result = BatchResult::with_capacity(ids.len());
1480
1481        for (index, id) in ids.into_iter().enumerate() {
1482            match self.delete(id) {
1483                Ok(()) => result.record_success(id),
1484                Err(e) => result.record_failure(index, Some(id.to_string()), &e),
1485            }
1486        }
1487
1488        Ok(result)
1489    }
1490
1491    fn delete_batch_atomic(&self, ids: Vec<OrderId>) -> Result<()> {
1492        validate_batch_size(&ids)?;
1493        if ids.is_empty() {
1494            return Ok(());
1495        }
1496
1497        let mut conn = self.conn()?;
1498        let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1499
1500        let placeholders = build_in_clause(ids.len());
1501        let raw_ids: Vec<Uuid> = ids.iter().map(|id| id.into_uuid()).collect();
1502        let params = uuid_params(&raw_ids);
1503        let params_refs = params_refs(&params);
1504
1505        // Delete order items first
1506        let sql = format!("DELETE FROM order_items WHERE order_id IN ({placeholders})");
1507        tx.execute(&sql, params_refs.as_slice()).map_err(map_db_error)?;
1508
1509        // Delete orders
1510        let sql = format!("DELETE FROM orders WHERE id IN ({placeholders})");
1511        tx.execute(&sql, params_refs.as_slice()).map_err(map_db_error)?;
1512
1513        tx.commit().map_err(map_db_error)?;
1514        Ok(())
1515    }
1516
1517    fn get_batch(&self, ids: Vec<OrderId>) -> Result<Vec<Order>> {
1518        validate_batch_size(&ids)?;
1519        if ids.is_empty() {
1520            return Ok(vec![]);
1521        }
1522
1523        let conn = self.conn()?;
1524        let placeholders = build_in_clause(ids.len());
1525        let sql = format!("SELECT * FROM orders WHERE id IN ({placeholders})");
1526
1527        let raw_ids: Vec<Uuid> = ids.iter().map(|id| id.into_uuid()).collect();
1528        let params = uuid_params(&raw_ids);
1529        let params_refs = params_refs(&params);
1530
1531        let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
1532        let orders = stmt
1533            .query_map(params_refs.as_slice(), Self::row_to_order)
1534            .map_err(map_db_error)?
1535            .collect::<rusqlite::Result<Vec<_>>>()
1536            .map_err(map_db_error)?;
1537
1538        // Load items for all orders in one batched query
1539        let order_ids: Vec<OrderId> = orders.iter().map(|o| o.id).collect();
1540        let mut items_by_id = Self::load_order_items_batch(&conn, &order_ids)?;
1541        let mut result = vec![];
1542        for mut order in orders {
1543            order.items = items_by_id.remove(&order.id).unwrap_or_default();
1544            result.push(order);
1545        }
1546
1547        Ok(result)
1548    }
1549}
1550
1551impl SqliteOrderRepository {
1552    fn update_order_total(&self, conn: &rusqlite::Connection, order_id: OrderId) -> Result<()> {
1553        let current_version: i32 = conn
1554            .query_row("SELECT version FROM orders WHERE id = ?", [order_id.to_string()], |row| {
1555                row.get(0)
1556            })
1557            .map_err(|e| match e {
1558                rusqlite::Error::QueryReturnedNoRows => {
1559                    CommerceError::OrderNotFound(order_id.into_uuid())
1560                }
1561                e => map_db_error(e),
1562            })?;
1563
1564        let order_id_param = order_id.to_string();
1565        let order_params: [&dyn rusqlite::ToSql; 1] = [&order_id_param];
1566        let total = sum_decimal_query(
1567            conn,
1568            "SELECT total FROM order_items WHERE order_id = ?",
1569            &order_params,
1570            "order_item",
1571            "total",
1572        )?;
1573        let total = total.to_string();
1574
1575        let rows_affected = conn
1576            .execute(
1577                "UPDATE orders SET total_amount = ?, updated_at = ?, version = version + 1 WHERE id = ? AND version = ?",
1578                rusqlite::params![
1579                    total,
1580                    Utc::now().to_rfc3339(),
1581                    order_id.to_string(),
1582                    current_version
1583                ],
1584            )
1585            .map_err(map_db_error)?;
1586        if rows_affected == 0 {
1587            return Err(CommerceError::VersionConflict {
1588                entity: "order".to_string(),
1589                id: order_id.to_string(),
1590                expected_version: current_version,
1591            });
1592        }
1593
1594        Ok(())
1595    }
1596}
1597
1598#[cfg(test)]
1599mod tests {
1600    use super::*;
1601    use std::str::FromStr;
1602
1603    #[test]
1604    fn parses_payment_status_snake_case_and_legacy() {
1605        assert_eq!(
1606            PaymentStatus::from_str("partially_paid").unwrap(),
1607            PaymentStatus::PartiallyPaid
1608        );
1609        assert_eq!(PaymentStatus::from_str("partiallypaid").unwrap(), PaymentStatus::PartiallyPaid);
1610        assert_eq!(
1611            PaymentStatus::from_str("partially_refunded").unwrap(),
1612            PaymentStatus::PartiallyRefunded
1613        );
1614        assert_eq!(
1615            PaymentStatus::from_str("partiallyrefunded").unwrap(),
1616            PaymentStatus::PartiallyRefunded
1617        );
1618    }
1619
1620    #[test]
1621    fn parses_fulfillment_status_snake_case_and_legacy() {
1622        assert_eq!(
1623            FulfillmentStatus::from_str("partially_fulfilled").unwrap(),
1624            FulfillmentStatus::PartiallyFulfilled
1625        );
1626        assert_eq!(
1627            FulfillmentStatus::from_str("partiallyfulfilled").unwrap(),
1628            FulfillmentStatus::PartiallyFulfilled
1629        );
1630    }
1631}