1use chrono::{DateTime, Utc};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use stateset_primitives::{CartId, CurrencyCode, CustomerId, OrderId, PaymentId, ProductId};
10use strum::{Display, EnumString};
11use uuid::Uuid;
12
13use super::Address;
14use super::x402::{X402Asset, X402IntentStatus, X402Network};
15use crate::errors::Result;
16use crate::validation::{Validate, ValidationBuilder};
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct Cart {
21 pub id: CartId,
22 pub cart_number: String,
23 pub customer_id: Option<CustomerId>,
24 pub status: CartStatus,
25 pub currency: CurrencyCode,
26
27 pub items: Vec<CartItem>,
29
30 pub subtotal: Decimal,
32 pub tax_amount: Decimal,
33 pub shipping_amount: Decimal,
34 pub discount_amount: Decimal,
35 pub grand_total: Decimal,
36
37 pub customer_email: Option<String>,
39 pub customer_phone: Option<String>,
40 pub customer_name: Option<String>,
41
42 pub shipping_address: Option<CartAddress>,
44 pub billing_address: Option<CartAddress>,
45 pub billing_same_as_shipping: bool,
46
47 pub fulfillment_type: Option<FulfillmentType>,
49 pub shipping_method: Option<String>,
50 pub shipping_carrier: Option<String>,
51 pub estimated_delivery: Option<DateTime<Utc>>,
52
53 pub payment_method: Option<String>,
55 pub payment_token: Option<String>,
56 pub payment_status: CartPaymentStatus,
57
58 pub coupon_code: Option<String>,
60 pub discount_description: Option<String>,
61
62 pub order_id: Option<OrderId>,
64 pub order_number: Option<String>,
65
66 pub notes: Option<String>,
68 pub metadata: Option<serde_json::Value>,
69
70 pub inventory_reserved: bool,
72 pub reservation_expires_at: Option<DateTime<Utc>>,
73
74 pub x402_payment: Option<CartX402Payment>,
76
77 pub expires_at: Option<DateTime<Utc>>,
79 pub completed_at: Option<DateTime<Utc>>,
80 pub created_at: DateTime<Utc>,
81 pub updated_at: DateTime<Utc>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct CartItem {
87 pub id: Uuid,
88 pub cart_id: CartId,
89 pub product_id: Option<ProductId>,
90 pub variant_id: Option<Uuid>,
91 pub sku: String,
92 pub name: String,
93 pub description: Option<String>,
94 pub image_url: Option<String>,
95 pub quantity: i32,
96 pub unit_price: Decimal,
97 pub original_price: Option<Decimal>,
98 pub discount_amount: Decimal,
99 pub tax_amount: Decimal,
100 pub total: Decimal,
101 pub weight: Option<Decimal>,
102 pub requires_shipping: bool,
103 pub metadata: Option<serde_json::Value>,
104 pub created_at: DateTime<Utc>,
105 pub updated_at: DateTime<Utc>,
106}
107
108#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
110pub struct CartAddress {
111 pub first_name: String,
112 pub last_name: String,
113 pub company: Option<String>,
114 pub line1: String,
115 pub line2: Option<String>,
116 pub city: String,
117 pub state: Option<String>,
118 pub postal_code: String,
119 pub country: String,
120 pub phone: Option<String>,
121 pub email: Option<String>,
122}
123
124impl From<CartAddress> for Address {
125 fn from(addr: CartAddress) -> Self {
126 Self {
127 line1: addr.line1,
128 line2: addr.line2,
129 city: addr.city,
130 state: addr.state,
131 postal_code: addr.postal_code,
132 country: addr.country,
133 }
134 }
135}
136
137impl From<Address> for CartAddress {
138 fn from(addr: Address) -> Self {
139 Self {
140 first_name: String::new(),
141 last_name: String::new(),
142 company: None,
143 line1: addr.line1,
144 line2: addr.line2,
145 city: addr.city,
146 state: addr.state,
147 postal_code: addr.postal_code,
148 country: addr.country,
149 phone: None,
150 email: None,
151 }
152 }
153}
154
155#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
157pub struct CartX402Payment {
158 pub intent_id: Option<Uuid>,
160 pub payer_address: String,
162 pub network: X402Network,
164 pub asset: X402Asset,
166 pub status: X402IntentStatus,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct SetCartX402Payment {
173 pub payer_address: String,
175 pub network: X402Network,
177 pub asset: X402Asset,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183#[non_exhaustive]
184pub enum X402CheckoutResult {
185 PaymentRequired(X402PaymentRequiredData),
187 IntentCreated(X402IntentCreatedData),
189 AwaitingSettlement(X402AwaitingSettlementData),
191 Completed(CheckoutResult),
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct X402PaymentRequiredData {
198 pub cart_id: CartId,
199 pub payee_address: String,
200 pub amount: u64,
201 pub amount_display: String,
202 pub asset: X402Asset,
203 pub network: X402Network,
204 pub chain_id: u64,
205 pub valid_seconds: u64,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct X402IntentCreatedData {
211 pub cart_id: CartId,
212 pub intent_id: Uuid,
213 pub signing_hash: String,
214 pub amount: u64,
215 pub amount_display: String,
216 pub asset: X402Asset,
217 pub network: X402Network,
218 pub payee_address: String,
219 pub valid_until: u64,
220 pub nonce: u64,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct X402AwaitingSettlementData {
226 pub cart_id: CartId,
227 pub intent_id: Uuid,
228 pub status: X402IntentStatus,
229 pub sequence_number: Option<u64>,
230 pub batch_id: Option<Uuid>,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
235#[serde(rename_all = "snake_case")]
236#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
237#[non_exhaustive]
238pub enum CartStatus {
239 Active,
241 #[strum(serialize = "ready_for_payment", serialize = "readyforpayment")]
243 ReadyForPayment,
244 #[strum(serialize = "payment_pending", serialize = "paymentpending")]
246 PaymentPending,
247 Completed,
249 Abandoned,
251 #[strum(serialize = "cancelled", serialize = "canceled")]
253 Cancelled,
254 Expired,
256}
257
258impl Default for CartStatus {
259 fn default() -> Self {
260 Self::Active
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
266#[serde(rename_all = "snake_case")]
267#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
268#[non_exhaustive]
269pub enum CartPaymentStatus {
270 None,
272 #[strum(serialize = "method_selected", serialize = "methodselected")]
274 MethodSelected,
275 Authorized,
277 Captured,
279 Failed,
281 Refunded,
283}
284
285impl Default for CartPaymentStatus {
286 fn default() -> Self {
287 Self::None
288 }
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
293#[serde(rename_all = "snake_case")]
294#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
295#[non_exhaustive]
296pub enum FulfillmentType {
297 Shipping,
299 #[strum(serialize = "pickup", serialize = "pick_up", serialize = "pick-up")]
301 Pickup,
302 Digital,
304}
305
306impl Default for FulfillmentType {
307 fn default() -> Self {
308 Self::Shipping
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
314pub struct ShippingRate {
315 pub id: String,
316 pub carrier: String,
317 pub service: String,
318 pub description: Option<String>,
319 pub price: Decimal,
320 pub currency: CurrencyCode,
321 pub estimated_days: Option<i32>,
322 pub estimated_delivery: Option<DateTime<Utc>>,
323}
324
325#[derive(Debug, Clone, Default, Serialize, Deserialize)]
327pub struct CreateCart {
328 pub customer_id: Option<CustomerId>,
329 pub customer_email: Option<String>,
330 pub customer_name: Option<String>,
331 pub currency: Option<CurrencyCode>,
332 pub items: Option<Vec<AddCartItem>>,
333 pub shipping_address: Option<CartAddress>,
334 pub billing_address: Option<CartAddress>,
335 pub notes: Option<String>,
336 pub metadata: Option<serde_json::Value>,
337 pub expires_in_minutes: Option<i64>,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct AddCartItem {
343 pub product_id: Option<ProductId>,
344 pub variant_id: Option<Uuid>,
345 pub sku: String,
346 pub name: String,
347 pub description: Option<String>,
348 pub image_url: Option<String>,
349 pub quantity: i32,
350 pub unit_price: Decimal,
351 pub original_price: Option<Decimal>,
352 pub weight: Option<Decimal>,
353 pub requires_shipping: Option<bool>,
354 pub metadata: Option<serde_json::Value>,
355}
356
357impl Default for AddCartItem {
358 fn default() -> Self {
359 Self {
360 product_id: None,
361 variant_id: None,
362 sku: String::new(),
363 name: String::new(),
364 description: None,
365 image_url: None,
366 quantity: 1,
367 unit_price: Decimal::ZERO,
368 original_price: None,
369 weight: None,
370 requires_shipping: Some(true),
371 metadata: None,
372 }
373 }
374}
375
376#[derive(Debug, Clone, Default, Serialize, Deserialize)]
378pub struct UpdateCartItem {
379 pub quantity: Option<i32>,
380 pub unit_price: Option<Decimal>,
381 pub discount_amount: Option<Decimal>,
382 pub metadata: Option<serde_json::Value>,
383}
384
385#[derive(Debug, Clone, Default, Serialize, Deserialize)]
387pub struct UpdateCart {
388 pub customer_id: Option<CustomerId>,
389 pub customer_email: Option<String>,
390 pub customer_phone: Option<String>,
391 pub customer_name: Option<String>,
392 pub shipping_address: Option<CartAddress>,
393 pub billing_address: Option<CartAddress>,
394 pub billing_same_as_shipping: Option<bool>,
395 pub fulfillment_type: Option<FulfillmentType>,
396 pub shipping_method: Option<String>,
397 pub shipping_carrier: Option<String>,
398 pub coupon_code: Option<String>,
399 pub discount_amount: Option<Decimal>,
400 pub discount_description: Option<String>,
401 pub notes: Option<String>,
402 pub metadata: Option<serde_json::Value>,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct SetCartShipping {
408 pub shipping_address: CartAddress,
409 pub shipping_method: Option<String>,
410 pub shipping_carrier: Option<String>,
411 pub shipping_amount: Option<Decimal>,
412}
413
414#[derive(Debug, Clone, Default, Serialize, Deserialize)]
416pub struct SetCartPayment {
417 pub payment_method: String,
418 pub payment_token: Option<String>,
419 pub billing_address: Option<CartAddress>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct ApplyCartDiscount {
425 pub coupon_code: String,
426}
427
428#[derive(Debug, Clone, Default, Serialize, Deserialize)]
430pub struct CartFilter {
431 pub customer_id: Option<CustomerId>,
432 pub customer_email: Option<String>,
433 pub status: Option<CartStatus>,
434 pub has_items: Option<bool>,
435 pub is_abandoned: Option<bool>,
436 pub created_after: Option<DateTime<Utc>>,
437 pub created_before: Option<DateTime<Utc>>,
438 pub limit: Option<u32>,
439 pub offset: Option<u32>,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct CheckoutResult {
445 pub cart_id: CartId,
446 pub order_id: OrderId,
447 pub order_number: String,
448 pub payment_id: Option<PaymentId>,
449 pub total_charged: Decimal,
450 pub currency: CurrencyCode,
451}
452
453impl Cart {
454 #[must_use]
456 pub fn has_items(&self) -> bool {
457 !self.items.is_empty()
458 }
459
460 #[must_use]
462 pub fn item_count(&self) -> i32 {
463 self.items.iter().map(|i| i.quantity).sum()
464 }
465
466 #[must_use]
468 pub fn requires_shipping(&self) -> bool {
469 self.items.iter().any(|i| i.requires_shipping)
470 }
471
472 #[must_use]
481 pub const fn is_checkoutable_status(&self) -> bool {
482 matches!(
483 self.status,
484 CartStatus::Active | CartStatus::ReadyForPayment | CartStatus::PaymentPending
485 )
486 }
487
488 #[must_use]
490 pub fn is_ready_for_checkout(&self) -> bool {
491 if !self.is_checkoutable_status() {
495 return false;
496 }
497
498 if self.items.is_empty() {
499 return false;
500 }
501
502 if self.customer_id.is_none() && self.customer_email.is_none() {
504 return false;
505 }
506
507 if self.requires_shipping() && self.shipping_address.is_none() {
509 return false;
510 }
511
512 true
513 }
514
515 #[must_use]
517 pub const fn can_modify(&self) -> bool {
518 matches!(self.status, CartStatus::Active)
519 }
520
521 #[must_use]
523 pub const fn can_complete(&self) -> bool {
524 matches!(self.status, CartStatus::ReadyForPayment | CartStatus::PaymentPending)
525 }
526
527 #[must_use]
529 pub const fn can_cancel(&self) -> bool {
530 matches!(
531 self.status,
532 CartStatus::Active | CartStatus::ReadyForPayment | CartStatus::PaymentPending
533 )
534 }
535
536 #[must_use]
538 pub fn is_abandoned(&self) -> bool {
539 self.status == CartStatus::Abandoned
540 }
541
542 #[must_use]
544 pub fn is_expired(&self) -> bool {
545 if let Some(expires_at) = self.expires_at { Utc::now() > expires_at } else { false }
546 }
547
548 pub fn recalculate_totals(&mut self) {
550 self.subtotal = self
551 .items
552 .iter()
553 .map(|i| (i.unit_price * Decimal::from(i.quantity)) - i.discount_amount)
554 .sum();
555 self.grand_total =
556 self.subtotal + self.tax_amount + self.shipping_amount - self.discount_amount;
557 }
558}
559
560impl CartItem {
561 #[must_use]
567 pub fn calculate_total(
568 quantity: i32,
569 unit_price: Decimal,
570 discount: Decimal,
571 tax: Decimal,
572 ) -> Decimal {
573 let subtotal = unit_price * Decimal::from(quantity);
574 (subtotal - discount + tax).round_dp(2)
575 }
576
577 pub fn recalculate(&mut self) {
579 self.total = Self::calculate_total(
580 self.quantity,
581 self.unit_price,
582 self.discount_amount,
583 self.tax_amount,
584 );
585 }
586}
587
588impl Validate for AddCartItem {
589 fn validate(&self) -> Result<()> {
595 ValidationBuilder::new()
596 .required("sku", &self.sku)
597 .required("name", &self.name)
598 .positive_i32("quantity", self.quantity)
599 .non_negative("unit_price", self.unit_price)
600 .non_negative("original_price", self.original_price.unwrap_or(Decimal::ZERO))
601 .build()
602 }
603}
604
605impl Validate for CreateCart {
606 fn validate(&self) -> Result<()> {
611 if let Some(items) = &self.items {
612 for item in items {
613 item.validate()?;
614 }
615 }
616 Ok(())
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use rust_decimal_macros::dec;
624
625 #[test]
626 fn test_cart_item_total_calculation() {
627 let total = CartItem::calculate_total(2, dec!(10.00), dec!(0), dec!(1.60));
628 assert_eq!(total, dec!(21.60));
629 }
630
631 #[test]
632 fn test_cart_is_ready_for_checkout() {
633 let mut cart = Cart {
634 id: CartId::new(),
635 cart_number: "CART-001".to_string(),
636 customer_id: Some(CustomerId::new()),
637 status: CartStatus::Active,
638 currency: CurrencyCode::USD,
639 items: vec![CartItem {
640 id: Uuid::new_v4(),
641 cart_id: CartId::new(),
642 product_id: None,
643 variant_id: None,
644 sku: "SKU-001".to_string(),
645 name: "Test Item".to_string(),
646 description: None,
647 image_url: None,
648 quantity: 1,
649 unit_price: dec!(10.00),
650 original_price: None,
651 discount_amount: dec!(0),
652 tax_amount: dec!(0.80),
653 total: dec!(10.80),
654 weight: None,
655 requires_shipping: true,
656 metadata: None,
657 created_at: Utc::now(),
658 updated_at: Utc::now(),
659 }],
660 subtotal: dec!(10.00),
661 tax_amount: dec!(0.80),
662 shipping_amount: dec!(5.00),
663 discount_amount: dec!(0),
664 grand_total: dec!(15.80),
665 customer_email: None,
666 customer_phone: None,
667 customer_name: None,
668 shipping_address: None,
669 billing_address: None,
670 billing_same_as_shipping: true,
671 fulfillment_type: Some(FulfillmentType::Shipping),
672 shipping_method: None,
673 shipping_carrier: None,
674 estimated_delivery: None,
675 payment_method: None,
676 payment_token: None,
677 payment_status: CartPaymentStatus::None,
678 coupon_code: None,
679 discount_description: None,
680 order_id: None,
681 order_number: None,
682 notes: None,
683 metadata: None,
684 inventory_reserved: false,
685 reservation_expires_at: None,
686 x402_payment: None,
687 expires_at: None,
688 completed_at: None,
689 created_at: Utc::now(),
690 updated_at: Utc::now(),
691 };
692
693 assert!(!cart.is_ready_for_checkout());
695
696 cart.shipping_address = Some(CartAddress {
698 first_name: "John".to_string(),
699 last_name: "Doe".to_string(),
700 company: None,
701 line1: "123 Main St".to_string(),
702 line2: None,
703 city: "Anytown".to_string(),
704 state: Some("CA".to_string()),
705 postal_code: "12345".to_string(),
706 country: "US".to_string(),
707 phone: None,
708 email: None,
709 });
710
711 assert!(cart.is_ready_for_checkout());
713 }
714
715 fn ready_cart_with_status(status: CartStatus) -> Cart {
718 Cart {
719 id: CartId::new(),
720 cart_number: "CART-READY".to_string(),
721 customer_id: Some(CustomerId::new()),
722 status,
723 currency: CurrencyCode::USD,
724 items: vec![CartItem {
725 id: Uuid::new_v4(),
726 cart_id: CartId::new(),
727 product_id: None,
728 variant_id: None,
729 sku: "SKU-001".to_string(),
730 name: "Test Item".to_string(),
731 description: None,
732 image_url: None,
733 quantity: 1,
734 unit_price: dec!(10.00),
735 original_price: None,
736 discount_amount: dec!(0),
737 tax_amount: dec!(0.80),
738 total: dec!(10.80),
739 weight: None,
740 requires_shipping: true,
741 metadata: None,
742 created_at: Utc::now(),
743 updated_at: Utc::now(),
744 }],
745 subtotal: dec!(10.00),
746 tax_amount: dec!(0.80),
747 shipping_amount: dec!(5.00),
748 discount_amount: dec!(0),
749 grand_total: dec!(15.80),
750 customer_email: None,
751 customer_phone: None,
752 customer_name: None,
753 shipping_address: Some(CartAddress {
754 first_name: "John".to_string(),
755 last_name: "Doe".to_string(),
756 company: None,
757 line1: "123 Main St".to_string(),
758 line2: None,
759 city: "Anytown".to_string(),
760 state: Some("CA".to_string()),
761 postal_code: "12345".to_string(),
762 country: "US".to_string(),
763 phone: None,
764 email: None,
765 }),
766 billing_address: None,
767 billing_same_as_shipping: true,
768 fulfillment_type: Some(FulfillmentType::Shipping),
769 shipping_method: None,
770 shipping_carrier: None,
771 estimated_delivery: None,
772 payment_method: None,
773 payment_token: None,
774 payment_status: CartPaymentStatus::None,
775 coupon_code: None,
776 discount_description: None,
777 order_id: None,
778 order_number: None,
779 notes: None,
780 metadata: None,
781 inventory_reserved: false,
782 reservation_expires_at: None,
783 x402_payment: None,
784 expires_at: None,
785 completed_at: None,
786 created_at: Utc::now(),
787 updated_at: Utc::now(),
788 }
789 }
790
791 #[test]
792 fn test_is_checkoutable_status() {
793 assert!(ready_cart_with_status(CartStatus::Active).is_checkoutable_status());
795 assert!(ready_cart_with_status(CartStatus::ReadyForPayment).is_checkoutable_status());
796 assert!(ready_cart_with_status(CartStatus::PaymentPending).is_checkoutable_status());
797
798 assert!(!ready_cart_with_status(CartStatus::Completed).is_checkoutable_status());
800 assert!(!ready_cart_with_status(CartStatus::Cancelled).is_checkoutable_status());
801 assert!(!ready_cart_with_status(CartStatus::Abandoned).is_checkoutable_status());
802 assert!(!ready_cart_with_status(CartStatus::Expired).is_checkoutable_status());
803 }
804
805 #[test]
806 fn test_ready_for_checkout_rejects_terminal_status() {
807 assert!(ready_cart_with_status(CartStatus::Active).is_ready_for_checkout());
809
810 for status in [CartStatus::Cancelled, CartStatus::Abandoned, CartStatus::Expired] {
813 let cart = ready_cart_with_status(status);
814 assert!(
815 !cart.is_ready_for_checkout(),
816 "cart in status {status:?} must not be ready for checkout"
817 );
818 }
819 }
820
821 #[test]
822 fn test_add_cart_item_validate_rejects_negative_and_zero_quantity() {
823 let mut item = AddCartItem { sku: "SKU".into(), name: "Item".into(), ..Default::default() };
825 item.unit_price = dec!(-1.00);
826 assert!(item.validate().is_err());
827
828 let mut item = AddCartItem { sku: "SKU".into(), name: "Item".into(), ..Default::default() };
830 item.quantity = 0;
831 assert!(item.validate().is_err());
832
833 let mut item = AddCartItem { sku: "SKU".into(), name: "Item".into(), ..Default::default() };
835 item.quantity = -3;
836 assert!(item.validate().is_err());
837
838 let item = AddCartItem {
840 sku: "FREE".into(),
841 name: "Free Gift".into(),
842 quantity: 1,
843 unit_price: dec!(0),
844 ..Default::default()
845 };
846 assert!(item.validate().is_ok());
847 }
848
849 #[test]
850 fn test_create_cart_validate() {
851 assert!(CreateCart::default().validate().is_ok());
853
854 let bad = CreateCart {
856 items: Some(vec![AddCartItem {
857 sku: "SKU".into(),
858 name: "Item".into(),
859 quantity: -1,
860 unit_price: dec!(5.00),
861 ..Default::default()
862 }]),
863 ..Default::default()
864 };
865 assert!(bad.validate().is_err());
866
867 let good = CreateCart {
869 items: Some(vec![AddCartItem {
870 sku: "SKU".into(),
871 name: "Item".into(),
872 quantity: 2,
873 unit_price: dec!(5.00),
874 ..Default::default()
875 }]),
876 ..Default::default()
877 };
878 assert!(good.validate().is_ok());
879 }
880
881 #[test]
882 fn test_cart_status_from_str() {
883 use std::str::FromStr;
884
885 assert_eq!(CartStatus::from_str("active").unwrap(), CartStatus::Active);
886 assert_eq!(CartStatus::from_str("ready_for_payment").unwrap(), CartStatus::ReadyForPayment);
887 assert_eq!(CartStatus::from_str("paymentpending").unwrap(), CartStatus::PaymentPending);
888 assert_eq!(CartStatus::from_str("canceled").unwrap(), CartStatus::Cancelled);
889 }
890
891 #[test]
892 fn test_cart_payment_status_from_str() {
893 use std::str::FromStr;
894
895 assert_eq!(
896 CartPaymentStatus::from_str("method_selected").unwrap(),
897 CartPaymentStatus::MethodSelected
898 );
899 assert_eq!(
900 CartPaymentStatus::from_str("methodselected").unwrap(),
901 CartPaymentStatus::MethodSelected
902 );
903 assert_eq!(
904 CartPaymentStatus::from_str("authorized").unwrap(),
905 CartPaymentStatus::Authorized
906 );
907 assert_eq!(CartPaymentStatus::from_str("captured").unwrap(), CartPaymentStatus::Captured);
908 }
909
910 #[test]
911 fn test_fulfillment_type_from_str() {
912 use std::str::FromStr;
913
914 assert_eq!(FulfillmentType::from_str("shipping").unwrap(), FulfillmentType::Shipping);
915 assert_eq!(FulfillmentType::from_str("pick_up").unwrap(), FulfillmentType::Pickup);
916 assert_eq!(FulfillmentType::from_str("digital").unwrap(), FulfillmentType::Digital);
917 }
918
919 #[test]
920 fn test_recalculate_totals_does_not_double_count_tax() {
921 let mut cart = Cart {
922 id: CartId::new(),
923 cart_number: "CART-002".to_string(),
924 customer_id: None,
925 status: CartStatus::Active,
926 currency: CurrencyCode::USD,
927 items: vec![CartItem {
928 id: Uuid::new_v4(),
929 cart_id: CartId::new(),
930 product_id: None,
931 variant_id: None,
932 sku: "SKU-TAX".to_string(),
933 name: "Taxed Item".to_string(),
934 description: None,
935 image_url: None,
936 quantity: 1,
937 unit_price: dec!(10.00),
938 original_price: None,
939 discount_amount: Decimal::ZERO,
940 tax_amount: dec!(0.80),
941 total: dec!(10.80),
942 weight: None,
943 requires_shipping: false,
944 metadata: None,
945 created_at: Utc::now(),
946 updated_at: Utc::now(),
947 }],
948 subtotal: Decimal::ZERO,
949 tax_amount: dec!(0.80),
950 shipping_amount: Decimal::ZERO,
951 discount_amount: Decimal::ZERO,
952 grand_total: Decimal::ZERO,
953 customer_email: None,
954 customer_phone: None,
955 customer_name: None,
956 shipping_address: None,
957 billing_address: None,
958 billing_same_as_shipping: true,
959 fulfillment_type: None,
960 shipping_method: None,
961 shipping_carrier: None,
962 estimated_delivery: None,
963 payment_method: None,
964 payment_token: None,
965 payment_status: CartPaymentStatus::None,
966 coupon_code: None,
967 discount_description: None,
968 order_id: None,
969 order_number: None,
970 notes: None,
971 metadata: None,
972 inventory_reserved: false,
973 reservation_expires_at: None,
974 x402_payment: None,
975 expires_at: None,
976 completed_at: None,
977 created_at: Utc::now(),
978 updated_at: Utc::now(),
979 };
980
981 cart.recalculate_totals();
982
983 assert_eq!(cart.subtotal, dec!(10.00));
984 assert_eq!(cart.grand_total, dec!(10.80));
985 }
986}