1use super::parse_helpers::{parse_decimal as parse_decimal_err, parse_uuid};
4use super::{
5 SqliteOrderRepository, SqlitePromotionRepository, build_in_clause, map_db_error, params_refs,
6 parse_datetime_opt_row, parse_datetime_row, parse_decimal_opt_row, parse_decimal_row,
7 parse_enum_row, parse_json_opt_row, parse_uuid_opt_row, parse_uuid_row, uuid_params,
8 with_immediate_transaction,
9};
10use chrono::{Duration, Utc};
11use r2d2::Pool;
12use r2d2_sqlite::SqliteConnectionManager;
13use rusqlite::OptionalExtension;
14use rust_decimal::Decimal;
15use stateset_core::{
16 AddCartItem, BatchResult, Cart, CartAddress, CartFilter, CartId, CartItem, CartPaymentStatus,
17 CartRepository, CartStatus, CartX402Payment, CheckoutResult, CommerceError, CreateCart,
18 CreateOrder, CreateOrderItem, CurrencyCode, CustomerId, OrderId, OrderStatus, PaymentStatus,
19 ProductId, PromotionType, Result, SetCartPayment, SetCartShipping, SetCartX402Payment,
20 ShippingRate, UpdateCart, UpdateCartItem, X402AwaitingSettlementData, X402CheckoutResult,
21 X402IntentCreatedData, X402IntentStatus, X402PaymentRequiredData, validate_batch_size,
22 validate_currency_code, validate_email, validate_phone, validate_price, validate_required_text,
23};
24use uuid::Uuid;
25
26#[derive(Debug)]
28pub struct SqliteCartRepository {
29 pool: Pool<SqliteConnectionManager>,
30}
31
32impl SqliteCartRepository {
33 #[must_use]
34 pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
35 Self { pool }
36 }
37
38 fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
39 self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))
40 }
41
42 fn generate_cart_number() -> String {
43 let timestamp_ms = Utc::now().timestamp_millis();
44 let random_suffix = (Uuid::new_v4().as_u128() & 0xFFFF_FFFF_FFFF_FFFF) as u64;
45 format!("CART-{timestamp_ms}-{random_suffix:016x}")
46 }
47
48 fn row_to_cart(row: &rusqlite::Row<'_>) -> rusqlite::Result<Cart> {
49 let shipping_addr: Option<String> = row.get("shipping_address")?;
50 let billing_addr: Option<String> = row.get("billing_address")?;
51 let metadata: Option<String> = row.get("metadata")?;
52
53 Ok(Cart {
54 id: CartId::from(parse_uuid_row(&row.get::<_, String>("id")?, "cart", "id")?),
55 cart_number: row.get("cart_number")?,
56 customer_id: parse_uuid_opt_row(
57 row.get::<_, Option<String>>("customer_id")?,
58 "cart",
59 "customer_id",
60 )?
61 .map(CustomerId::from),
62 status: parse_enum_row(&row.get::<_, String>("status")?, "cart", "status")?,
63 currency: row.get("currency")?,
64
65 items: vec![], subtotal: parse_decimal_row(&row.get::<_, String>("subtotal")?, "cart", "subtotal")?,
68 tax_amount: parse_decimal_row(
69 &row.get::<_, String>("tax_amount")?,
70 "cart",
71 "tax_amount",
72 )?,
73 shipping_amount: parse_decimal_row(
74 &row.get::<_, String>("shipping_amount")?,
75 "cart",
76 "shipping_amount",
77 )?,
78 discount_amount: parse_decimal_row(
79 &row.get::<_, String>("discount_amount")?,
80 "cart",
81 "discount_amount",
82 )?,
83 grand_total: parse_decimal_row(
84 &row.get::<_, String>("grand_total")?,
85 "cart",
86 "grand_total",
87 )?,
88
89 customer_email: row.get("customer_email")?,
90 customer_phone: row.get("customer_phone")?,
91 customer_name: row.get("customer_name")?,
92
93 shipping_address: parse_json_opt_row(shipping_addr, "cart", "shipping_address")?,
94 billing_address: parse_json_opt_row(billing_addr, "cart", "billing_address")?,
95 billing_same_as_shipping: row.get::<_, i32>("billing_same_as_shipping")? == 1,
96
97 fulfillment_type: match row.get::<_, Option<String>>("fulfillment_type")? {
98 Some(value) => Some(parse_enum_row(&value, "cart", "fulfillment_type")?),
99 None => None,
100 },
101 shipping_method: row.get("shipping_method")?,
102 shipping_carrier: row.get("shipping_carrier")?,
103 estimated_delivery: parse_datetime_opt_row(
104 row.get::<_, Option<String>>("estimated_delivery")?,
105 "cart",
106 "estimated_delivery",
107 )?,
108
109 payment_method: row.get("payment_method")?,
110 payment_token: row.get("payment_token")?,
111 payment_status: parse_enum_row(
112 &row.get::<_, String>("payment_status")?,
113 "cart",
114 "payment_status",
115 )?,
116
117 coupon_code: row.get("coupon_code")?,
118 discount_description: row.get("discount_description")?,
119
120 order_id: parse_uuid_opt_row(
121 row.get::<_, Option<String>>("order_id")?,
122 "cart",
123 "order_id",
124 )?
125 .map(OrderId::from),
126 order_number: row.get("order_number")?,
127
128 notes: row.get("notes")?,
129 metadata: parse_json_opt_row(metadata, "cart", "metadata")?,
130
131 inventory_reserved: row.get::<_, i32>("inventory_reserved")? == 1,
132 reservation_expires_at: parse_datetime_opt_row(
133 row.get::<_, Option<String>>("reservation_expires_at")?,
134 "cart",
135 "reservation_expires_at",
136 )?,
137
138 x402_payment: {
140 let payer: Option<String> = row.get("x402_payer_address")?;
141 if let Some(payer_address) = payer {
142 let network_str: Option<String> = row.get("x402_network")?;
143 let asset_str: Option<String> = row.get("x402_asset")?;
144 let intent_id: Option<String> = row.get("x402_intent_id")?;
145 let status_str: Option<String> = row.get("x402_status")?;
146 Some(CartX402Payment {
147 intent_id: parse_uuid_opt_row(intent_id, "cart", "x402_intent_id")?,
148 payer_address,
149 network: match network_str {
150 Some(value) => parse_enum_row(&value, "cart", "x402_network")?,
151 None => Default::default(),
152 },
153 asset: match asset_str {
154 Some(value) => parse_enum_row(&value, "cart", "x402_asset")?,
155 None => Default::default(),
156 },
157 status: match status_str {
158 Some(value) => parse_enum_row(&value, "cart", "x402_status")?,
159 None => Default::default(),
160 },
161 })
162 } else {
163 None
164 }
165 },
166
167 expires_at: parse_datetime_opt_row(
168 row.get::<_, Option<String>>("expires_at")?,
169 "cart",
170 "expires_at",
171 )?,
172 completed_at: parse_datetime_opt_row(
173 row.get::<_, Option<String>>("completed_at")?,
174 "cart",
175 "completed_at",
176 )?,
177 created_at: parse_datetime_row(
178 &row.get::<_, String>("created_at")?,
179 "cart",
180 "created_at",
181 )?,
182 updated_at: parse_datetime_row(
183 &row.get::<_, String>("updated_at")?,
184 "cart",
185 "updated_at",
186 )?,
187 })
188 }
189
190 fn load_cart_items_with_conn(
191 conn: &rusqlite::Connection,
192 cart_id: CartId,
193 ) -> Result<Vec<CartItem>> {
194 let mut stmt = conn
195 .prepare(
196 "SELECT id, cart_id, product_id, variant_id, sku, name, description, image_url,
197 quantity, unit_price, original_price, discount_amount, tax_amount, total,
198 weight, requires_shipping, metadata, created_at, updated_at
199 FROM cart_items WHERE cart_id = ?",
200 )
201 .map_err(map_db_error)?;
202
203 let items = stmt
204 .query_map([cart_id.to_string()], Self::row_to_cart_item)
205 .map_err(map_db_error)?
206 .collect::<rusqlite::Result<Vec<_>>>()
207 .map_err(map_db_error)?;
208
209 Ok(items)
210 }
211
212 fn row_to_cart_item(row: &rusqlite::Row<'_>) -> rusqlite::Result<CartItem> {
213 let metadata: Option<String> = row.get("metadata")?;
214 Ok(CartItem {
215 id: parse_uuid_row(&row.get::<_, String>("id")?, "cart_item", "id")?,
216 cart_id: CartId::from(parse_uuid_row(
217 &row.get::<_, String>("cart_id")?,
218 "cart_item",
219 "cart_id",
220 )?),
221 product_id: parse_uuid_opt_row(
222 row.get::<_, Option<String>>("product_id")?,
223 "cart_item",
224 "product_id",
225 )?
226 .map(ProductId::from),
227 variant_id: parse_uuid_opt_row(
228 row.get::<_, Option<String>>("variant_id")?,
229 "cart_item",
230 "variant_id",
231 )?,
232 sku: row.get("sku")?,
233 name: row.get("name")?,
234 description: row.get("description")?,
235 image_url: row.get("image_url")?,
236 quantity: row.get("quantity")?,
237 unit_price: parse_decimal_row(
238 &row.get::<_, String>("unit_price")?,
239 "cart_item",
240 "unit_price",
241 )?,
242 original_price: parse_decimal_opt_row(
243 row.get::<_, Option<String>>("original_price")?,
244 "cart_item",
245 "original_price",
246 )?,
247 discount_amount: parse_decimal_row(
248 &row.get::<_, String>("discount_amount")?,
249 "cart_item",
250 "discount_amount",
251 )?,
252 tax_amount: parse_decimal_row(
253 &row.get::<_, String>("tax_amount")?,
254 "cart_item",
255 "tax_amount",
256 )?,
257 total: parse_decimal_row(&row.get::<_, String>("total")?, "cart_item", "total")?,
258 weight: parse_decimal_opt_row(
259 row.get::<_, Option<String>>("weight")?,
260 "cart_item",
261 "weight",
262 )?,
263 requires_shipping: row.get::<_, i32>("requires_shipping")? == 1,
264 metadata: parse_json_opt_row(metadata, "cart_item", "metadata")?,
265 created_at: parse_datetime_row(
266 &row.get::<_, String>("created_at")?,
267 "cart_item",
268 "created_at",
269 )?,
270 updated_at: parse_datetime_row(
271 &row.get::<_, String>("updated_at")?,
272 "cart_item",
273 "updated_at",
274 )?,
275 })
276 }
277
278 fn load_cart_items_batch(
279 conn: &rusqlite::Connection,
280 ids: &[CartId],
281 ) -> Result<std::collections::HashMap<CartId, Vec<CartItem>>> {
282 let mut map: std::collections::HashMap<CartId, Vec<CartItem>> =
283 std::collections::HashMap::with_capacity(ids.len());
284 for chunk in ids.chunks(500) {
285 let placeholders = build_in_clause(chunk.len());
286 let sql = format!(
287 "SELECT id, cart_id, product_id, variant_id, sku, name, description, image_url,
288 quantity, unit_price, original_price, discount_amount, tax_amount, total,
289 weight, requires_shipping, metadata, created_at, updated_at
290 FROM cart_items WHERE cart_id IN ({placeholders})"
291 );
292 let id_strs: Vec<String> = chunk.iter().map(ToString::to_string).collect();
293 let param_refs: Vec<&dyn rusqlite::ToSql> =
294 id_strs.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
295 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
296 let rows = stmt
297 .query_map(param_refs.as_slice(), Self::row_to_cart_item)
298 .map_err(map_db_error)?;
299 for row in rows {
300 let item = row.map_err(map_db_error)?;
301 map.entry(item.cart_id).or_default().push(item);
302 }
303 }
304 Ok(map)
305 }
306
307 fn load_cart_with_conn(conn: &rusqlite::Connection, id: CartId) -> Result<Option<Cart>> {
308 let result =
309 conn.query_row("SELECT * FROM carts WHERE id = ?", [id.to_string()], Self::row_to_cart);
310
311 match result {
312 Ok(mut cart) => {
313 cart.items = Self::load_cart_items_with_conn(conn, id)?;
314 Ok(Some(cart))
315 }
316 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
317 Err(error) => Err(map_db_error(error)),
318 }
319 }
320
321 fn finalize_x402_checkout(&self, cart_id: CartId) -> Result<X402CheckoutResult> {
323 let result = with_immediate_transaction(&self.pool, |tx| {
324 self.complete_checkout_in_tx(tx, cart_id, true, true)
325 })?;
326 Ok(X402CheckoutResult::Completed(result))
327 }
328
329 fn update_cart_totals(&self, conn: &rusqlite::Connection, cart_id: CartId) -> Result<()> {
330 let mut subtotal = Decimal::ZERO;
332 let mut stmt = conn
333 .prepare(
334 "SELECT quantity, unit_price, discount_amount FROM cart_items WHERE cart_id = ?",
335 )
336 .map_err(map_db_error)?;
337 let rows = stmt
338 .query_map([cart_id.to_string()], |row| {
339 Ok((
340 row.get::<_, i32>("quantity")?,
341 row.get::<_, String>("unit_price")?,
342 row.get::<_, String>("discount_amount")?,
343 ))
344 })
345 .map_err(map_db_error)?;
346
347 for row in rows {
348 let (quantity, unit_price, discount_amount) = row.map_err(map_db_error)?;
349 let line_subtotal = parse_decimal_err(&unit_price, "cart_item", "unit_price")?
350 * Decimal::from(quantity)
351 - parse_decimal_err(&discount_amount, "cart_item", "discount_amount")?;
352 subtotal += line_subtotal;
353 }
354
355 let subtotal = subtotal.round_dp(2);
359
360 let (tax, shipping, discount): (String, String, String) = conn
362 .query_row(
363 "SELECT tax_amount, shipping_amount, discount_amount FROM carts WHERE id = ?",
364 [cart_id.to_string()],
365 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
366 )
367 .map_err(map_db_error)?;
368
369 let tax_dec = parse_decimal_err(&tax, "cart", "tax_amount")?;
373 let shipping_dec = parse_decimal_err(&shipping, "cart", "shipping_amount")?;
374 let discount_dec = parse_decimal_err(&discount, "cart", "discount_amount")?;
375 let grand_total =
376 (subtotal + tax_dec + shipping_dec - discount_dec).round_dp(2).max(Decimal::ZERO);
377
378 conn.execute(
379 "UPDATE carts SET subtotal = ?, grand_total = ?, updated_at = ? WHERE id = ?",
380 rusqlite::params![
381 subtotal.to_string(),
382 grand_total.to_string(),
383 Utc::now().to_rfc3339(),
384 cart_id.to_string()
385 ],
386 )
387 .map_err(map_db_error)?;
388
389 Ok(())
390 }
391}
392
393impl CartRepository for SqliteCartRepository {
394 fn create(&self, input: CreateCart) -> Result<Cart> {
395 if let Some(ref currency) = input.currency {
397 validate_currency_code(currency.as_str())?;
398 }
399
400 let mut conn = self.conn()?;
401 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
402 let id = CartId::new();
403 let cart_number = Self::generate_cart_number();
404 let now = Utc::now();
405 let currency = input.currency.unwrap_or_default();
406
407 let expires_at = input.expires_in_minutes.map(|mins| now + Duration::minutes(mins));
408
409 let shipping_address_json =
410 input.shipping_address.as_ref().map(|a| serde_json::to_string(a).unwrap_or_default());
411 let billing_address_json =
412 input.billing_address.as_ref().map(|a| serde_json::to_string(a).unwrap_or_default());
413 let metadata_json =
414 input.metadata.as_ref().map(|m| serde_json::to_string(m).unwrap_or_default());
415
416 tx.execute(
417 "INSERT INTO carts (id, cart_number, customer_id, status, currency,
418 subtotal, tax_amount, shipping_amount, discount_amount, grand_total,
419 customer_email, customer_name, shipping_address, billing_address,
420 notes, metadata, expires_at, created_at, updated_at)
421 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
422 rusqlite::params![
423 id.to_string(),
424 &cart_number,
425 input.customer_id.map(|c| c.to_string()),
426 "active",
427 ¤cy,
428 "0",
429 "0",
430 "0",
431 "0",
432 "0",
433 &input.customer_email,
434 &input.customer_name,
435 &shipping_address_json,
436 &billing_address_json,
437 &input.notes,
438 &metadata_json,
439 expires_at.map(|e| e.to_rfc3339()),
440 now.to_rfc3339(),
441 now.to_rfc3339(),
442 ],
443 )
444 .map_err(map_db_error)?;
445
446 let mut items = vec![];
448 if let Some(input_items) = &input.items {
449 for item_input in input_items {
450 let item = self.add_item_internal(&tx, id, item_input.clone())?;
451 items.push(item);
452 }
453 self.update_cart_totals(&tx, id)?;
454 }
455
456 tx.commit().map_err(map_db_error)?;
457
458 let mut cart = Cart {
459 id,
460 cart_number,
461 customer_id: input.customer_id,
462 status: CartStatus::Active,
463 currency,
464 items,
465 subtotal: Decimal::ZERO,
466 tax_amount: Decimal::ZERO,
467 shipping_amount: Decimal::ZERO,
468 discount_amount: Decimal::ZERO,
469 grand_total: Decimal::ZERO,
470 customer_email: input.customer_email,
471 customer_phone: None,
472 customer_name: input.customer_name,
473 shipping_address: input.shipping_address,
474 billing_address: input.billing_address,
475 billing_same_as_shipping: true,
476 fulfillment_type: None,
477 shipping_method: None,
478 shipping_carrier: None,
479 estimated_delivery: None,
480 payment_method: None,
481 payment_token: None,
482 payment_status: CartPaymentStatus::None,
483 coupon_code: None,
484 discount_description: None,
485 order_id: None,
486 order_number: None,
487 notes: input.notes,
488 metadata: input.metadata,
489 inventory_reserved: false,
490 reservation_expires_at: None,
491 x402_payment: None,
492 expires_at,
493 completed_at: None,
494 created_at: now,
495 updated_at: now,
496 };
497
498 cart.recalculate_totals();
500
501 Ok(cart)
502 }
503
504 fn get(&self, id: CartId) -> Result<Option<Cart>> {
505 let conn = self.conn()?;
506 Self::load_cart_with_conn(&conn, id)
507 }
508
509 fn get_by_number(&self, cart_number: &str) -> Result<Option<Cart>> {
510 let conn = self.conn()?;
511 let result = conn.query_row(
512 "SELECT * FROM carts WHERE cart_number = ?",
513 [cart_number],
514 Self::row_to_cart,
515 );
516
517 match result {
518 Ok(mut cart) => {
519 cart.items = Self::load_cart_items_with_conn(&conn, cart.id)?;
520 Ok(Some(cart))
521 }
522 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
523 Err(e) => Err(map_db_error(e)),
524 }
525 }
526
527 fn update(&self, id: CartId, input: UpdateCart) -> Result<Cart> {
528 let now = Utc::now();
529
530 let mut updates = vec!["updated_at = ?"];
531 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(now.to_rfc3339())];
532
533 if let Some(customer_id) = &input.customer_id {
534 updates.push("customer_id = ?");
535 params.push(Box::new(customer_id.to_string()));
536 }
537 if let Some(email) = &input.customer_email {
538 updates.push("customer_email = ?");
539 params.push(Box::new(email.clone()));
540 }
541 if let Some(phone) = &input.customer_phone {
542 updates.push("customer_phone = ?");
543 params.push(Box::new(phone.clone()));
544 }
545 if let Some(name) = &input.customer_name {
546 updates.push("customer_name = ?");
547 params.push(Box::new(name.clone()));
548 }
549 if let Some(addr) = &input.shipping_address {
550 updates.push("shipping_address = ?");
551 params.push(Box::new(serde_json::to_string(addr).unwrap_or_default()));
552 }
553 if let Some(addr) = &input.billing_address {
554 updates.push("billing_address = ?");
555 params.push(Box::new(serde_json::to_string(addr).unwrap_or_default()));
556 }
557 if let Some(same) = &input.billing_same_as_shipping {
558 updates.push("billing_same_as_shipping = ?");
559 params.push(Box::new(i32::from(*same)));
560 }
561 if let Some(ft) = &input.fulfillment_type {
562 updates.push("fulfillment_type = ?");
563 params.push(Box::new(ft.to_string()));
564 }
565 if let Some(method) = &input.shipping_method {
566 updates.push("shipping_method = ?");
567 params.push(Box::new(method.clone()));
568 }
569 if let Some(carrier) = &input.shipping_carrier {
570 updates.push("shipping_carrier = ?");
571 params.push(Box::new(carrier.clone()));
572 }
573 if let Some(coupon) = &input.coupon_code {
574 updates.push("coupon_code = ?");
575 params.push(Box::new(coupon.clone()));
576 }
577 if let Some(discount_amount) = &input.discount_amount {
578 updates.push("discount_amount = ?");
579 params.push(Box::new(discount_amount.to_string()));
580 }
581 if let Some(description) = &input.discount_description {
582 updates.push("discount_description = ?");
583 params.push(Box::new(description.clone()));
584 }
585 if let Some(notes) = &input.notes {
586 updates.push("notes = ?");
587 params.push(Box::new(notes.clone()));
588 }
589 if let Some(meta) = &input.metadata {
590 updates.push("metadata = ?");
591 params.push(Box::new(serde_json::to_string(meta).unwrap_or_default()));
592 }
593
594 params.push(Box::new(id.to_string()));
595
596 let sql = format!("UPDATE carts SET {} WHERE id = ?", updates.join(", "));
597 let params_refs: Vec<&dyn rusqlite::ToSql> =
598 params.iter().map(std::convert::AsRef::as_ref).collect();
599 {
600 let conn = self.conn()?;
601 conn.execute(&sql, params_refs.as_slice()).map_err(map_db_error)?;
602 }
603
604 self.get(id)?.ok_or(CommerceError::NotFound)
605 }
606
607 fn list(&self, filter: CartFilter) -> Result<Vec<Cart>> {
608 let conn = self.conn()?;
609 let mut sql = "SELECT * FROM carts WHERE 1=1".to_string();
610 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![];
611
612 if let Some(customer_id) = &filter.customer_id {
613 sql.push_str(" AND customer_id = ?");
614 params.push(Box::new(customer_id.to_string()));
615 }
616 if let Some(email) = &filter.customer_email {
617 sql.push_str(" AND customer_email = ?");
618 params.push(Box::new(email.clone()));
619 }
620 if let Some(status) = &filter.status {
621 sql.push_str(" AND status = ?");
622 params.push(Box::new(status.to_string()));
623 }
624 if let Some(has_items) = &filter.has_items {
625 if *has_items {
626 sql.push_str(" AND id IN (SELECT DISTINCT cart_id FROM cart_items)");
627 } else {
628 sql.push_str(" AND id NOT IN (SELECT DISTINCT cart_id FROM cart_items)");
629 }
630 }
631 if let Some(true) = &filter.is_abandoned {
632 sql.push_str(" AND status = 'abandoned'");
633 }
634 if let Some(from) = &filter.created_after {
635 sql.push_str(" AND created_at >= ?");
636 params.push(Box::new(from.to_rfc3339()));
637 }
638 if let Some(to) = &filter.created_before {
639 sql.push_str(" AND created_at <= ?");
640 params.push(Box::new(to.to_rfc3339()));
641 }
642
643 sql.push_str(" ORDER BY created_at DESC");
644
645 crate::sqlite::append_limit_offset(&mut sql, filter.limit, filter.offset);
646
647 let params_refs: Vec<&dyn rusqlite::ToSql> =
648 params.iter().map(std::convert::AsRef::as_ref).collect();
649 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
650
651 let carts = stmt
652 .query_map(params_refs.as_slice(), Self::row_to_cart)
653 .map_err(map_db_error)?
654 .collect::<rusqlite::Result<Vec<_>>>()
655 .map_err(map_db_error)?;
656
657 let ids: Vec<CartId> = carts.iter().map(|c| c.id).collect();
658 let mut items_by_id = Self::load_cart_items_batch(&conn, &ids)?;
659 let mut result = vec![];
660 for mut cart in carts {
661 cart.items = items_by_id.remove(&cart.id).unwrap_or_default();
662 result.push(cart);
663 }
664
665 Ok(result)
666 }
667
668 fn for_customer(&self, customer_id: CustomerId) -> Result<Vec<Cart>> {
669 self.list(CartFilter { customer_id: Some(customer_id), ..Default::default() })
670 }
671
672 fn delete(&self, id: CartId) -> Result<()> {
673 let mut conn = self.conn()?;
674 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
675 tx.execute("DELETE FROM cart_items WHERE cart_id = ?", [id.to_string()])
676 .map_err(map_db_error)?;
677 tx.execute("DELETE FROM carts WHERE id = ?", [id.to_string()]).map_err(map_db_error)?;
678 tx.commit().map_err(map_db_error)?;
679 Ok(())
680 }
681
682 fn add_item(&self, cart_id: CartId, item: AddCartItem) -> Result<CartItem> {
683 if item.quantity <= 0 {
685 return Err(CommerceError::ValidationError(format!(
686 "Item quantity must be positive, got {} for '{}'",
687 item.quantity, item.name
688 )));
689 }
690
691 validate_price(item.unit_price)?;
693 if let Some(original_price) = item.original_price {
694 validate_price(original_price)?;
695 }
696
697 let mut conn = self.conn()?;
698 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
699 let result = self.add_item_internal(&tx, cart_id, item)?;
700 self.update_cart_totals(&tx, cart_id)?;
701 tx.commit().map_err(map_db_error)?;
702 Ok(result)
703 }
704
705 fn update_item(&self, item_id: Uuid, input: UpdateCartItem) -> Result<CartItem> {
706 let mut conn = self.conn()?;
707 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
708 let now = Utc::now();
709
710 let cart_id: String = tx
712 .query_row(
713 "SELECT cart_id FROM cart_items WHERE id = ?",
714 [item_id.to_string()],
715 |row| row.get(0),
716 )
717 .map_err(map_db_error)?;
718
719 let mut updates = vec!["updated_at = ?"];
720 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(now.to_rfc3339())];
721
722 if let Some(qty) = input.quantity {
723 updates.push("quantity = ?");
724 params.push(Box::new(qty));
725 }
726 if let Some(price) = input.unit_price {
727 updates.push("unit_price = ?");
728 params.push(Box::new(price.to_string()));
729 }
730 if let Some(discount) = input.discount_amount {
731 updates.push("discount_amount = ?");
732 params.push(Box::new(discount.to_string()));
733 }
734 if let Some(meta) = &input.metadata {
735 updates.push("metadata = ?");
736 params.push(Box::new(serde_json::to_string(meta).unwrap_or_default()));
737 }
738
739 params.push(Box::new(item_id.to_string()));
740
741 let sql = format!("UPDATE cart_items SET {} WHERE id = ?", updates.join(", "));
742 let params_refs: Vec<&dyn rusqlite::ToSql> =
743 params.iter().map(std::convert::AsRef::as_ref).collect();
744 tx.execute(&sql, params_refs.as_slice()).map_err(map_db_error)?;
745
746 let (qty, unit_price, discount, tax): (i32, String, String, String) = tx
748 .query_row(
749 "SELECT quantity, unit_price, discount_amount, tax_amount FROM cart_items WHERE id = ?",
750 [item_id.to_string()],
751 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
752 )
753 .map_err(map_db_error)?;
754
755 let total = CartItem::calculate_total(
756 qty,
757 parse_decimal_err(&unit_price, "cart_item", "unit_price")?,
758 parse_decimal_err(&discount, "cart_item", "discount_amount")?,
759 parse_decimal_err(&tax, "cart_item", "tax_amount")?,
760 );
761
762 tx.execute(
763 "UPDATE cart_items SET total = ? WHERE id = ?",
764 rusqlite::params![total.to_string(), item_id.to_string()],
765 )
766 .map_err(map_db_error)?;
767
768 let cart_uuid = CartId::from(parse_uuid(&cart_id, "cart_item", "cart_id")?);
770 self.update_cart_totals(&tx, cart_uuid)?;
771
772 let item = tx
774 .query_row("SELECT * FROM cart_items WHERE id = ?", [item_id.to_string()], |row| {
775 let metadata: Option<String> = row.get("metadata")?;
776 Ok(CartItem {
777 id: parse_uuid_row(&row.get::<_, String>("id")?, "cart_item", "id")?,
778 cart_id: CartId::from(parse_uuid_row(
779 &row.get::<_, String>("cart_id")?,
780 "cart_item",
781 "cart_id",
782 )?),
783 product_id: parse_uuid_opt_row(
784 row.get::<_, Option<String>>("product_id")?,
785 "cart_item",
786 "product_id",
787 )?
788 .map(ProductId::from),
789 variant_id: parse_uuid_opt_row(
790 row.get::<_, Option<String>>("variant_id")?,
791 "cart_item",
792 "variant_id",
793 )?,
794 sku: row.get("sku")?,
795 name: row.get("name")?,
796 description: row.get("description")?,
797 image_url: row.get("image_url")?,
798 quantity: row.get("quantity")?,
799 unit_price: parse_decimal_row(
800 &row.get::<_, String>("unit_price")?,
801 "cart_item",
802 "unit_price",
803 )?,
804 original_price: parse_decimal_opt_row(
805 row.get::<_, Option<String>>("original_price")?,
806 "cart_item",
807 "original_price",
808 )?,
809 discount_amount: parse_decimal_row(
810 &row.get::<_, String>("discount_amount")?,
811 "cart_item",
812 "discount_amount",
813 )?,
814 tax_amount: parse_decimal_row(
815 &row.get::<_, String>("tax_amount")?,
816 "cart_item",
817 "tax_amount",
818 )?,
819 total: parse_decimal_row(
820 &row.get::<_, String>("total")?,
821 "cart_item",
822 "total",
823 )?,
824 weight: parse_decimal_opt_row(
825 row.get::<_, Option<String>>("weight")?,
826 "cart_item",
827 "weight",
828 )?,
829 requires_shipping: row.get::<_, i32>("requires_shipping")? == 1,
830 metadata: parse_json_opt_row(metadata, "cart_item", "metadata")?,
831 created_at: parse_datetime_row(
832 &row.get::<_, String>("created_at")?,
833 "cart_item",
834 "created_at",
835 )?,
836 updated_at: parse_datetime_row(
837 &row.get::<_, String>("updated_at")?,
838 "cart_item",
839 "updated_at",
840 )?,
841 })
842 })
843 .map_err(map_db_error)?;
844
845 tx.commit().map_err(map_db_error)?;
846
847 Ok(item)
848 }
849
850 fn remove_item(&self, item_id: Uuid) -> Result<()> {
851 let mut conn = self.conn()?;
852 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
853
854 let cart_id: String = tx
856 .query_row(
857 "SELECT cart_id FROM cart_items WHERE id = ?",
858 [item_id.to_string()],
859 |row| row.get(0),
860 )
861 .map_err(map_db_error)?;
862
863 tx.execute("DELETE FROM cart_items WHERE id = ?", [item_id.to_string()])
864 .map_err(map_db_error)?;
865
866 let cart_uuid = CartId::from(parse_uuid(&cart_id, "cart_item", "cart_id")?);
867 self.update_cart_totals(&tx, cart_uuid)?;
868 tx.commit().map_err(map_db_error)?;
869
870 Ok(())
871 }
872
873 fn get_items(&self, cart_id: CartId) -> Result<Vec<CartItem>> {
874 let conn = self.conn()?;
875 Self::load_cart_items_with_conn(&conn, cart_id)
876 }
877
878 fn clear_items(&self, cart_id: CartId) -> Result<()> {
879 let mut conn = self.conn()?;
880 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
881 tx.execute("DELETE FROM cart_items WHERE cart_id = ?", [cart_id.to_string()])
882 .map_err(map_db_error)?;
883 self.update_cart_totals(&tx, cart_id)?;
884 tx.commit().map_err(map_db_error)?;
885 Ok(())
886 }
887
888 fn set_shipping_address(&self, id: CartId, address: CartAddress) -> Result<Cart> {
889 let address_json = serde_json::to_string(&address).unwrap_or_default();
890
891 {
892 let conn = self.conn()?;
893 conn.execute(
894 "UPDATE carts SET shipping_address = ?, updated_at = ? WHERE id = ?",
895 rusqlite::params![address_json, Utc::now().to_rfc3339(), id.to_string()],
896 )
897 .map_err(map_db_error)?;
898 }
899
900 self.get(id)?.ok_or(CommerceError::NotFound)
901 }
902
903 fn set_billing_address(&self, id: CartId, address: CartAddress) -> Result<Cart> {
904 let address_json = serde_json::to_string(&address).unwrap_or_default();
905
906 {
907 let conn = self.conn()?;
908 conn.execute(
909 "UPDATE carts SET billing_address = ?, billing_same_as_shipping = 0, updated_at = ? WHERE id = ?",
910 rusqlite::params![address_json, Utc::now().to_rfc3339(), id.to_string()],
911 )
912 .map_err(map_db_error)?;
913 }
914
915 self.get(id)?.ok_or(CommerceError::NotFound)
916 }
917
918 fn set_shipping(&self, id: CartId, shipping: SetCartShipping) -> Result<Cart> {
919 let address_json = serde_json::to_string(&shipping.shipping_address).unwrap_or_default();
920 let shipping_amount = shipping.shipping_amount.unwrap_or_default();
921
922 {
923 let conn = self.conn()?;
924 conn.execute(
925 "UPDATE carts SET shipping_address = ?, shipping_method = ?, shipping_carrier = ?,
926 shipping_amount = ?, updated_at = ? WHERE id = ?",
927 rusqlite::params![
928 address_json,
929 shipping.shipping_method,
930 shipping.shipping_carrier,
931 shipping_amount.to_string(),
932 Utc::now().to_rfc3339(),
933 id.to_string()
934 ],
935 )
936 .map_err(map_db_error)?;
937 }
938
939 self.recalculate(id)
941 }
942
943 fn get_shipping_rates(&self, _id: CartId) -> Result<Vec<ShippingRate>> {
944 Ok(vec![
947 ShippingRate {
948 id: "standard".to_string(),
949 carrier: "USPS".to_string(),
950 service: "Ground".to_string(),
951 description: Some("Standard shipping (5-7 business days)".to_string()),
952 price: Decimal::new(599, 2), currency: CurrencyCode::default(),
954 estimated_days: Some(7),
955 estimated_delivery: None,
956 },
957 ShippingRate {
958 id: "express".to_string(),
959 carrier: "UPS".to_string(),
960 service: "Express".to_string(),
961 description: Some("Express shipping (2-3 business days)".to_string()),
962 price: Decimal::new(1499, 2), currency: CurrencyCode::default(),
964 estimated_days: Some(3),
965 estimated_delivery: None,
966 },
967 ShippingRate {
968 id: "overnight".to_string(),
969 carrier: "FedEx".to_string(),
970 service: "Overnight".to_string(),
971 description: Some("Next business day delivery".to_string()),
972 price: Decimal::new(2999, 2), currency: CurrencyCode::default(),
974 estimated_days: Some(1),
975 estimated_delivery: None,
976 },
977 ])
978 }
979
980 fn set_payment(&self, id: CartId, payment: SetCartPayment) -> Result<Cart> {
981 let billing_json = payment
982 .billing_address
983 .as_ref()
984 .map(|addr| serde_json::to_string(addr).unwrap_or_default());
985
986 {
987 let conn = self.conn()?;
988 conn.execute(
989 "UPDATE carts SET payment_method = ?, payment_token = ?, payment_status = 'method_selected',
990 billing_address = COALESCE(?, billing_address), updated_at = ? WHERE id = ?",
991 rusqlite::params![
992 payment.payment_method,
993 payment.payment_token,
994 billing_json,
995 Utc::now().to_rfc3339(),
996 id.to_string()
997 ],
998 )
999 .map_err(map_db_error)?;
1000 }
1001
1002 self.get(id)?.ok_or(CommerceError::NotFound)
1003 }
1004
1005 fn set_x402_payment(&self, id: CartId, payment: SetCartX402Payment) -> Result<Cart> {
1006 let conn = self.conn()?;
1007
1008 conn.execute(
1009 "UPDATE carts SET
1010 x402_payer_address = ?, x402_network = ?, x402_asset = ?,
1011 x402_status = ?, payment_method = 'x402', updated_at = ?
1012 WHERE id = ?",
1013 rusqlite::params![
1014 payment.payer_address,
1015 payment.network.to_string(),
1016 payment.asset.to_string().to_lowercase(),
1017 X402IntentStatus::Created.to_string(),
1018 Utc::now().to_rfc3339(),
1019 id.to_string()
1020 ],
1021 )
1022 .map_err(map_db_error)?;
1023
1024 self.get(id)?.ok_or(CommerceError::NotFound)
1025 }
1026
1027 fn complete_with_x402(&self, id: CartId, payee_address: &str) -> Result<X402CheckoutResult> {
1028 use rust_decimal::prelude::ToPrimitive;
1029
1030 let cart = self.get(id)?.ok_or(CommerceError::NotFound)?;
1031
1032 if cart.status == CartStatus::Completed {
1033 if let (Some(order_id), Some(order_number)) = (cart.order_id, cart.order_number.clone())
1034 {
1035 return Ok(X402CheckoutResult::Completed(CheckoutResult {
1036 cart_id: id,
1037 order_id,
1038 order_number,
1039 payment_id: None,
1040 total_charged: cart.grand_total,
1041 currency: cart.currency,
1042 }));
1043 }
1044 }
1045
1046 if !cart.is_checkoutable_status() {
1048 return Err(CommerceError::Conflict(format!(
1049 "Cart cannot be checked out in status: {}",
1050 cart.status
1051 )));
1052 }
1053
1054 if !cart.is_ready_for_checkout() {
1056 return Err(CommerceError::ValidationError(
1057 "Cart is not ready for checkout - ensure items, customer info, and shipping address are set".to_string(),
1058 ));
1059 }
1060
1061 let x402_payment = cart.x402_payment.as_ref().ok_or_else(|| {
1063 CommerceError::ValidationError(
1064 "x402 payment not configured. Call set_x402_payment first".to_string(),
1065 )
1066 })?;
1067
1068 let decimals = x402_payment.asset.decimals();
1070 let multiplier = rust_decimal::Decimal::from(10u64.pow(u32::from(decimals)));
1071 let amount_scaled = cart.grand_total * multiplier;
1072 let amount = amount_scaled.to_u64().unwrap_or(0);
1073 let amount_display = format!("{:.6} {}", cart.grand_total, x402_payment.asset);
1074
1075 if let Some(intent_id) = x402_payment.intent_id {
1077 let conn = self.conn()?;
1079
1080 type IntentStatusRow = (String, Option<String>, Option<i64>, Option<String>);
1081
1082 let status_result: Option<IntentStatusRow> = conn
1083 .query_row(
1084 "SELECT status, signing_hash, sequence_number, batch_id FROM x402_payment_intents WHERE id = ?",
1085 [intent_id.to_string()],
1086 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
1087 )
1088 .optional()
1089 .map_err(map_db_error)?;
1090
1091 if let Some((status_str, signing_hash, seq_num, batch_id_str)) = status_result {
1092 let status: X402IntentStatus = status_str.parse().unwrap_or_default();
1093 match status {
1094 X402IntentStatus::Settled => {
1095 return self.finalize_x402_checkout(id);
1097 }
1098 X402IntentStatus::Signed
1099 | X402IntentStatus::Sequenced
1100 | X402IntentStatus::Batched => {
1101 return Ok(X402CheckoutResult::AwaitingSettlement(
1103 X402AwaitingSettlementData {
1104 cart_id: id,
1105 intent_id,
1106 status,
1107 sequence_number: seq_num.map(|n| n as u64),
1108 batch_id: batch_id_str.and_then(|s| s.parse().ok()),
1109 },
1110 ));
1111 }
1112 X402IntentStatus::Created => {
1113 return Ok(X402CheckoutResult::IntentCreated(X402IntentCreatedData {
1115 cart_id: id,
1116 intent_id,
1117 signing_hash: signing_hash.unwrap_or_default(),
1118 amount,
1119 amount_display,
1120 asset: x402_payment.asset,
1121 network: x402_payment.network,
1122 payee_address: payee_address.to_string(),
1123 valid_until: 0, nonce: 0,
1125 }));
1126 }
1127 X402IntentStatus::Expired
1128 | X402IntentStatus::Failed
1129 | X402IntentStatus::Cancelled
1130 | _ => {
1131 }
1133 }
1134 }
1135 }
1136
1137 let chain_id = x402_payment.network.chain_id();
1139 Ok(X402CheckoutResult::PaymentRequired(X402PaymentRequiredData {
1140 cart_id: id,
1141 payee_address: payee_address.to_string(),
1142 amount,
1143 amount_display,
1144 asset: x402_payment.asset,
1145 network: x402_payment.network,
1146 chain_id,
1147 valid_seconds: 3600, }))
1149 }
1150
1151 fn apply_discount(&self, id: CartId, coupon_code: &str) -> Result<Cart> {
1152 let cart = self.get(id)?.ok_or(CommerceError::NotFound)?;
1154
1155 let promo_repo = SqlitePromotionRepository::new(self.pool.clone());
1157 let coupon = promo_repo.get_coupon_by_code(coupon_code)?.ok_or_else(|| {
1158 CommerceError::ValidationError(format!("Invalid coupon code: {coupon_code}"))
1159 })?;
1160
1161 let promotion = promo_repo
1162 .get(coupon.promotion_id)?
1163 .ok_or_else(|| CommerceError::ValidationError("Promotion not found".into()))?;
1164
1165 let subtotal = cart.subtotal;
1167 let discount_amount = match promotion.promotion_type {
1168 PromotionType::PercentageOff => {
1169 let percentage = promotion.percentage_off.unwrap_or(Decimal::ZERO);
1170 let discount = subtotal * percentage;
1171 if let Some(max) = promotion.max_discount_amount {
1173 discount.min(max)
1174 } else {
1175 discount
1176 }
1177 }
1178 PromotionType::FixedAmountOff => {
1179 promotion.fixed_amount_off.unwrap_or(Decimal::ZERO).min(subtotal)
1180 }
1181 _ => Decimal::ZERO, };
1183
1184 let discount_description = Some(promotion.name);
1185
1186 {
1188 let conn = self.conn()?;
1189 conn.execute(
1190 "UPDATE carts SET coupon_code = ?, discount_amount = ?, discount_description = ?, updated_at = ? WHERE id = ?",
1191 rusqlite::params![
1192 coupon_code,
1193 discount_amount.to_string(),
1194 discount_description,
1195 Utc::now().to_rfc3339(),
1196 id.to_string()
1197 ],
1198 )
1199 .map_err(map_db_error)?;
1200 }
1201
1202 self.recalculate(id)
1204 }
1205
1206 fn remove_discount(&self, id: CartId) -> Result<Cart> {
1207 {
1208 let conn = self.conn()?;
1209 conn.execute(
1210 "UPDATE carts SET coupon_code = NULL, discount_amount = '0', discount_description = NULL,
1211 updated_at = ? WHERE id = ?",
1212 rusqlite::params![Utc::now().to_rfc3339(), id.to_string()],
1213 )
1214 .map_err(map_db_error)?;
1215 }
1216
1217 self.recalculate(id)
1218 }
1219
1220 fn mark_ready_for_payment(&self, id: CartId) -> Result<Cart> {
1221 let cart = self.get(id)?.ok_or(CommerceError::NotFound)?;
1222
1223 if !cart.is_ready_for_checkout() {
1224 return Err(CommerceError::ValidationError(
1225 "Cart is not ready for checkout".to_string(),
1226 ));
1227 }
1228
1229 {
1230 let conn = self.conn()?;
1231 conn.execute(
1232 "UPDATE carts SET status = 'ready_for_payment', updated_at = ? WHERE id = ?",
1233 rusqlite::params![Utc::now().to_rfc3339(), id.to_string()],
1234 )
1235 .map_err(map_db_error)?;
1236 }
1237
1238 self.get(id)?.ok_or(CommerceError::NotFound)
1239 }
1240
1241 fn begin_checkout(&self, id: CartId) -> Result<Cart> {
1242 {
1243 let conn = self.conn()?;
1244 conn.execute(
1245 "UPDATE carts SET status = 'payment_pending', updated_at = ? WHERE id = ?",
1246 rusqlite::params![Utc::now().to_rfc3339(), id.to_string()],
1247 )
1248 .map_err(map_db_error)?;
1249 }
1250
1251 self.get(id)?.ok_or(CommerceError::NotFound)
1252 }
1253
1254 fn complete(&self, id: CartId) -> Result<CheckoutResult> {
1255 with_immediate_transaction(&self.pool, |tx| {
1256 self.complete_checkout_in_tx(tx, id, false, false)
1257 })
1258 }
1259
1260 fn complete_settled_externally(&self, id: CartId) -> Result<CheckoutResult> {
1261 with_immediate_transaction(&self.pool, |tx| {
1262 self.complete_checkout_in_tx(tx, id, false, true)
1263 })
1264 }
1265
1266 fn cancel(&self, id: CartId) -> Result<Cart> {
1267 {
1268 let conn = self.conn()?;
1269 conn.execute(
1270 "UPDATE carts SET status = 'cancelled', updated_at = ? WHERE id = ?",
1271 rusqlite::params![Utc::now().to_rfc3339(), id.to_string()],
1272 )
1273 .map_err(map_db_error)?;
1274 }
1275
1276 self.get(id)?.ok_or(CommerceError::NotFound)
1277 }
1278
1279 fn abandon(&self, id: CartId) -> Result<Cart> {
1280 {
1281 let conn = self.conn()?;
1282 conn.execute(
1283 "UPDATE carts SET status = 'abandoned', updated_at = ? WHERE id = ?",
1284 rusqlite::params![Utc::now().to_rfc3339(), id.to_string()],
1285 )
1286 .map_err(map_db_error)?;
1287 }
1288
1289 self.get(id)?.ok_or(CommerceError::NotFound)
1290 }
1291
1292 fn expire(&self, id: CartId) -> Result<Cart> {
1293 {
1294 let conn = self.conn()?;
1295 conn.execute(
1296 "UPDATE carts SET status = 'expired', updated_at = ? WHERE id = ?",
1297 rusqlite::params![Utc::now().to_rfc3339(), id.to_string()],
1298 )
1299 .map_err(map_db_error)?;
1300 }
1301
1302 self.get(id)?.ok_or(CommerceError::NotFound)
1303 }
1304
1305 fn reserve_inventory(&self, id: CartId) -> Result<Cart> {
1306 let reservation_expires = Utc::now() + Duration::minutes(15);
1307
1308 {
1309 let conn = self.conn()?;
1310 conn.execute(
1311 "UPDATE carts SET inventory_reserved = 1, reservation_expires_at = ?, updated_at = ? WHERE id = ?",
1312 rusqlite::params![
1313 reservation_expires.to_rfc3339(),
1314 Utc::now().to_rfc3339(),
1315 id.to_string()
1316 ],
1317 )
1318 .map_err(map_db_error)?;
1319 }
1320
1321 self.get(id)?.ok_or(CommerceError::NotFound)
1322 }
1323
1324 fn release_inventory(&self, id: CartId) -> Result<Cart> {
1325 {
1326 let conn = self.conn()?;
1327 conn.execute(
1328 "UPDATE carts SET inventory_reserved = 0, reservation_expires_at = NULL, updated_at = ? WHERE id = ?",
1329 rusqlite::params![Utc::now().to_rfc3339(), id.to_string()],
1330 )
1331 .map_err(map_db_error)?;
1332 }
1333
1334 self.get(id)?.ok_or(CommerceError::NotFound)
1335 }
1336
1337 fn recalculate(&self, id: CartId) -> Result<Cart> {
1338 {
1339 let conn = self.conn()?;
1340 self.update_cart_totals(&conn, id)?;
1341 }
1342
1343 self.get(id)?.ok_or(CommerceError::NotFound)
1344 }
1345
1346 fn set_tax(&self, id: CartId, tax_amount: Decimal) -> Result<Cart> {
1347 {
1348 let conn = self.conn()?;
1349 conn.execute(
1350 "UPDATE carts SET tax_amount = ?, updated_at = ? WHERE id = ?",
1351 rusqlite::params![tax_amount.to_string(), Utc::now().to_rfc3339(), id.to_string()],
1352 )
1353 .map_err(map_db_error)?;
1354 }
1355
1356 self.recalculate(id)
1357 }
1358
1359 fn get_abandoned(&self) -> Result<Vec<Cart>> {
1360 self.list(CartFilter { status: Some(CartStatus::Abandoned), ..Default::default() })
1361 }
1362
1363 fn get_expired(&self) -> Result<Vec<Cart>> {
1364 let now = Utc::now();
1365
1366 {
1368 let conn = self.conn()?;
1369 conn.execute(
1370 "UPDATE carts SET status = 'expired' WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at < ?",
1371 [now.to_rfc3339()],
1372 )
1373 .map_err(map_db_error)?;
1374 }
1375
1376 self.list(CartFilter { status: Some(CartStatus::Expired), ..Default::default() })
1377 }
1378
1379 fn count(&self, filter: CartFilter) -> Result<u64> {
1380 let conn = self.conn()?;
1381 let mut sql = "SELECT COUNT(*) FROM carts WHERE 1=1".to_string();
1382 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![];
1383
1384 if let Some(customer_id) = &filter.customer_id {
1385 sql.push_str(" AND customer_id = ?");
1386 params.push(Box::new(customer_id.to_string()));
1387 }
1388 if let Some(status) = &filter.status {
1389 sql.push_str(" AND status = ?");
1390 params.push(Box::new(status.to_string()));
1391 }
1392
1393 let params_refs: Vec<&dyn rusqlite::ToSql> =
1394 params.iter().map(std::convert::AsRef::as_ref).collect();
1395 let count: i64 =
1396 conn.query_row(&sql, params_refs.as_slice(), |row| row.get(0)).map_err(map_db_error)?;
1397
1398 Ok(count as u64)
1399 }
1400
1401 fn create_batch(&self, inputs: Vec<CreateCart>) -> Result<BatchResult<Cart>> {
1404 validate_batch_size(&inputs)?;
1405 let mut result = BatchResult::with_capacity(inputs.len());
1406
1407 for (index, input) in inputs.into_iter().enumerate() {
1408 match self.create(input) {
1409 Ok(cart) => result.record_success(cart),
1410 Err(e) => result.record_failure(index, None, &e),
1411 }
1412 }
1413
1414 Ok(result)
1415 }
1416
1417 fn create_batch_atomic(&self, inputs: Vec<CreateCart>) -> Result<Vec<Cart>> {
1418 validate_batch_size(&inputs)?;
1419 if inputs.is_empty() {
1420 return Ok(vec![]);
1421 }
1422
1423 let mut conn = self.conn()?;
1424 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1425 let mut results = Vec::with_capacity(inputs.len());
1426
1427 for input in inputs {
1428 let id = CartId::new();
1429 let cart_number = Self::generate_cart_number();
1430 let now = Utc::now();
1431 let currency = input.currency.unwrap_or_default();
1432
1433 let expires_at = input.expires_in_minutes.map(|mins| now + Duration::minutes(mins));
1434
1435 let shipping_address_json = input
1436 .shipping_address
1437 .as_ref()
1438 .map(|a| serde_json::to_string(a).unwrap_or_default());
1439 let billing_address_json = input
1440 .billing_address
1441 .as_ref()
1442 .map(|a| serde_json::to_string(a).unwrap_or_default());
1443 let metadata_json =
1444 input.metadata.as_ref().map(|m| serde_json::to_string(m).unwrap_or_default());
1445
1446 tx.execute(
1447 "INSERT INTO carts (id, cart_number, customer_id, status, currency,
1448 subtotal, tax_amount, shipping_amount, discount_amount, grand_total,
1449 customer_email, customer_name, shipping_address, billing_address,
1450 notes, metadata, expires_at, created_at, updated_at)
1451 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1452 rusqlite::params![
1453 id.to_string(),
1454 &cart_number,
1455 input.customer_id.map(|c| c.to_string()),
1456 "active",
1457 ¤cy,
1458 "0",
1459 "0",
1460 "0",
1461 "0",
1462 "0",
1463 &input.customer_email,
1464 &input.customer_name,
1465 &shipping_address_json,
1466 &billing_address_json,
1467 &input.notes,
1468 &metadata_json,
1469 expires_at.map(|e| e.to_rfc3339()),
1470 now.to_rfc3339(),
1471 now.to_rfc3339(),
1472 ],
1473 )
1474 .map_err(map_db_error)?;
1475
1476 let mut items = vec![];
1478 if let Some(input_items) = &input.items {
1479 for item_input in input_items {
1480 let item = self.add_item_internal(&tx, id, item_input.clone())?;
1481 items.push(item);
1482 }
1483 self.update_cart_totals(&tx, id)?;
1484 }
1485
1486 let mut cart = Cart {
1487 id,
1488 cart_number,
1489 customer_id: input.customer_id,
1490 status: CartStatus::Active,
1491 currency,
1492 items,
1493 subtotal: Decimal::ZERO,
1494 tax_amount: Decimal::ZERO,
1495 shipping_amount: Decimal::ZERO,
1496 discount_amount: Decimal::ZERO,
1497 grand_total: Decimal::ZERO,
1498 customer_email: input.customer_email,
1499 customer_phone: None,
1500 customer_name: input.customer_name,
1501 shipping_address: input.shipping_address,
1502 billing_address: input.billing_address,
1503 billing_same_as_shipping: true,
1504 fulfillment_type: None,
1505 shipping_method: None,
1506 shipping_carrier: None,
1507 estimated_delivery: None,
1508 payment_method: None,
1509 payment_token: None,
1510 payment_status: CartPaymentStatus::None,
1511 coupon_code: None,
1512 discount_description: None,
1513 order_id: None,
1514 order_number: None,
1515 notes: input.notes,
1516 metadata: input.metadata,
1517 inventory_reserved: false,
1518 reservation_expires_at: None,
1519 expires_at,
1520 completed_at: None,
1521 x402_payment: None,
1522 created_at: now,
1523 updated_at: now,
1524 };
1525
1526 cart.recalculate_totals();
1527 results.push(cart);
1528 }
1529
1530 tx.commit().map_err(map_db_error)?;
1531 Ok(results)
1532 }
1533
1534 fn update_batch(&self, updates: Vec<(CartId, UpdateCart)>) -> Result<BatchResult<Cart>> {
1535 validate_batch_size(&updates)?;
1536 let mut result = BatchResult::with_capacity(updates.len());
1537
1538 for (index, (id, input)) in updates.into_iter().enumerate() {
1539 match self.update(id, input) {
1540 Ok(cart) => result.record_success(cart),
1541 Err(e) => result.record_failure(index, Some(id.to_string()), &e),
1542 }
1543 }
1544
1545 Ok(result)
1546 }
1547
1548 fn update_batch_atomic(&self, updates: Vec<(CartId, UpdateCart)>) -> Result<Vec<Cart>> {
1549 validate_batch_size(&updates)?;
1550 if updates.is_empty() {
1551 return Ok(vec![]);
1552 }
1553
1554 let mut conn = self.conn()?;
1555 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1556 let mut results = Vec::with_capacity(updates.len());
1557
1558 for (id, input) in updates {
1559 let now = Utc::now();
1560
1561 let mut update_parts = vec!["updated_at = ?"];
1562 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(now.to_rfc3339())];
1563
1564 if let Some(customer_id) = &input.customer_id {
1565 update_parts.push("customer_id = ?");
1566 params.push(Box::new(customer_id.to_string()));
1567 }
1568 if let Some(email) = &input.customer_email {
1569 update_parts.push("customer_email = ?");
1570 params.push(Box::new(email.clone()));
1571 }
1572 if let Some(phone) = &input.customer_phone {
1573 update_parts.push("customer_phone = ?");
1574 params.push(Box::new(phone.clone()));
1575 }
1576 if let Some(name) = &input.customer_name {
1577 update_parts.push("customer_name = ?");
1578 params.push(Box::new(name.clone()));
1579 }
1580 if let Some(addr) = &input.shipping_address {
1581 update_parts.push("shipping_address = ?");
1582 params.push(Box::new(serde_json::to_string(addr).unwrap_or_default()));
1583 }
1584 if let Some(addr) = &input.billing_address {
1585 update_parts.push("billing_address = ?");
1586 params.push(Box::new(serde_json::to_string(addr).unwrap_or_default()));
1587 }
1588 if let Some(same) = &input.billing_same_as_shipping {
1589 update_parts.push("billing_same_as_shipping = ?");
1590 params.push(Box::new(i32::from(*same)));
1591 }
1592 if let Some(ft) = &input.fulfillment_type {
1593 update_parts.push("fulfillment_type = ?");
1594 params.push(Box::new(ft.to_string()));
1595 }
1596 if let Some(method) = &input.shipping_method {
1597 update_parts.push("shipping_method = ?");
1598 params.push(Box::new(method.clone()));
1599 }
1600 if let Some(carrier) = &input.shipping_carrier {
1601 update_parts.push("shipping_carrier = ?");
1602 params.push(Box::new(carrier.clone()));
1603 }
1604 if let Some(coupon) = &input.coupon_code {
1605 update_parts.push("coupon_code = ?");
1606 params.push(Box::new(coupon.clone()));
1607 }
1608 if let Some(notes) = &input.notes {
1609 update_parts.push("notes = ?");
1610 params.push(Box::new(notes.clone()));
1611 }
1612 if let Some(meta) = &input.metadata {
1613 update_parts.push("metadata = ?");
1614 params.push(Box::new(serde_json::to_string(meta).unwrap_or_default()));
1615 }
1616
1617 params.push(Box::new(id.to_string()));
1618
1619 let sql = format!("UPDATE carts SET {} WHERE id = ?", update_parts.join(", "));
1620 let params_refs: Vec<&dyn rusqlite::ToSql> =
1621 params.iter().map(std::convert::AsRef::as_ref).collect();
1622 let rows_affected = tx.execute(&sql, params_refs.as_slice()).map_err(map_db_error)?;
1623
1624 if rows_affected == 0 {
1625 return Err(CommerceError::NotFound);
1626 }
1627
1628 let cart = tx
1630 .query_row("SELECT * FROM carts WHERE id = ?", [id.to_string()], Self::row_to_cart)
1631 .map_err(map_db_error)?;
1632
1633 results.push(cart);
1634 }
1635
1636 tx.commit().map_err(map_db_error)?;
1637
1638 let conn = self.conn()?;
1640 let ids: Vec<CartId> = results.iter().map(|c| c.id).collect();
1641 let mut items_by_id = Self::load_cart_items_batch(&conn, &ids)?;
1642 for cart in &mut results {
1643 cart.items = items_by_id.remove(&cart.id).unwrap_or_default();
1644 }
1645
1646 Ok(results)
1647 }
1648
1649 fn delete_batch(&self, ids: Vec<CartId>) -> Result<BatchResult<CartId>> {
1650 validate_batch_size(&ids)?;
1651 let mut result = BatchResult::with_capacity(ids.len());
1652
1653 for (index, id) in ids.into_iter().enumerate() {
1654 match self.delete(id) {
1655 Ok(()) => result.record_success(id),
1656 Err(e) => result.record_failure(index, Some(id.to_string()), &e),
1657 }
1658 }
1659
1660 Ok(result)
1661 }
1662
1663 fn delete_batch_atomic(&self, ids: Vec<CartId>) -> Result<()> {
1664 validate_batch_size(&ids)?;
1665 if ids.is_empty() {
1666 return Ok(());
1667 }
1668
1669 let mut conn = self.conn()?;
1670 let tx = super::begin_immediate(&mut conn).map_err(map_db_error)?;
1671
1672 let raw_ids: Vec<Uuid> = ids.iter().map(|id| id.into_uuid()).collect();
1673 let placeholders = build_in_clause(ids.len());
1674 let params = uuid_params(&raw_ids);
1675 let params_refs = params_refs(¶ms);
1676
1677 let sql = format!("DELETE FROM cart_items WHERE cart_id IN ({placeholders})");
1679 tx.execute(&sql, params_refs.as_slice()).map_err(map_db_error)?;
1680
1681 let sql = format!("DELETE FROM carts WHERE id IN ({placeholders})");
1683 tx.execute(&sql, params_refs.as_slice()).map_err(map_db_error)?;
1684
1685 tx.commit().map_err(map_db_error)?;
1686 Ok(())
1687 }
1688
1689 fn get_batch(&self, ids: Vec<CartId>) -> Result<Vec<Cart>> {
1690 validate_batch_size(&ids)?;
1691 if ids.is_empty() {
1692 return Ok(vec![]);
1693 }
1694
1695 let conn = self.conn()?;
1696 let raw_ids: Vec<Uuid> = ids.iter().map(|id| id.into_uuid()).collect();
1697 let placeholders = build_in_clause(ids.len());
1698 let sql = format!("SELECT * FROM carts WHERE id IN ({placeholders})");
1699
1700 let params = uuid_params(&raw_ids);
1701 let params_refs = params_refs(¶ms);
1702
1703 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
1704 let carts = stmt
1705 .query_map(params_refs.as_slice(), Self::row_to_cart)
1706 .map_err(map_db_error)?
1707 .collect::<rusqlite::Result<Vec<_>>>()
1708 .map_err(map_db_error)?;
1709
1710 let cart_ids: Vec<CartId> = carts.iter().map(|c| c.id).collect();
1712 let mut items_by_id = Self::load_cart_items_batch(&conn, &cart_ids)?;
1713 let mut result = vec![];
1714 for mut cart in carts {
1715 cart.items = items_by_id.remove(&cart.id).unwrap_or_default();
1716 result.push(cart);
1717 }
1718
1719 Ok(result)
1720 }
1721}
1722
1723impl SqliteCartRepository {
1725 fn add_item_internal(
1726 &self,
1727 conn: &rusqlite::Connection,
1728 cart_id: CartId,
1729 item: AddCartItem,
1730 ) -> Result<CartItem> {
1731 let item_id = Uuid::new_v4();
1732 let now = Utc::now();
1733 let requires_shipping = item.requires_shipping.unwrap_or(true);
1734
1735 let total =
1736 CartItem::calculate_total(item.quantity, item.unit_price, Decimal::ZERO, Decimal::ZERO);
1737
1738 let metadata_json =
1739 item.metadata.as_ref().map(|m| serde_json::to_string(m).unwrap_or_default());
1740
1741 conn.execute(
1742 "INSERT INTO cart_items (id, cart_id, product_id, variant_id, sku, name, description,
1743 image_url, quantity, unit_price, original_price, discount_amount,
1744 tax_amount, total, weight, requires_shipping, metadata,
1745 created_at, updated_at)
1746 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1747 rusqlite::params![
1748 item_id.to_string(),
1749 cart_id.to_string(),
1750 item.product_id.map(|p| p.to_string()),
1751 item.variant_id.map(|v| v.to_string()),
1752 item.sku,
1753 item.name,
1754 item.description,
1755 item.image_url,
1756 item.quantity,
1757 item.unit_price.to_string(),
1758 item.original_price.map(|p| p.to_string()),
1759 "0",
1760 "0",
1761 total.to_string(),
1762 item.weight.map(|w| w.to_string()),
1763 i32::from(requires_shipping),
1764 metadata_json,
1765 now.to_rfc3339(),
1766 now.to_rfc3339(),
1767 ],
1768 )
1769 .map_err(map_db_error)?;
1770
1771 Ok(CartItem {
1772 id: item_id,
1773 cart_id,
1774 product_id: item.product_id,
1775 variant_id: item.variant_id,
1776 sku: item.sku,
1777 name: item.name,
1778 description: item.description,
1779 image_url: item.image_url,
1780 quantity: item.quantity,
1781 unit_price: item.unit_price,
1782 original_price: item.original_price,
1783 discount_amount: Decimal::ZERO,
1784 tax_amount: Decimal::ZERO,
1785 total,
1786 weight: item.weight,
1787 requires_shipping,
1788 metadata: item.metadata,
1789 created_at: now,
1790 updated_at: now,
1791 })
1792 }
1793
1794 fn resolve_customer_id_with_conn(
1795 conn: &rusqlite::Connection,
1796 cart: &Cart,
1797 ) -> Result<CustomerId> {
1798 if let Some(customer_id) = cart.customer_id {
1799 return Ok(customer_id);
1800 }
1801
1802 let email = cart.customer_email.as_deref().ok_or_else(|| {
1803 CommerceError::ValidationError(
1804 "Cart must have a customer_id or customer_email to create an order".to_string(),
1805 )
1806 })?;
1807
1808 validate_email(email)?;
1809
1810 let (first_name, last_name) = split_customer_name(cart.customer_name.as_deref());
1811 validate_required_text("customer.first_name", &first_name, 100)?;
1812 validate_required_text("customer.last_name", &last_name, 100)?;
1813 if let Some(phone) = &cart.customer_phone {
1814 validate_phone(phone)?;
1815 }
1816
1817 let result = conn.query_row("SELECT id FROM customers WHERE email = ?", [email], |row| {
1818 row.get::<_, String>(0)
1819 });
1820
1821 match result {
1822 Ok(id) => Ok(CustomerId::from(parse_uuid(&id, "customer", "id")?)),
1823 Err(rusqlite::Error::QueryReturnedNoRows) => {
1824 let customer_id = CustomerId::new();
1825 let now = Utc::now();
1826 conn.execute(
1827 "INSERT INTO customers (id, email, first_name, last_name, phone, status,
1828 accepts_marketing, email_verified, tags, metadata,
1829 created_at, updated_at)
1830 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1831 rusqlite::params![
1832 customer_id.to_string(),
1833 email,
1834 &first_name,
1835 &last_name,
1836 &cart.customer_phone,
1837 "active",
1838 0,
1839 0,
1840 "[]",
1841 Option::<String>::None,
1842 now.to_rfc3339(),
1843 now.to_rfc3339(),
1844 ],
1845 )
1846 .map_err(map_db_error)?;
1847 Ok(customer_id)
1848 }
1849 Err(error) => Err(map_db_error(error)),
1850 }
1851 }
1852
1853 fn resolve_customer_id_in_tx(
1854 tx: &rusqlite::Transaction<'_>,
1855 cart: &Cart,
1856 ) -> std::result::Result<CustomerId, rusqlite::Error> {
1857 Self::resolve_customer_id_with_conn(tx, cart)
1858 .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))
1859 }
1860
1861 fn complete_checkout_in_tx(
1868 &self,
1869 tx: &rusqlite::Transaction<'_>,
1870 cart_id: CartId,
1871 x402_settled: bool,
1872 mark_paid: bool,
1873 ) -> std::result::Result<CheckoutResult, rusqlite::Error> {
1874 let mut cart = match tx.query_row(
1875 "SELECT * FROM carts WHERE id = ?",
1876 [cart_id.to_string()],
1877 Self::row_to_cart,
1878 ) {
1879 Ok(cart) => cart,
1880 Err(rusqlite::Error::QueryReturnedNoRows) => {
1881 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
1882 CommerceError::NotFound,
1883 )));
1884 }
1885 Err(error) => return Err(error),
1886 };
1887 cart.items = Self::load_cart_items_with_conn(tx, cart_id)
1888 .map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?;
1889
1890 if cart.status == CartStatus::Completed {
1891 if let (Some(order_id), Some(order_number)) = (cart.order_id, cart.order_number.clone())
1892 {
1893 return Ok(CheckoutResult {
1894 cart_id,
1895 order_id,
1896 order_number,
1897 payment_id: None,
1898 total_charged: cart.grand_total,
1899 currency: cart.currency,
1900 });
1901 }
1902 }
1903
1904 if !cart.is_checkoutable_status() {
1906 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
1907 CommerceError::Conflict(format!(
1908 "Cart cannot be checked out in status: {}",
1909 cart.status
1910 )),
1911 )));
1912 }
1913
1914 if !cart.is_ready_for_checkout() {
1915 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
1916 CommerceError::ValidationError(
1917 "Cart is not ready for checkout - ensure items, customer info, and shipping address are set".to_string(),
1918 ),
1919 )));
1920 }
1921
1922 let customer_id = Self::resolve_customer_id_in_tx(tx, &cart)?;
1923 let order_items: Vec<CreateOrderItem> = cart
1924 .items
1925 .iter()
1926 .map(|item| CreateOrderItem {
1927 product_id: item.product_id.unwrap_or_else(ProductId::new),
1928 variant_id: item.variant_id,
1929 sku: item.sku.clone(),
1930 name: item.name.clone(),
1931 quantity: item.quantity,
1932 unit_price: item.unit_price,
1933 discount: Some(item.discount_amount),
1934 tax_amount: Some(item.tax_amount),
1935 })
1936 .collect();
1937
1938 let shipping_address = cart.shipping_address.clone().map(Into::into);
1939 let billing_address = if cart.billing_same_as_shipping {
1940 cart.billing_address.clone().or_else(|| cart.shipping_address.clone()).map(Into::into)
1941 } else {
1942 cart.billing_address.clone().map(Into::into)
1943 };
1944
1945 let mut order = SqliteOrderRepository::create_from_cart_in_tx(
1946 tx,
1947 cart_id.into_uuid(),
1948 &CreateOrder {
1949 customer_id,
1950 items: order_items,
1951 currency: Some(cart.currency),
1952 shipping_address,
1953 billing_address,
1954 notes: cart.notes.clone(),
1955 payment_method: cart.payment_method.clone(),
1956 shipping_method: cart.shipping_method.clone(),
1957 },
1958 )?;
1959
1960 if order.status != OrderStatus::Confirmed
1961 && !order.status.can_transition_to(OrderStatus::Confirmed)
1962 {
1963 return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
1964 CommerceError::InvalidOrderStatusTransition {
1965 from: order.status.to_string(),
1966 to: OrderStatus::Confirmed.to_string(),
1967 },
1968 )));
1969 }
1970
1971 let target_payment_status =
1972 if mark_paid { PaymentStatus::Paid } else { order.payment_status };
1973 if order.status != OrderStatus::Confirmed || order.payment_status != target_payment_status {
1974 let now = Utc::now();
1975 tx.execute(
1976 "UPDATE orders SET status = ?, payment_status = ?, updated_at = ?, version = version + 1 WHERE id = ?",
1977 rusqlite::params![
1978 OrderStatus::Confirmed.to_string(),
1979 target_payment_status.to_string(),
1980 now.to_rfc3339(),
1981 order.id.to_string(),
1982 ],
1983 )?;
1984 order.status = OrderStatus::Confirmed;
1985 order.payment_status = target_payment_status;
1986 order.updated_at = now;
1987 order.version += 1;
1988 }
1989
1990 let completed_at = Utc::now();
1991 if x402_settled {
1992 tx.execute(
1993 "UPDATE carts SET
1994 status = 'completed', order_id = ?, order_number = ?,
1995 payment_status = 'captured', x402_status = 'settled',
1996 completed_at = ?, updated_at = ?, customer_id = ?
1997 WHERE id = ?",
1998 rusqlite::params![
1999 order.id.to_string(),
2000 &order.order_number,
2001 completed_at.to_rfc3339(),
2002 completed_at.to_rfc3339(),
2003 customer_id.to_string(),
2004 cart_id.to_string()
2005 ],
2006 )?;
2007 } else if mark_paid {
2008 tx.execute(
2009 "UPDATE carts SET status = 'completed', order_id = ?, order_number = ?,
2010 payment_status = 'captured', completed_at = ?, updated_at = ?, customer_id = ? WHERE id = ?",
2011 rusqlite::params![
2012 order.id.to_string(),
2013 &order.order_number,
2014 completed_at.to_rfc3339(),
2015 completed_at.to_rfc3339(),
2016 customer_id.to_string(),
2017 cart_id.to_string()
2018 ],
2019 )?;
2020 } else {
2021 tx.execute(
2022 "UPDATE carts SET status = 'completed', order_id = ?, order_number = ?,
2023 completed_at = ?, updated_at = ?, customer_id = ? WHERE id = ?",
2024 rusqlite::params![
2025 order.id.to_string(),
2026 &order.order_number,
2027 completed_at.to_rfc3339(),
2028 completed_at.to_rfc3339(),
2029 customer_id.to_string(),
2030 cart_id.to_string()
2031 ],
2032 )?;
2033 }
2034
2035 Ok(CheckoutResult {
2036 cart_id,
2037 order_id: order.id,
2038 order_number: order.order_number,
2039 payment_id: None,
2040 total_charged: cart.grand_total,
2041 currency: cart.currency,
2042 })
2043 }
2044}
2045
2046fn split_customer_name(name: Option<&str>) -> (String, String) {
2047 let trimmed = name.unwrap_or("").trim();
2048 if trimmed.is_empty() {
2049 return ("Guest".to_string(), "Customer".to_string());
2050 }
2051
2052 let mut parts = trimmed.split_whitespace();
2053 let first_name = parts.next().unwrap_or("Guest");
2054 let last_name = parts.collect::<Vec<_>>().join(" ");
2055
2056 if last_name.is_empty() {
2057 (first_name.to_string(), "Customer".to_string())
2058 } else {
2059 (first_name.to_string(), last_name)
2060 }
2061}
2062
2063#[cfg(test)]
2064mod tests {
2065 use super::*;
2066 use crate::SqliteDatabase;
2067 use rust_decimal_macros::dec;
2068 use stateset_core::{CartStatus, CommerceError};
2069
2070 fn fresh_repo() -> SqliteCartRepository {
2071 SqliteDatabase::in_memory().expect("in-memory sqlite").carts()
2072 }
2073
2074 fn addr() -> CartAddress {
2075 CartAddress {
2076 first_name: "Ada".into(),
2077 last_name: "Lovelace".into(),
2078 company: None,
2079 line1: "1 Babbage Way".into(),
2080 line2: None,
2081 city: "London".into(),
2082 state: None,
2083 postal_code: "NW1".into(),
2084 country: "GB".into(),
2085 phone: Some("+44 20 7946 0000".into()),
2086 email: Some("ada@example.com".into()),
2087 }
2088 }
2089
2090 fn add_item(sku: &str, qty: i32, price: Decimal) -> AddCartItem {
2091 AddCartItem {
2092 sku: sku.into(),
2093 name: format!("Item {sku}"),
2094 quantity: qty,
2095 unit_price: price,
2096 ..Default::default()
2097 }
2098 }
2099
2100 #[test]
2101 fn create_cart_minimal_starts_active_and_zeroed() {
2102 let repo = fresh_repo();
2103 let cart = repo
2104 .create(CreateCart {
2105 customer_email: Some("buyer@example.com".into()),
2106 customer_name: Some("Test Buyer".into()),
2107 ..Default::default()
2108 })
2109 .expect("create");
2110 assert_eq!(cart.status, CartStatus::Active);
2111 assert_eq!(cart.subtotal, dec!(0));
2112 assert_eq!(cart.tax_amount, dec!(0));
2113 assert_eq!(cart.shipping_amount, dec!(0));
2114 assert_eq!(cart.grand_total, dec!(0));
2115 assert!(cart.items.is_empty());
2116 assert!(cart.cart_number.starts_with("CART-"));
2117 }
2118
2119 #[test]
2120 fn create_cart_with_items_persists_them() {
2121 let repo = fresh_repo();
2122 let cart = repo
2123 .create(CreateCart {
2124 customer_email: Some("with-items@example.com".into()),
2125 items: Some(vec![add_item("SKU-A", 2, dec!(10)), add_item("SKU-B", 1, dec!(5))]),
2126 ..Default::default()
2127 })
2128 .expect("create");
2129 let items = repo.get_items(cart.id).expect("get_items");
2130 assert_eq!(items.len(), 2);
2131 let fresh = repo.get(cart.id).expect("ok").expect("found");
2132 assert_eq!(fresh.subtotal, dec!(25));
2133 }
2134
2135 #[test]
2136 fn get_by_number_round_trips() {
2137 let repo = fresh_repo();
2138 let cart = repo.create(CreateCart::default()).expect("create");
2139 let by_num = repo.get_by_number(&cart.cart_number).expect("get_by_number").expect("found");
2140 assert_eq!(by_num.id, cart.id);
2141 assert!(repo.get_by_number("missing").expect("ok").is_none());
2142 }
2143
2144 #[test]
2145 fn add_item_recomputes_subtotal() {
2146 let repo = fresh_repo();
2147 let cart = repo.create(CreateCart::default()).expect("create");
2148 repo.add_item(cart.id, add_item("SKU-X", 3, dec!(7))).expect("add");
2149 let fresh = repo.get(cart.id).expect("ok").expect("found");
2150 assert_eq!(fresh.subtotal, dec!(21));
2151 }
2152
2153 #[test]
2154 fn grand_total_never_goes_negative_from_oversized_discount() {
2155 let repo = fresh_repo();
2159 let cart = repo
2160 .create(CreateCart {
2161 items: Some(vec![add_item("SKU-A", 2, dec!(10)), add_item("SKU-B", 1, dec!(5))]),
2162 ..Default::default()
2163 })
2164 .expect("create"); repo.update(cart.id, UpdateCart { discount_amount: Some(dec!(100)), ..Default::default() })
2167 .expect("set oversized discount");
2168 let recalculated = repo.recalculate(cart.id).expect("recalc");
2169
2170 assert_eq!(recalculated.subtotal, dec!(25));
2171 assert_eq!(
2172 recalculated.grand_total,
2173 dec!(0),
2174 "grand total must clamp at zero, not go negative: {recalculated:?}"
2175 );
2176 }
2177
2178 #[test]
2179 fn update_item_changes_quantity_and_recomputes() {
2180 let repo = fresh_repo();
2181 let cart = repo.create(CreateCart::default()).expect("create");
2182 let item = repo.add_item(cart.id, add_item("SKU-U", 1, dec!(10))).expect("add");
2183 let updated = repo
2184 .update_item(item.id, UpdateCartItem { quantity: Some(5), ..Default::default() })
2185 .expect("update");
2186 assert_eq!(updated.quantity, 5);
2187 let fresh = repo.get(cart.id).expect("ok").expect("found");
2188 assert_eq!(fresh.subtotal, dec!(50));
2189 }
2190
2191 #[test]
2192 fn remove_item_drops_line_and_decrements_subtotal() {
2193 let repo = fresh_repo();
2194 let cart = repo.create(CreateCart::default()).expect("create");
2195 let item_a = repo.add_item(cart.id, add_item("SKU-A", 1, dec!(8))).expect("a");
2196 let _item_b = repo.add_item(cart.id, add_item("SKU-B", 1, dec!(2))).expect("b");
2197 repo.remove_item(item_a.id).expect("remove");
2198 let items = repo.get_items(cart.id).expect("items");
2199 assert_eq!(items.len(), 1);
2200 let fresh = repo.get(cart.id).expect("ok").expect("found");
2201 assert_eq!(fresh.subtotal, dec!(2));
2202 }
2203
2204 #[test]
2205 fn set_shipping_address_persists() {
2206 let repo = fresh_repo();
2207 let cart = repo.create(CreateCart::default()).expect("create");
2208 let updated = repo.set_shipping_address(cart.id, addr()).expect("set");
2209 assert!(updated.shipping_address.is_some());
2210 let stored = updated.shipping_address.expect("addr");
2211 assert_eq!(stored.first_name, "Ada");
2212 assert_eq!(stored.country, "GB");
2213 }
2214
2215 #[test]
2216 fn set_shipping_applies_amount_to_total() {
2217 let repo = fresh_repo();
2218 let cart = repo.create(CreateCart::default()).expect("create");
2219 repo.add_item(cart.id, add_item("SKU-S", 1, dec!(20))).expect("add");
2220 let updated = repo
2221 .set_shipping(
2222 cart.id,
2223 SetCartShipping {
2224 shipping_address: addr(),
2225 shipping_method: Some("standard".into()),
2226 shipping_carrier: Some("usps".into()),
2227 shipping_amount: Some(dec!(7)),
2228 },
2229 )
2230 .expect("set shipping");
2231 assert_eq!(updated.shipping_amount, dec!(7));
2232 assert!(updated.grand_total >= dec!(27));
2233 }
2234
2235 #[test]
2236 fn set_payment_records_method_and_token() {
2237 let repo = fresh_repo();
2238 let cart = repo.create(CreateCart::default()).expect("create");
2239 let updated = repo
2240 .set_payment(
2241 cart.id,
2242 SetCartPayment {
2243 payment_method: "credit_card".into(),
2244 payment_token: Some("tok_123".into()),
2245 billing_address: None,
2246 },
2247 )
2248 .expect("set payment");
2249 assert_eq!(updated.payment_method.as_deref(), Some("credit_card"));
2250 }
2251
2252 #[test]
2253 fn apply_discount_with_invalid_coupon_returns_validation_error() {
2254 let repo = fresh_repo();
2255 let cart = repo.create(CreateCart::default()).expect("create");
2256 let err = repo.apply_discount(cart.id, "DOES-NOT-EXIST").expect_err("err");
2257 assert!(matches!(err, CommerceError::ValidationError(_)));
2258 }
2259
2260 #[test]
2261 fn abandon_marks_status_abandoned() {
2262 let repo = fresh_repo();
2263 let cart = repo.create(CreateCart::default()).expect("create");
2264 let abandoned = repo.abandon(cart.id).expect("abandon");
2265 assert_eq!(abandoned.status, CartStatus::Abandoned);
2266 }
2267
2268 fn checkoutable_cart(repo: &SqliteCartRepository) -> Cart {
2272 let cart = repo
2273 .create(CreateCart {
2274 customer_email: Some("buyer@example.com".into()),
2275 customer_name: Some("Ada Lovelace".into()),
2276 items: Some(vec![add_item("SKU-CHK", 1, dec!(10))]),
2277 shipping_address: Some(addr()),
2278 ..Default::default()
2279 })
2280 .expect("create");
2281 repo.set_shipping_address(cart.id, addr()).expect("ship addr");
2282 repo.get(cart.id).expect("ok").expect("found")
2283 }
2284
2285 #[test]
2286 fn complete_checks_out_active_cart() {
2287 let repo = fresh_repo();
2288 let cart = checkoutable_cart(&repo);
2289 assert_eq!(cart.status, CartStatus::Active);
2290 let result = repo.complete(cart.id).expect("checkout should succeed");
2291 assert!(result.order_number.starts_with("ORD-") || !result.order_number.is_empty());
2292 let completed = repo.get(cart.id).expect("ok").expect("found");
2293 assert_eq!(completed.status, CartStatus::Completed);
2294 assert!(completed.order_id.is_some());
2295 }
2296
2297 #[test]
2298 fn complete_leaves_payment_pending_without_explicit_settlement() {
2299 use stateset_core::OrderRepository as _;
2300
2301 let db = SqliteDatabase::in_memory().expect("in-memory sqlite");
2302 let repo = db.carts();
2303 let cart = checkoutable_cart(&repo);
2304 let result = repo.complete(cart.id).expect("checkout should succeed");
2305 assert!(result.payment_id.is_none());
2306
2307 let order = db.orders().get(result.order_id).expect("ok").expect("order exists");
2308 assert_eq!(order.status, stateset_core::OrderStatus::Confirmed);
2309 assert_eq!(
2310 order.payment_status,
2311 stateset_core::PaymentStatus::Pending,
2312 "plain complete() must not mark an order paid with no payment record; \
2313 out-of-band settlement uses complete_settled_externally()"
2314 );
2315 }
2316
2317 #[test]
2318 fn complete_settled_externally_marks_order_paid() {
2319 use stateset_core::OrderRepository as _;
2320
2321 let db = SqliteDatabase::in_memory().expect("in-memory sqlite");
2322 let repo = db.carts();
2323 let cart = checkoutable_cart(&repo);
2324 let result = repo.complete_settled_externally(cart.id).expect("checkout should succeed");
2325
2326 let order = db.orders().get(result.order_id).expect("ok").expect("order exists");
2327 assert_eq!(order.status, stateset_core::OrderStatus::Confirmed);
2328 assert_eq!(order.payment_status, stateset_core::PaymentStatus::Paid);
2329
2330 let completed = repo.get(cart.id).expect("ok").expect("found");
2331 assert_eq!(completed.status, CartStatus::Completed);
2332 }
2333
2334 #[test]
2335 fn complete_rejects_cancelled_cart() {
2336 let repo = fresh_repo();
2337 let cart = checkoutable_cart(&repo);
2338 repo.cancel(cart.id).expect("cancel");
2339 let err = repo.complete(cart.id).expect_err("cancelled cart must not check out");
2340 assert!(matches!(err, CommerceError::Conflict(_)), "got {err:?}");
2341 let after = repo.get(cart.id).expect("ok").expect("found");
2343 assert_eq!(after.status, CartStatus::Cancelled);
2344 assert!(after.order_id.is_none());
2345 }
2346
2347 #[test]
2348 fn complete_rejects_abandoned_cart() {
2349 let repo = fresh_repo();
2350 let cart = checkoutable_cart(&repo);
2351 repo.abandon(cart.id).expect("abandon");
2352 let err = repo.complete(cart.id).expect_err("abandoned cart must not check out");
2353 assert!(matches!(err, CommerceError::Conflict(_)), "got {err:?}");
2354 let after = repo.get(cart.id).expect("ok").expect("found");
2355 assert!(after.order_id.is_none());
2356 }
2357
2358 #[test]
2359 fn complete_rejects_expired_cart() {
2360 let repo = fresh_repo();
2361 let cart = checkoutable_cart(&repo);
2362 repo.expire(cart.id).expect("expire");
2363 let err = repo.complete(cart.id).expect_err("expired cart must not check out");
2364 assert!(matches!(err, CommerceError::Conflict(_)), "got {err:?}");
2365 let after = repo.get(cart.id).expect("ok").expect("found");
2366 assert!(after.order_id.is_none());
2367 }
2368
2369 #[test]
2370 fn delete_removes_cart() {
2371 let repo = fresh_repo();
2372 let cart = repo.create(CreateCart::default()).expect("create");
2373 repo.delete(cart.id).expect("delete");
2374 assert!(repo.get(cart.id).expect("ok").is_none());
2375 }
2376
2377 #[test]
2378 fn list_filters_by_customer_email() {
2379 let repo = fresh_repo();
2380 repo.create(CreateCart {
2381 customer_email: Some("alice@example.com".into()),
2382 ..Default::default()
2383 })
2384 .expect("alice");
2385 repo.create(CreateCart {
2386 customer_email: Some("bob@example.com".into()),
2387 ..Default::default()
2388 })
2389 .expect("bob");
2390 repo.create(CreateCart {
2391 customer_email: Some("alice@example.com".into()),
2392 ..Default::default()
2393 })
2394 .expect("alice2");
2395
2396 let alices = repo
2397 .list(CartFilter {
2398 customer_email: Some("alice@example.com".into()),
2399 ..Default::default()
2400 })
2401 .expect("list");
2402 assert_eq!(alices.len(), 2);
2403 }
2404
2405 #[test]
2406 fn list_filters_by_status() {
2407 let repo = fresh_repo();
2408 let cart_a = repo.create(CreateCart::default()).expect("a");
2409 let _cart_b = repo.create(CreateCart::default()).expect("b");
2410 repo.abandon(cart_a.id).expect("abandon");
2411
2412 let active = repo
2413 .list(CartFilter { status: Some(CartStatus::Active), ..Default::default() })
2414 .expect("active");
2415 let abandoned = repo
2416 .list(CartFilter { status: Some(CartStatus::Abandoned), ..Default::default() })
2417 .expect("abandoned");
2418 assert_eq!(active.len(), 1);
2419 assert_eq!(abandoned.len(), 1);
2420 }
2421
2422 #[test]
2423 fn create_batch_returns_all_succeeded() {
2424 let repo = fresh_repo();
2425 let batch = repo
2426 .create_batch(vec![
2427 CreateCart { customer_email: Some("c1@example.com".into()), ..Default::default() },
2428 CreateCart { customer_email: Some("c2@example.com".into()), ..Default::default() },
2429 CreateCart { customer_email: Some("c3@example.com".into()), ..Default::default() },
2430 ])
2431 .expect("batch");
2432 assert_eq!(batch.success_count, 3);
2433 assert_eq!(batch.failure_count, 0);
2434 }
2435
2436 #[test]
2437 fn get_batch_returns_only_existing() {
2438 let repo = fresh_repo();
2439 let cart_a = repo.create(CreateCart::default()).expect("a");
2440 let cart_b = repo.create(CreateCart::default()).expect("b");
2441 let stranger = CartId::new();
2442 let fetched = repo.get_batch(vec![cart_a.id, cart_b.id, stranger]).expect("get_batch");
2443 assert_eq!(fetched.len(), 2);
2444 }
2445
2446 #[test]
2447 fn get_returns_none_for_missing_id() {
2448 let repo = fresh_repo();
2449 assert!(repo.get(CartId::new()).expect("ok").is_none());
2450 }
2451
2452 #[test]
2453 fn get_abandoned_returns_only_abandoned() {
2454 let repo = fresh_repo();
2455 let active = repo.create(CreateCart::default()).expect("active");
2456 let to_abandon = repo.create(CreateCart::default()).expect("to-abandon");
2457 repo.abandon(to_abandon.id).expect("abandon");
2458
2459 let abandoned = repo.get_abandoned().expect("get_abandoned");
2460 let ids: Vec<CartId> = abandoned.iter().map(|c| c.id).collect();
2461 assert!(ids.contains(&to_abandon.id));
2462 assert!(!ids.contains(&active.id));
2463 }
2464}