Skip to main content

stateset_db/sqlite/
carts.rs

1//! SQLite cart repository implementation
2
3use 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/// SQLite implementation of `CartRepository`
27#[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![], // Loaded separately
66
67            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 fields
139            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    /// Finalize x402 checkout after payment settlement
322    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        // Calculate subtotal from pre-tax line amounts to avoid double-counting tax.
331        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        // Round the subtotal to the currency minor unit so the stored value is a
356        // real money amount (a sub-cent subtotal like 9.999 is not chargeable),
357        // matching the Postgres DECIMAL(12,2) columns and the order pipeline.
358        let subtotal = subtotal.round_dp(2);
359
360        // Get current tax and shipping
361        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        // Calculate grand total, clamped at zero: a discount larger than
370        // subtotal + tax + shipping must not produce a negative total (which
371        // would charge the buyer a negative amount at checkout).
372        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        // Validate currency if provided
396        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                &currency,
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        // Add initial items if provided
447        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        // Recalculate totals
499        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        // Validate item quantity (must be positive)
684        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 item price
692        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        // Get cart_id for this item
711        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        // Recalculate item total
747        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        // Update cart totals
769        let cart_uuid = CartId::from(parse_uuid(&cart_id, "cart_item", "cart_id")?);
770        self.update_cart_totals(&tx, cart_uuid)?;
771
772        // Return updated item
773        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        // Get cart_id before deleting
855        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        // Recalculate grand total
940        self.recalculate(id)
941    }
942
943    fn get_shipping_rates(&self, _id: CartId) -> Result<Vec<ShippingRate>> {
944        // This would typically integrate with shipping providers
945        // For now, return some default rates
946        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), // $5.99
953                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), // $14.99
963                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), // $29.99
973                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        // A cancelled/abandoned/expired cart must never be minted into an order.
1047        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        // Validate cart is ready for checkout
1055        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        // Ensure x402 payment is configured
1062        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        // Calculate amount in smallest unit
1069        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        // Check if there's an existing intent
1076        if let Some(intent_id) = x402_payment.intent_id {
1077            // Get the intent status from x402_payment_intents table
1078            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                        // Payment is settled - complete the checkout
1096                        return self.finalize_x402_checkout(id);
1097                    }
1098                    X402IntentStatus::Signed
1099                    | X402IntentStatus::Sequenced
1100                    | X402IntentStatus::Batched => {
1101                        // Awaiting settlement
1102                        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                        // Intent exists but not signed yet
1114                        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, // Would need to fetch from intent
1124                            nonce: 0,
1125                        }));
1126                    }
1127                    X402IntentStatus::Expired
1128                    | X402IntentStatus::Failed
1129                    | X402IntentStatus::Cancelled
1130                    | _ => {
1131                        // Need to create a new intent
1132                    }
1133                }
1134            }
1135        }
1136
1137        // No valid intent exists - return PaymentRequired
1138        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, // 1 hour default
1148        }))
1149    }
1150
1151    fn apply_discount(&self, id: CartId, coupon_code: &str) -> Result<Cart> {
1152        // Get the cart first to calculate discount
1153        let cart = self.get(id)?.ok_or(CommerceError::NotFound)?;
1154
1155        // Look up the coupon and its promotion
1156        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        // Calculate the discount based on promotion type
1166        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                // Apply max discount cap if set
1172                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, // Other types not fully implemented
1182        };
1183
1184        let discount_description = Some(promotion.name);
1185
1186        // Update the cart with the discount
1187        {
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        // Recalculate totals and return
1203        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        // Also mark expired carts
1367        {
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    // === Batch Operations ===
1402
1403    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                    &currency,
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            // Add initial items if provided
1477            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            // Fetch the updated cart
1629            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        // Load items for all carts in one batched query
1639        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(&params);
1676
1677        // Delete cart items first
1678        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        // Delete carts
1682        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(&params);
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        // Load items for all carts in one batched query
1711        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
1723// Internal helper methods
1724impl 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    /// `mark_paid` promotes the minted order straight to `PaymentStatus::Paid`
1862    /// without a payment record. That is only correct when settlement happened
1863    /// out of band (x402, ACP, external PSP) and the caller opted in
1864    /// explicitly; the plain checkout path leaves payment pending so a
1865    /// miswired integration cannot mint revenue-recognized orders with no
1866    /// payment trail.
1867    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        // A cancelled/abandoned/expired cart must never be minted into an order.
1905        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        // A cart-level discount larger than subtotal + tax + shipping must not
2156        // drive the grand total negative — a negative total would mean charging
2157        // the buyer a negative amount (crediting them) at checkout.
2158        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"); // subtotal $25
2165
2166        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    /// Create a cart that satisfies every `is_ready_for_checkout` requirement
2269    /// (item, customer email/name, shipping address) so that `complete()` will
2270    /// mint an order unless the lifecycle guard rejects it.
2271    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        // No order was minted.
2342        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}