use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use stateset_primitives::{CurrencyCode, CustomerId, OrderId, OrderItemId, ProductId};
use strum::{Display, EnumString};
use uuid::Uuid;
use crate::errors::Result;
use crate::validation::{Validate, ValidationBuilder};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Order {
pub id: OrderId,
pub order_number: String,
pub customer_id: CustomerId,
pub status: OrderStatus,
pub order_date: DateTime<Utc>,
pub total_amount: Decimal,
pub currency: CurrencyCode,
pub payment_status: PaymentStatus,
pub fulfillment_status: FulfillmentStatus,
pub payment_method: Option<String>,
pub shipping_method: Option<String>,
pub tracking_number: Option<String>,
pub notes: Option<String>,
pub shipping_address: Option<Address>,
pub billing_address: Option<Address>,
pub items: Vec<OrderItem>,
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrderItem {
pub id: OrderItemId,
pub order_id: OrderId,
pub product_id: ProductId,
pub variant_id: Option<Uuid>,
pub sku: String,
pub name: String,
pub quantity: i32,
pub unit_price: Decimal,
pub discount: Decimal,
pub tax_amount: Decimal,
pub total: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Address {
pub line1: String,
pub line2: Option<String>,
pub city: String,
pub state: Option<String>,
pub postal_code: String,
pub country: String,
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum OrderStatus {
#[default]
Pending,
Confirmed,
Processing,
Shipped,
Delivered,
#[strum(serialize = "cancelled", serialize = "canceled")]
Cancelled,
Refunded,
}
impl OrderStatus {
#[must_use]
pub fn can_transition_to(self, next: Self) -> bool {
if self == next {
return true;
}
match self {
Self::Pending => matches!(next, Self::Confirmed | Self::Cancelled),
Self::Confirmed => matches!(next, Self::Processing | Self::Cancelled),
Self::Processing => matches!(next, Self::Shipped | Self::Cancelled),
Self::Shipped => matches!(next, Self::Delivered),
Self::Delivered => matches!(next, Self::Refunded),
Self::Cancelled | Self::Refunded => false,
}
}
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum PaymentStatus {
#[default]
Pending,
Authorized,
Paid,
#[strum(serialize = "partially_paid", serialize = "partiallypaid")]
PartiallyPaid,
Refunded,
#[strum(serialize = "partially_refunded", serialize = "partiallyrefunded")]
PartiallyRefunded,
Failed,
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum FulfillmentStatus {
#[default]
Unfulfilled,
#[strum(serialize = "partially_fulfilled", serialize = "partiallyfulfilled")]
PartiallyFulfilled,
Fulfilled,
Shipped,
Delivered,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateOrder {
pub customer_id: CustomerId,
pub items: Vec<CreateOrderItem>,
pub currency: Option<CurrencyCode>,
pub shipping_address: Option<Address>,
pub billing_address: Option<Address>,
pub notes: Option<String>,
pub payment_method: Option<String>,
pub shipping_method: Option<String>,
}
impl Default for CreateOrder {
fn default() -> Self {
Self {
customer_id: CustomerId::nil(),
items: vec![],
currency: None,
shipping_address: None,
billing_address: None,
notes: None,
payment_method: None,
shipping_method: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateOrderItem {
pub product_id: ProductId,
pub variant_id: Option<Uuid>,
pub sku: String,
pub name: String,
pub quantity: i32,
pub unit_price: Decimal,
pub discount: Option<Decimal>,
pub tax_amount: Option<Decimal>,
}
impl Default for CreateOrderItem {
fn default() -> Self {
Self {
product_id: ProductId::nil(),
variant_id: None,
sku: String::new(),
name: String::new(),
quantity: 0,
unit_price: Decimal::ZERO,
discount: None,
tax_amount: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateOrder {
pub status: Option<OrderStatus>,
pub payment_status: Option<PaymentStatus>,
pub fulfillment_status: Option<FulfillmentStatus>,
pub tracking_number: Option<String>,
pub notes: Option<String>,
pub shipping_address: Option<Address>,
pub billing_address: Option<Address>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OrderFilter {
pub customer_id: Option<CustomerId>,
pub status: Option<OrderStatus>,
pub payment_status: Option<PaymentStatus>,
pub fulfillment_status: Option<FulfillmentStatus>,
pub from_date: Option<DateTime<Utc>>,
pub to_date: Option<DateTime<Utc>>,
pub limit: Option<u32>,
pub offset: Option<u32>,
pub after_cursor: Option<(String, String)>,
}
impl Order {
#[must_use]
pub fn calculate_total(&self) -> Decimal {
self.items.iter().map(|item| item.total).sum()
}
#[must_use]
pub const fn can_cancel(&self) -> bool {
matches!(
self.status,
OrderStatus::Pending | OrderStatus::Confirmed | OrderStatus::Processing
)
}
#[must_use]
pub const fn can_refund(&self) -> bool {
matches!(self.payment_status, PaymentStatus::Paid | PaymentStatus::PartiallyPaid)
}
}
pub(crate) const MONEY_SCALE: u32 = 2;
impl OrderItem {
#[must_use]
pub fn calculate_total(
quantity: i32,
unit_price: Decimal,
discount: Decimal,
tax: Decimal,
) -> Decimal {
let subtotal = unit_price * Decimal::from(quantity);
(subtotal - discount + tax).round_dp(MONEY_SCALE)
}
}
impl Validate for CreateOrderItem {
fn validate(&self) -> Result<()> {
ValidationBuilder::new()
.required("sku", &self.sku)
.required("name", &self.name)
.positive_i32("quantity", self.quantity)
.non_negative("unit_price", self.unit_price)
.non_negative("discount", self.discount.unwrap_or(Decimal::ZERO))
.non_negative("tax_amount", self.tax_amount.unwrap_or(Decimal::ZERO))
.build()
}
}
impl Validate for CreateOrder {
fn validate(&self) -> Result<()> {
ValidationBuilder::new()
.uuid_not_nil("customer_id", self.customer_id.into_uuid())
.non_empty_list("items", &self.items)
.build()?;
for item in &self.items {
item.validate()?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn create_test_address() -> Address {
Address {
line1: "123 Main St".to_string(),
line2: Some("Apt 4".to_string()),
city: "San Francisco".to_string(),
state: Some("CA".to_string()),
postal_code: "94102".to_string(),
country: "US".to_string(),
}
}
fn create_test_order_item(quantity: i32, unit_price: Decimal) -> OrderItem {
let order_id = OrderId::new();
let discount = dec!(0.00);
let tax = (unit_price * Decimal::from(quantity) * dec!(0.08)).round_dp(2);
let total = OrderItem::calculate_total(quantity, unit_price, discount, tax);
OrderItem {
id: OrderItemId::new(),
order_id,
product_id: ProductId::new(),
variant_id: None,
sku: "TEST-SKU-001".to_string(),
name: "Test Product".to_string(),
quantity,
unit_price,
discount,
tax_amount: tax,
total,
}
}
fn create_test_order(status: OrderStatus, payment_status: PaymentStatus) -> Order {
let now = Utc::now();
let items =
vec![create_test_order_item(2, dec!(29.99)), create_test_order_item(1, dec!(49.99))];
let total: Decimal = items.iter().map(|i| i.total).sum();
Order {
id: OrderId::new(),
order_number: "ORD-2024-001".to_string(),
customer_id: CustomerId::new(),
status,
order_date: now,
total_amount: total,
currency: CurrencyCode::USD,
payment_status,
fulfillment_status: FulfillmentStatus::Unfulfilled,
payment_method: Some("credit_card".to_string()),
shipping_method: Some("standard".to_string()),
tracking_number: None,
notes: None,
shipping_address: Some(create_test_address()),
billing_address: None,
items,
version: 1,
created_at: now,
updated_at: now,
}
}
#[test]
fn test_order_calculate_total() {
let order = create_test_order(OrderStatus::Pending, PaymentStatus::Pending);
let calculated = order.calculate_total();
let expected: Decimal = order.items.iter().map(|i| i.total).sum();
assert_eq!(calculated, expected);
}
#[test]
fn test_order_calculate_total_empty_items() {
let mut order = create_test_order(OrderStatus::Pending, PaymentStatus::Pending);
order.items.clear();
assert_eq!(order.calculate_total(), dec!(0));
}
#[test]
fn test_order_can_cancel_pending() {
let order = create_test_order(OrderStatus::Pending, PaymentStatus::Pending);
assert!(order.can_cancel());
}
#[test]
fn test_order_can_cancel_confirmed() {
let order = create_test_order(OrderStatus::Confirmed, PaymentStatus::Authorized);
assert!(order.can_cancel());
}
#[test]
fn test_order_can_cancel_processing() {
let order = create_test_order(OrderStatus::Processing, PaymentStatus::Paid);
assert!(order.can_cancel());
}
#[test]
fn test_order_cannot_cancel_shipped() {
let order = create_test_order(OrderStatus::Shipped, PaymentStatus::Paid);
assert!(!order.can_cancel());
}
#[test]
fn test_order_cannot_cancel_delivered() {
let order = create_test_order(OrderStatus::Delivered, PaymentStatus::Paid);
assert!(!order.can_cancel());
}
#[test]
fn test_order_cannot_cancel_already_cancelled() {
let order = create_test_order(OrderStatus::Cancelled, PaymentStatus::Refunded);
assert!(!order.can_cancel());
}
#[test]
fn test_order_can_refund_when_paid() {
let order = create_test_order(OrderStatus::Delivered, PaymentStatus::Paid);
assert!(order.can_refund());
}
#[test]
fn test_order_can_refund_when_partially_paid() {
let order = create_test_order(OrderStatus::Delivered, PaymentStatus::PartiallyPaid);
assert!(order.can_refund());
}
#[test]
fn test_order_status_allows_valid_transitions() {
assert!(OrderStatus::Pending.can_transition_to(OrderStatus::Confirmed));
assert!(OrderStatus::Confirmed.can_transition_to(OrderStatus::Processing));
assert!(OrderStatus::Processing.can_transition_to(OrderStatus::Shipped));
assert!(OrderStatus::Shipped.can_transition_to(OrderStatus::Delivered));
assert!(OrderStatus::Delivered.can_transition_to(OrderStatus::Refunded));
assert!(OrderStatus::Pending.can_transition_to(OrderStatus::Cancelled));
}
#[test]
fn test_order_status_rejects_invalid_transitions() {
assert!(!OrderStatus::Pending.can_transition_to(OrderStatus::Delivered));
assert!(!OrderStatus::Shipped.can_transition_to(OrderStatus::Cancelled));
assert!(!OrderStatus::Refunded.can_transition_to(OrderStatus::Processing));
}
#[test]
fn test_order_status_allows_idempotent_transition() {
assert!(OrderStatus::Pending.can_transition_to(OrderStatus::Pending));
assert!(OrderStatus::Cancelled.can_transition_to(OrderStatus::Cancelled));
}
#[test]
fn test_status_from_str_accepts_legacy_variants() {
use std::str::FromStr;
assert_eq!(PaymentStatus::from_str("partiallypaid").unwrap(), PaymentStatus::PartiallyPaid);
assert_eq!(
PaymentStatus::from_str("partiallyrefunded").unwrap(),
PaymentStatus::PartiallyRefunded
);
assert_eq!(
FulfillmentStatus::from_str("partiallyfulfilled").unwrap(),
FulfillmentStatus::PartiallyFulfilled
);
assert_eq!(OrderStatus::from_str("canceled").unwrap(), OrderStatus::Cancelled);
}
#[test]
fn test_order_cannot_refund_when_pending() {
let order = create_test_order(OrderStatus::Pending, PaymentStatus::Pending);
assert!(!order.can_refund());
}
#[test]
fn test_order_cannot_refund_when_already_refunded() {
let order = create_test_order(OrderStatus::Refunded, PaymentStatus::Refunded);
assert!(!order.can_refund());
}
#[test]
fn test_order_item_calculate_total_basic() {
let total = OrderItem::calculate_total(2, dec!(29.99), dec!(0), dec!(4.80));
assert_eq!(total, dec!(64.78));
}
#[test]
fn test_order_item_calculate_total_with_discount() {
let total = OrderItem::calculate_total(2, dec!(29.99), dec!(10.00), dec!(4.00));
assert_eq!(total, dec!(53.98));
}
#[test]
fn test_order_item_calculate_total_zero_quantity() {
let total = OrderItem::calculate_total(0, dec!(29.99), dec!(0), dec!(0));
assert_eq!(total, dec!(0));
}
#[test]
fn test_order_item_calculate_total_high_quantity() {
let total = OrderItem::calculate_total(1000, dec!(9.99), dec!(0), dec!(799.20));
assert_eq!(total, dec!(10789.20));
}
#[test]
fn test_order_status_default() {
assert_eq!(OrderStatus::default(), OrderStatus::Pending);
}
#[test]
fn test_order_status_display() {
assert_eq!(format!("{}", OrderStatus::Pending), "pending");
assert_eq!(format!("{}", OrderStatus::Confirmed), "confirmed");
assert_eq!(format!("{}", OrderStatus::Processing), "processing");
assert_eq!(format!("{}", OrderStatus::Shipped), "shipped");
assert_eq!(format!("{}", OrderStatus::Delivered), "delivered");
assert_eq!(format!("{}", OrderStatus::Cancelled), "cancelled");
assert_eq!(format!("{}", OrderStatus::Refunded), "refunded");
}
#[test]
fn test_order_status_serialization() {
let status = OrderStatus::Processing;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, "\"processing\"");
let deserialized: OrderStatus = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, status);
}
#[test]
fn test_payment_status_default() {
assert_eq!(PaymentStatus::default(), PaymentStatus::Pending);
}
#[test]
fn test_payment_status_display() {
assert_eq!(format!("{}", PaymentStatus::Pending), "pending");
assert_eq!(format!("{}", PaymentStatus::Authorized), "authorized");
assert_eq!(format!("{}", PaymentStatus::Paid), "paid");
assert_eq!(format!("{}", PaymentStatus::PartiallyPaid), "partially_paid");
assert_eq!(format!("{}", PaymentStatus::Refunded), "refunded");
assert_eq!(format!("{}", PaymentStatus::PartiallyRefunded), "partially_refunded");
assert_eq!(format!("{}", PaymentStatus::Failed), "failed");
}
#[test]
fn test_payment_status_serialization() {
let status = PaymentStatus::PartiallyPaid;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, "\"partially_paid\"");
let deserialized: PaymentStatus = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, status);
}
#[test]
fn test_fulfillment_status_default() {
assert_eq!(FulfillmentStatus::default(), FulfillmentStatus::Unfulfilled);
}
#[test]
fn test_fulfillment_status_display() {
assert_eq!(format!("{}", FulfillmentStatus::Unfulfilled), "unfulfilled");
assert_eq!(format!("{}", FulfillmentStatus::PartiallyFulfilled), "partially_fulfilled");
assert_eq!(format!("{}", FulfillmentStatus::Fulfilled), "fulfilled");
assert_eq!(format!("{}", FulfillmentStatus::Shipped), "shipped");
assert_eq!(format!("{}", FulfillmentStatus::Delivered), "delivered");
}
#[test]
fn test_create_order_default() {
let create_order = CreateOrder::default();
assert!(create_order.customer_id.is_nil());
assert!(create_order.items.is_empty());
assert!(create_order.currency.is_none());
assert!(create_order.shipping_address.is_none());
}
#[test]
fn test_create_order_item_default() {
let item = CreateOrderItem::default();
assert!(item.product_id.is_nil());
assert_eq!(item.quantity, 0);
assert_eq!(item.unit_price, Decimal::ZERO);
assert!(item.sku.is_empty());
}
#[test]
fn test_address_serialization_roundtrip() {
let address = create_test_address();
let json = serde_json::to_string(&address).unwrap();
let deserialized: Address = serde_json::from_str(&json).unwrap();
assert_eq!(address, deserialized);
}
#[test]
fn test_address_without_optional_fields() {
let address = Address {
line1: "123 Main St".to_string(),
line2: None,
city: "NYC".to_string(),
state: None,
postal_code: "10001".to_string(),
country: "US".to_string(),
};
let json = serde_json::to_string(&address).unwrap();
let deserialized: Address = serde_json::from_str(&json).unwrap();
assert_eq!(address, deserialized);
}
#[test]
fn test_order_serialization_roundtrip() {
let order = create_test_order(OrderStatus::Confirmed, PaymentStatus::Paid);
let json = serde_json::to_string(&order).unwrap();
let deserialized: Order = serde_json::from_str(&json).unwrap();
assert_eq!(order, deserialized);
}
#[test]
fn test_order_item_serialization_roundtrip() {
let item = create_test_order_item(3, dec!(19.99));
let json = serde_json::to_string(&item).unwrap();
let deserialized: OrderItem = serde_json::from_str(&json).unwrap();
assert_eq!(item, deserialized);
}
#[test]
fn test_update_order_default() {
let update = UpdateOrder::default();
assert!(update.status.is_none());
assert!(update.payment_status.is_none());
assert!(update.fulfillment_status.is_none());
assert!(update.tracking_number.is_none());
}
#[test]
fn test_update_order_partial() {
let update = UpdateOrder {
status: Some(OrderStatus::Shipped),
tracking_number: Some("1Z999AA10123456784".to_string()),
..Default::default()
};
assert_eq!(update.status, Some(OrderStatus::Shipped));
assert!(update.tracking_number.is_some());
assert!(update.payment_status.is_none());
}
#[test]
fn test_order_filter_default() {
let filter = OrderFilter::default();
assert!(filter.customer_id.is_none());
assert!(filter.status.is_none());
assert!(filter.limit.is_none());
assert!(filter.offset.is_none());
}
#[test]
fn test_order_filter_with_values() {
let customer_id = CustomerId::new();
let filter = OrderFilter {
customer_id: Some(customer_id),
status: Some(OrderStatus::Pending),
limit: Some(10),
offset: Some(0),
..Default::default()
};
assert_eq!(filter.customer_id, Some(customer_id));
assert_eq!(filter.status, Some(OrderStatus::Pending));
assert_eq!(filter.limit, Some(10));
}
fn valid_create_order_item() -> CreateOrderItem {
CreateOrderItem {
product_id: ProductId::new(),
sku: "SKU-1".to_string(),
name: "Item".to_string(),
quantity: 1,
unit_price: dec!(10.00),
..Default::default()
}
}
#[test]
fn test_create_order_item_validate() {
assert!(valid_create_order_item().validate().is_ok());
let mut item = valid_create_order_item();
item.unit_price = dec!(-0.01);
assert!(item.validate().is_err());
let mut item = valid_create_order_item();
item.quantity = 0;
assert!(item.validate().is_err());
item.quantity = -2;
assert!(item.validate().is_err());
let mut item = valid_create_order_item();
item.discount = Some(dec!(-1));
assert!(item.validate().is_err());
let mut item = valid_create_order_item();
item.tax_amount = Some(dec!(-1));
assert!(item.validate().is_err());
let mut item = valid_create_order_item();
item.unit_price = dec!(0);
assert!(item.validate().is_ok());
}
#[test]
fn test_create_order_validate() {
let order = CreateOrder {
customer_id: CustomerId::new(),
items: vec![valid_create_order_item()],
..Default::default()
};
assert!(order.validate().is_ok());
let order = CreateOrder {
customer_id: CustomerId::nil(),
items: vec![valid_create_order_item()],
..Default::default()
};
assert!(order.validate().is_err());
let order =
CreateOrder { customer_id: CustomerId::new(), items: vec![], ..Default::default() };
assert!(order.validate().is_err());
let mut bad_item = valid_create_order_item();
bad_item.quantity = -1;
let order = CreateOrder {
customer_id: CustomerId::new(),
items: vec![bad_item],
..Default::default()
};
assert!(order.validate().is_err());
}
}