1use 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#[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 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![], 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 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 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 ¤cy,
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 ¤cy,
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(¤t_status_raw, "order", "status")
743 .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
744 let current_payment_status: PaymentStatus =
745 parse_enum(¤t_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 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 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 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 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 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 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 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 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 ¤cy,
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 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(¤t_status_raw, "order", "status")?;
1356 let current_payment_status: PaymentStatus =
1357 parse_enum(¤t_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 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(¶ms);
1504
1505 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 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(¶ms);
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 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}