Skip to main content

stateset_core/models/
order.rs

1//! Order domain models
2
3use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use stateset_primitives::{CurrencyCode, CustomerId, OrderId, OrderItemId, ProductId};
7use strum::{Display, EnumString};
8use uuid::Uuid;
9
10use crate::errors::Result;
11use crate::validation::{Validate, ValidationBuilder};
12
13/// Order aggregate root
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct Order {
16    pub id: OrderId,
17    pub order_number: String,
18    pub customer_id: CustomerId,
19    pub status: OrderStatus,
20    pub order_date: DateTime<Utc>,
21    pub total_amount: Decimal,
22    pub currency: CurrencyCode,
23    pub payment_status: PaymentStatus,
24    pub fulfillment_status: FulfillmentStatus,
25    pub payment_method: Option<String>,
26    pub shipping_method: Option<String>,
27    pub tracking_number: Option<String>,
28    pub notes: Option<String>,
29    pub shipping_address: Option<Address>,
30    pub billing_address: Option<Address>,
31    pub items: Vec<OrderItem>,
32    /// Version for optimistic locking
33    pub version: i32,
34    pub created_at: DateTime<Utc>,
35    pub updated_at: DateTime<Utc>,
36}
37
38/// Order line item
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct OrderItem {
41    pub id: OrderItemId,
42    pub order_id: OrderId,
43    pub product_id: ProductId,
44    pub variant_id: Option<Uuid>,
45    pub sku: String,
46    pub name: String,
47    pub quantity: i32,
48    pub unit_price: Decimal,
49    pub discount: Decimal,
50    pub tax_amount: Decimal,
51    pub total: Decimal,
52}
53
54/// Address structure (shipping/billing)
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct Address {
57    pub line1: String,
58    pub line2: Option<String>,
59    pub city: String,
60    pub state: Option<String>,
61    pub postal_code: String,
62    pub country: String,
63}
64
65/// Order status enumeration.
66///
67/// Uses [`strum`] derives for `Display` (`snake_case`) and `FromStr` (case-insensitive).
68#[derive(
69    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString,
70)]
71#[serde(rename_all = "snake_case")]
72#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
73#[non_exhaustive]
74pub enum OrderStatus {
75    #[default]
76    Pending,
77    Confirmed,
78    Processing,
79    Shipped,
80    Delivered,
81    #[strum(serialize = "cancelled", serialize = "canceled")]
82    Cancelled,
83    Refunded,
84}
85
86impl OrderStatus {
87    /// Check if a status transition is allowed.
88    #[must_use]
89    pub fn can_transition_to(self, next: Self) -> bool {
90        if self == next {
91            return true;
92        }
93
94        match self {
95            Self::Pending => matches!(next, Self::Confirmed | Self::Cancelled),
96            Self::Confirmed => matches!(next, Self::Processing | Self::Cancelled),
97            Self::Processing => matches!(next, Self::Shipped | Self::Cancelled),
98            Self::Shipped => matches!(next, Self::Delivered),
99            Self::Delivered => matches!(next, Self::Refunded),
100            Self::Cancelled | Self::Refunded => false,
101        }
102    }
103}
104
105/// Payment status enumeration.
106///
107/// Uses [`strum`] derives for `Display` (`snake_case`) and `FromStr` (case-insensitive).
108#[derive(
109    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString,
110)]
111#[serde(rename_all = "snake_case")]
112#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
113#[non_exhaustive]
114pub enum PaymentStatus {
115    #[default]
116    Pending,
117    Authorized,
118    Paid,
119    #[strum(serialize = "partially_paid", serialize = "partiallypaid")]
120    PartiallyPaid,
121    Refunded,
122    #[strum(serialize = "partially_refunded", serialize = "partiallyrefunded")]
123    PartiallyRefunded,
124    Failed,
125}
126
127/// Fulfillment status enumeration.
128///
129/// Uses [`strum`] derives for `Display` (`snake_case`) and `FromStr` (case-insensitive).
130#[derive(
131    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString,
132)]
133#[serde(rename_all = "snake_case")]
134#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
135#[non_exhaustive]
136pub enum FulfillmentStatus {
137    #[default]
138    Unfulfilled,
139    #[strum(serialize = "partially_fulfilled", serialize = "partiallyfulfilled")]
140    PartiallyFulfilled,
141    Fulfilled,
142    Shipped,
143    Delivered,
144}
145
146/// Input for creating a new order
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct CreateOrder {
149    pub customer_id: CustomerId,
150    pub items: Vec<CreateOrderItem>,
151    pub currency: Option<CurrencyCode>,
152    pub shipping_address: Option<Address>,
153    pub billing_address: Option<Address>,
154    pub notes: Option<String>,
155    pub payment_method: Option<String>,
156    pub shipping_method: Option<String>,
157}
158
159impl Default for CreateOrder {
160    fn default() -> Self {
161        Self {
162            customer_id: CustomerId::nil(),
163            items: vec![],
164            currency: None,
165            shipping_address: None,
166            billing_address: None,
167            notes: None,
168            payment_method: None,
169            shipping_method: None,
170        }
171    }
172}
173
174/// Input for creating an order item
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct CreateOrderItem {
177    pub product_id: ProductId,
178    pub variant_id: Option<Uuid>,
179    pub sku: String,
180    pub name: String,
181    pub quantity: i32,
182    pub unit_price: Decimal,
183    pub discount: Option<Decimal>,
184    pub tax_amount: Option<Decimal>,
185}
186
187impl Default for CreateOrderItem {
188    fn default() -> Self {
189        Self {
190            product_id: ProductId::nil(),
191            variant_id: None,
192            sku: String::new(),
193            name: String::new(),
194            quantity: 0,
195            unit_price: Decimal::ZERO,
196            discount: None,
197            tax_amount: None,
198        }
199    }
200}
201
202/// Input for updating an order
203#[derive(Debug, Clone, Default, Serialize, Deserialize)]
204pub struct UpdateOrder {
205    pub status: Option<OrderStatus>,
206    pub payment_status: Option<PaymentStatus>,
207    pub fulfillment_status: Option<FulfillmentStatus>,
208    pub tracking_number: Option<String>,
209    pub notes: Option<String>,
210    pub shipping_address: Option<Address>,
211    pub billing_address: Option<Address>,
212}
213
214/// Order filter for querying
215#[derive(Debug, Clone, Default, Serialize, Deserialize)]
216pub struct OrderFilter {
217    pub customer_id: Option<CustomerId>,
218    pub status: Option<OrderStatus>,
219    pub payment_status: Option<PaymentStatus>,
220    pub fulfillment_status: Option<FulfillmentStatus>,
221    pub from_date: Option<DateTime<Utc>>,
222    pub to_date: Option<DateTime<Utc>>,
223    pub limit: Option<u32>,
224    pub offset: Option<u32>,
225    /// Keyset cursor: return records after this `(sort_key, id)` pair.
226    /// Sort key is `order_date` (DESC ordering).
227    pub after_cursor: Option<(String, String)>,
228}
229
230impl Order {
231    /// Calculate total from items
232    #[must_use]
233    pub fn calculate_total(&self) -> Decimal {
234        self.items.iter().map(|item| item.total).sum()
235    }
236
237    /// Check if order can be cancelled
238    #[must_use]
239    pub const fn can_cancel(&self) -> bool {
240        matches!(
241            self.status,
242            OrderStatus::Pending | OrderStatus::Confirmed | OrderStatus::Processing
243        )
244    }
245
246    /// Check if order can be refunded
247    #[must_use]
248    pub const fn can_refund(&self) -> bool {
249        matches!(self.payment_status, PaymentStatus::Paid | PaymentStatus::PartiallyPaid)
250    }
251}
252
253/// Decimal places a line/order money total is rounded to.
254///
255/// Currency minor units vary (JPY has 0, most have 2, a few have 3), but the
256/// order pipeline has historically assumed 2; keeping a single constant here
257/// keeps every backend's rounding identical. A currency-aware version would
258/// thread the currency into `calculate_total`.
259pub(crate) const MONEY_SCALE: u32 = 2;
260
261impl OrderItem {
262    /// Calculate a line item's money total, rounded to the currency minor unit.
263    ///
264    /// The result is rounded to `MONEY_SCALE` (2) decimal places so the stored
265    /// line total is a real money amount and an order's `total_amount` (the sum
266    /// of these line totals) foots exactly to its line items. All order-creation
267    /// paths on both backends route through this function so they agree to the
268    /// cent.
269    #[must_use]
270    pub fn calculate_total(
271        quantity: i32,
272        unit_price: Decimal,
273        discount: Decimal,
274        tax: Decimal,
275    ) -> Decimal {
276        let subtotal = unit_price * Decimal::from(quantity);
277        (subtotal - discount + tax).round_dp(MONEY_SCALE)
278    }
279}
280
281impl Validate for CreateOrderItem {
282    /// Validate a single order line item.
283    ///
284    /// Rejects empty SKU/name, a non-positive quantity, a negative unit price,
285    /// and negative discount/tax amounts. A zero unit price is permitted (e.g.
286    /// free/gift items); only negative monetary amounts are rejected.
287    fn validate(&self) -> Result<()> {
288        ValidationBuilder::new()
289            .required("sku", &self.sku)
290            .required("name", &self.name)
291            .positive_i32("quantity", self.quantity)
292            .non_negative("unit_price", self.unit_price)
293            .non_negative("discount", self.discount.unwrap_or(Decimal::ZERO))
294            .non_negative("tax_amount", self.tax_amount.unwrap_or(Decimal::ZERO))
295            .build()
296    }
297}
298
299impl Validate for CreateOrder {
300    /// Validate an order create request.
301    ///
302    /// Requires a non-nil customer, at least one line item, and validates each
303    /// item. Currency consistency is enforced at the item level where prices
304    /// share the order's single currency.
305    fn validate(&self) -> Result<()> {
306        ValidationBuilder::new()
307            .uuid_not_nil("customer_id", self.customer_id.into_uuid())
308            .non_empty_list("items", &self.items)
309            .build()?;
310
311        for item in &self.items {
312            item.validate()?;
313        }
314
315        Ok(())
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use rust_decimal_macros::dec;
323
324    // ============================================================================
325    // Test Helpers
326    // ============================================================================
327
328    fn create_test_address() -> Address {
329        Address {
330            line1: "123 Main St".to_string(),
331            line2: Some("Apt 4".to_string()),
332            city: "San Francisco".to_string(),
333            state: Some("CA".to_string()),
334            postal_code: "94102".to_string(),
335            country: "US".to_string(),
336        }
337    }
338
339    fn create_test_order_item(quantity: i32, unit_price: Decimal) -> OrderItem {
340        let order_id = OrderId::new();
341        let discount = dec!(0.00);
342        let tax = (unit_price * Decimal::from(quantity) * dec!(0.08)).round_dp(2);
343        let total = OrderItem::calculate_total(quantity, unit_price, discount, tax);
344
345        OrderItem {
346            id: OrderItemId::new(),
347            order_id,
348            product_id: ProductId::new(),
349            variant_id: None,
350            sku: "TEST-SKU-001".to_string(),
351            name: "Test Product".to_string(),
352            quantity,
353            unit_price,
354            discount,
355            tax_amount: tax,
356            total,
357        }
358    }
359
360    fn create_test_order(status: OrderStatus, payment_status: PaymentStatus) -> Order {
361        let now = Utc::now();
362        let items =
363            vec![create_test_order_item(2, dec!(29.99)), create_test_order_item(1, dec!(49.99))];
364        let total: Decimal = items.iter().map(|i| i.total).sum();
365
366        Order {
367            id: OrderId::new(),
368            order_number: "ORD-2024-001".to_string(),
369            customer_id: CustomerId::new(),
370            status,
371            order_date: now,
372            total_amount: total,
373            currency: CurrencyCode::USD,
374            payment_status,
375            fulfillment_status: FulfillmentStatus::Unfulfilled,
376            payment_method: Some("credit_card".to_string()),
377            shipping_method: Some("standard".to_string()),
378            tracking_number: None,
379            notes: None,
380            shipping_address: Some(create_test_address()),
381            billing_address: None,
382            items,
383            version: 1,
384            created_at: now,
385            updated_at: now,
386        }
387    }
388
389    // ============================================================================
390    // Order Tests
391    // ============================================================================
392
393    #[test]
394    fn test_order_calculate_total() {
395        let order = create_test_order(OrderStatus::Pending, PaymentStatus::Pending);
396        let calculated = order.calculate_total();
397        let expected: Decimal = order.items.iter().map(|i| i.total).sum();
398        assert_eq!(calculated, expected);
399    }
400
401    #[test]
402    fn test_order_calculate_total_empty_items() {
403        let mut order = create_test_order(OrderStatus::Pending, PaymentStatus::Pending);
404        order.items.clear();
405        assert_eq!(order.calculate_total(), dec!(0));
406    }
407
408    #[test]
409    fn test_order_can_cancel_pending() {
410        let order = create_test_order(OrderStatus::Pending, PaymentStatus::Pending);
411        assert!(order.can_cancel());
412    }
413
414    #[test]
415    fn test_order_can_cancel_confirmed() {
416        let order = create_test_order(OrderStatus::Confirmed, PaymentStatus::Authorized);
417        assert!(order.can_cancel());
418    }
419
420    #[test]
421    fn test_order_can_cancel_processing() {
422        let order = create_test_order(OrderStatus::Processing, PaymentStatus::Paid);
423        assert!(order.can_cancel());
424    }
425
426    #[test]
427    fn test_order_cannot_cancel_shipped() {
428        let order = create_test_order(OrderStatus::Shipped, PaymentStatus::Paid);
429        assert!(!order.can_cancel());
430    }
431
432    #[test]
433    fn test_order_cannot_cancel_delivered() {
434        let order = create_test_order(OrderStatus::Delivered, PaymentStatus::Paid);
435        assert!(!order.can_cancel());
436    }
437
438    #[test]
439    fn test_order_cannot_cancel_already_cancelled() {
440        let order = create_test_order(OrderStatus::Cancelled, PaymentStatus::Refunded);
441        assert!(!order.can_cancel());
442    }
443
444    #[test]
445    fn test_order_can_refund_when_paid() {
446        let order = create_test_order(OrderStatus::Delivered, PaymentStatus::Paid);
447        assert!(order.can_refund());
448    }
449
450    #[test]
451    fn test_order_can_refund_when_partially_paid() {
452        let order = create_test_order(OrderStatus::Delivered, PaymentStatus::PartiallyPaid);
453        assert!(order.can_refund());
454    }
455
456    #[test]
457    fn test_order_status_allows_valid_transitions() {
458        assert!(OrderStatus::Pending.can_transition_to(OrderStatus::Confirmed));
459        assert!(OrderStatus::Confirmed.can_transition_to(OrderStatus::Processing));
460        assert!(OrderStatus::Processing.can_transition_to(OrderStatus::Shipped));
461        assert!(OrderStatus::Shipped.can_transition_to(OrderStatus::Delivered));
462        assert!(OrderStatus::Delivered.can_transition_to(OrderStatus::Refunded));
463        assert!(OrderStatus::Pending.can_transition_to(OrderStatus::Cancelled));
464    }
465
466    #[test]
467    fn test_order_status_rejects_invalid_transitions() {
468        assert!(!OrderStatus::Pending.can_transition_to(OrderStatus::Delivered));
469        assert!(!OrderStatus::Shipped.can_transition_to(OrderStatus::Cancelled));
470        assert!(!OrderStatus::Refunded.can_transition_to(OrderStatus::Processing));
471    }
472
473    #[test]
474    fn test_order_status_allows_idempotent_transition() {
475        assert!(OrderStatus::Pending.can_transition_to(OrderStatus::Pending));
476        assert!(OrderStatus::Cancelled.can_transition_to(OrderStatus::Cancelled));
477    }
478
479    #[test]
480    fn test_status_from_str_accepts_legacy_variants() {
481        use std::str::FromStr;
482
483        assert_eq!(PaymentStatus::from_str("partiallypaid").unwrap(), PaymentStatus::PartiallyPaid);
484        assert_eq!(
485            PaymentStatus::from_str("partiallyrefunded").unwrap(),
486            PaymentStatus::PartiallyRefunded
487        );
488        assert_eq!(
489            FulfillmentStatus::from_str("partiallyfulfilled").unwrap(),
490            FulfillmentStatus::PartiallyFulfilled
491        );
492        assert_eq!(OrderStatus::from_str("canceled").unwrap(), OrderStatus::Cancelled);
493    }
494
495    #[test]
496    fn test_order_cannot_refund_when_pending() {
497        let order = create_test_order(OrderStatus::Pending, PaymentStatus::Pending);
498        assert!(!order.can_refund());
499    }
500
501    #[test]
502    fn test_order_cannot_refund_when_already_refunded() {
503        let order = create_test_order(OrderStatus::Refunded, PaymentStatus::Refunded);
504        assert!(!order.can_refund());
505    }
506
507    // ============================================================================
508    // OrderItem Tests
509    // ============================================================================
510
511    #[test]
512    fn test_order_item_calculate_total_basic() {
513        let total = OrderItem::calculate_total(2, dec!(29.99), dec!(0), dec!(4.80));
514        // 2 * 29.99 = 59.98, - 0 + 4.80 = 64.78
515        assert_eq!(total, dec!(64.78));
516    }
517
518    #[test]
519    fn test_order_item_calculate_total_with_discount() {
520        let total = OrderItem::calculate_total(2, dec!(29.99), dec!(10.00), dec!(4.00));
521        // 2 * 29.99 = 59.98, - 10.00 + 4.00 = 53.98
522        assert_eq!(total, dec!(53.98));
523    }
524
525    #[test]
526    fn test_order_item_calculate_total_zero_quantity() {
527        let total = OrderItem::calculate_total(0, dec!(29.99), dec!(0), dec!(0));
528        assert_eq!(total, dec!(0));
529    }
530
531    #[test]
532    fn test_order_item_calculate_total_high_quantity() {
533        let total = OrderItem::calculate_total(1000, dec!(9.99), dec!(0), dec!(799.20));
534        // 1000 * 9.99 = 9990, + 799.20 = 10789.20
535        assert_eq!(total, dec!(10789.20));
536    }
537
538    // ============================================================================
539    // OrderStatus Tests
540    // ============================================================================
541
542    #[test]
543    fn test_order_status_default() {
544        assert_eq!(OrderStatus::default(), OrderStatus::Pending);
545    }
546
547    #[test]
548    fn test_order_status_display() {
549        assert_eq!(format!("{}", OrderStatus::Pending), "pending");
550        assert_eq!(format!("{}", OrderStatus::Confirmed), "confirmed");
551        assert_eq!(format!("{}", OrderStatus::Processing), "processing");
552        assert_eq!(format!("{}", OrderStatus::Shipped), "shipped");
553        assert_eq!(format!("{}", OrderStatus::Delivered), "delivered");
554        assert_eq!(format!("{}", OrderStatus::Cancelled), "cancelled");
555        assert_eq!(format!("{}", OrderStatus::Refunded), "refunded");
556    }
557
558    #[test]
559    fn test_order_status_serialization() {
560        let status = OrderStatus::Processing;
561        let json = serde_json::to_string(&status).unwrap();
562        assert_eq!(json, "\"processing\"");
563
564        let deserialized: OrderStatus = serde_json::from_str(&json).unwrap();
565        assert_eq!(deserialized, status);
566    }
567
568    // ============================================================================
569    // PaymentStatus Tests
570    // ============================================================================
571
572    #[test]
573    fn test_payment_status_default() {
574        assert_eq!(PaymentStatus::default(), PaymentStatus::Pending);
575    }
576
577    #[test]
578    fn test_payment_status_display() {
579        assert_eq!(format!("{}", PaymentStatus::Pending), "pending");
580        assert_eq!(format!("{}", PaymentStatus::Authorized), "authorized");
581        assert_eq!(format!("{}", PaymentStatus::Paid), "paid");
582        assert_eq!(format!("{}", PaymentStatus::PartiallyPaid), "partially_paid");
583        assert_eq!(format!("{}", PaymentStatus::Refunded), "refunded");
584        assert_eq!(format!("{}", PaymentStatus::PartiallyRefunded), "partially_refunded");
585        assert_eq!(format!("{}", PaymentStatus::Failed), "failed");
586    }
587
588    #[test]
589    fn test_payment_status_serialization() {
590        let status = PaymentStatus::PartiallyPaid;
591        let json = serde_json::to_string(&status).unwrap();
592        assert_eq!(json, "\"partially_paid\"");
593
594        let deserialized: PaymentStatus = serde_json::from_str(&json).unwrap();
595        assert_eq!(deserialized, status);
596    }
597
598    // ============================================================================
599    // FulfillmentStatus Tests
600    // ============================================================================
601
602    #[test]
603    fn test_fulfillment_status_default() {
604        assert_eq!(FulfillmentStatus::default(), FulfillmentStatus::Unfulfilled);
605    }
606
607    #[test]
608    fn test_fulfillment_status_display() {
609        assert_eq!(format!("{}", FulfillmentStatus::Unfulfilled), "unfulfilled");
610        assert_eq!(format!("{}", FulfillmentStatus::PartiallyFulfilled), "partially_fulfilled");
611        assert_eq!(format!("{}", FulfillmentStatus::Fulfilled), "fulfilled");
612        assert_eq!(format!("{}", FulfillmentStatus::Shipped), "shipped");
613        assert_eq!(format!("{}", FulfillmentStatus::Delivered), "delivered");
614    }
615
616    // ============================================================================
617    // CreateOrder Tests
618    // ============================================================================
619
620    #[test]
621    fn test_create_order_default() {
622        let create_order = CreateOrder::default();
623        assert!(create_order.customer_id.is_nil());
624        assert!(create_order.items.is_empty());
625        assert!(create_order.currency.is_none());
626        assert!(create_order.shipping_address.is_none());
627    }
628
629    // ============================================================================
630    // CreateOrderItem Tests
631    // ============================================================================
632
633    #[test]
634    fn test_create_order_item_default() {
635        let item = CreateOrderItem::default();
636        assert!(item.product_id.is_nil());
637        assert_eq!(item.quantity, 0);
638        assert_eq!(item.unit_price, Decimal::ZERO);
639        assert!(item.sku.is_empty());
640    }
641
642    // ============================================================================
643    // Address Tests
644    // ============================================================================
645
646    #[test]
647    fn test_address_serialization_roundtrip() {
648        let address = create_test_address();
649        let json = serde_json::to_string(&address).unwrap();
650        let deserialized: Address = serde_json::from_str(&json).unwrap();
651        assert_eq!(address, deserialized);
652    }
653
654    #[test]
655    fn test_address_without_optional_fields() {
656        let address = Address {
657            line1: "123 Main St".to_string(),
658            line2: None,
659            city: "NYC".to_string(),
660            state: None,
661            postal_code: "10001".to_string(),
662            country: "US".to_string(),
663        };
664
665        let json = serde_json::to_string(&address).unwrap();
666        let deserialized: Address = serde_json::from_str(&json).unwrap();
667        assert_eq!(address, deserialized);
668    }
669
670    // ============================================================================
671    // Order Serialization Tests
672    // ============================================================================
673
674    #[test]
675    fn test_order_serialization_roundtrip() {
676        let order = create_test_order(OrderStatus::Confirmed, PaymentStatus::Paid);
677        let json = serde_json::to_string(&order).unwrap();
678        let deserialized: Order = serde_json::from_str(&json).unwrap();
679        assert_eq!(order, deserialized);
680    }
681
682    #[test]
683    fn test_order_item_serialization_roundtrip() {
684        let item = create_test_order_item(3, dec!(19.99));
685        let json = serde_json::to_string(&item).unwrap();
686        let deserialized: OrderItem = serde_json::from_str(&json).unwrap();
687        assert_eq!(item, deserialized);
688    }
689
690    // ============================================================================
691    // UpdateOrder Tests
692    // ============================================================================
693
694    #[test]
695    fn test_update_order_default() {
696        let update = UpdateOrder::default();
697        assert!(update.status.is_none());
698        assert!(update.payment_status.is_none());
699        assert!(update.fulfillment_status.is_none());
700        assert!(update.tracking_number.is_none());
701    }
702
703    #[test]
704    fn test_update_order_partial() {
705        let update = UpdateOrder {
706            status: Some(OrderStatus::Shipped),
707            tracking_number: Some("1Z999AA10123456784".to_string()),
708            ..Default::default()
709        };
710
711        assert_eq!(update.status, Some(OrderStatus::Shipped));
712        assert!(update.tracking_number.is_some());
713        assert!(update.payment_status.is_none());
714    }
715
716    // ============================================================================
717    // OrderFilter Tests
718    // ============================================================================
719
720    #[test]
721    fn test_order_filter_default() {
722        let filter = OrderFilter::default();
723        assert!(filter.customer_id.is_none());
724        assert!(filter.status.is_none());
725        assert!(filter.limit.is_none());
726        assert!(filter.offset.is_none());
727    }
728
729    #[test]
730    fn test_order_filter_with_values() {
731        let customer_id = CustomerId::new();
732        let filter = OrderFilter {
733            customer_id: Some(customer_id),
734            status: Some(OrderStatus::Pending),
735            limit: Some(10),
736            offset: Some(0),
737            ..Default::default()
738        };
739
740        assert_eq!(filter.customer_id, Some(customer_id));
741        assert_eq!(filter.status, Some(OrderStatus::Pending));
742        assert_eq!(filter.limit, Some(10));
743    }
744
745    fn valid_create_order_item() -> CreateOrderItem {
746        CreateOrderItem {
747            product_id: ProductId::new(),
748            sku: "SKU-1".to_string(),
749            name: "Item".to_string(),
750            quantity: 1,
751            unit_price: dec!(10.00),
752            ..Default::default()
753        }
754    }
755
756    #[test]
757    fn test_create_order_item_validate() {
758        // Valid item passes.
759        assert!(valid_create_order_item().validate().is_ok());
760
761        // Negative unit price is rejected.
762        let mut item = valid_create_order_item();
763        item.unit_price = dec!(-0.01);
764        assert!(item.validate().is_err());
765
766        // Non-positive quantity is rejected.
767        let mut item = valid_create_order_item();
768        item.quantity = 0;
769        assert!(item.validate().is_err());
770        item.quantity = -2;
771        assert!(item.validate().is_err());
772
773        // Negative discount / tax are rejected.
774        let mut item = valid_create_order_item();
775        item.discount = Some(dec!(-1));
776        assert!(item.validate().is_err());
777        let mut item = valid_create_order_item();
778        item.tax_amount = Some(dec!(-1));
779        assert!(item.validate().is_err());
780
781        // A free ($0) item with positive quantity is valid.
782        let mut item = valid_create_order_item();
783        item.unit_price = dec!(0);
784        assert!(item.validate().is_ok());
785    }
786
787    #[test]
788    fn test_create_order_validate() {
789        // Valid order with one item passes.
790        let order = CreateOrder {
791            customer_id: CustomerId::new(),
792            items: vec![valid_create_order_item()],
793            ..Default::default()
794        };
795        assert!(order.validate().is_ok());
796
797        // Nil customer is rejected.
798        let order = CreateOrder {
799            customer_id: CustomerId::nil(),
800            items: vec![valid_create_order_item()],
801            ..Default::default()
802        };
803        assert!(order.validate().is_err());
804
805        // Empty items list is rejected.
806        let order =
807            CreateOrder { customer_id: CustomerId::new(), items: vec![], ..Default::default() };
808        assert!(order.validate().is_err());
809
810        // An invalid line item is rejected.
811        let mut bad_item = valid_create_order_item();
812        bad_item.quantity = -1;
813        let order = CreateOrder {
814            customer_id: CustomerId::new(),
815            items: vec![bad_item],
816            ..Default::default()
817        };
818        assert!(order.validate().is_err());
819    }
820}