use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use stateset_primitives::{CustomerId, OrderId, ProductId, ReturnId};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
#[derive(Debug, Clone, Deserialize, Serialize, Default, IntoParams)]
pub struct PaginationParams {
pub limit: Option<u32>,
pub offset: Option<u32>,
}
impl PaginationParams {
pub const DEFAULT_LIMIT: u32 = 50;
pub const MAX_LIMIT: u32 = 200;
#[must_use]
pub fn resolved_limit(&self) -> u32 {
self.limit.unwrap_or(Self::DEFAULT_LIMIT).min(Self::MAX_LIMIT)
}
#[must_use]
pub fn resolved_offset(&self) -> u32 {
self.offset.unwrap_or(0)
}
}
#[must_use]
pub const fn overfetch_limit(limit: u32) -> u32 {
limit.saturating_add(1)
}
pub fn finalize_page<T>(items: &mut Vec<T>, requested_limit: u32) -> bool {
let requested_limit = requested_limit as usize;
let has_more = items.len() > requested_limit;
if has_more {
items.truncate(requested_limit);
}
has_more
}
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
#[must_use]
pub fn encode_cursor(sort_key: &str, id: &str) -> String {
let payload = format!("{}\x00{}", sort_key, id);
URL_SAFE_NO_PAD.encode(payload.as_bytes())
}
pub fn decode_cursor(cursor: &str) -> Option<(String, String)> {
let bytes = URL_SAFE_NO_PAD.decode(cursor).ok()?;
let s = String::from_utf8(bytes).ok()?;
let (sort_key, id) = s.split_once('\x00')?;
Some((sort_key.to_string(), id.to_string()))
}
#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
pub struct OrderFilterParams {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub after: Option<String>,
pub customer_id: Option<String>,
pub status: Option<String>,
pub payment_status: Option<String>,
pub fulfillment_status: Option<String>,
pub from_date: Option<String>,
pub to_date: Option<String>,
}
impl OrderFilterParams {
#[must_use]
pub fn resolved_limit(&self) -> u32 {
self.limit.unwrap_or(PaginationParams::DEFAULT_LIMIT).min(PaginationParams::MAX_LIMIT)
}
#[must_use]
pub fn resolved_offset(&self) -> u32 {
self.offset.unwrap_or(0)
}
}
#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
pub struct CustomerFilterParams {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub after: Option<String>,
pub email: Option<String>,
pub status: Option<String>,
pub tag: Option<String>,
pub accepts_marketing: Option<bool>,
}
impl CustomerFilterParams {
#[must_use]
pub fn resolved_limit(&self) -> u32 {
self.limit.unwrap_or(PaginationParams::DEFAULT_LIMIT).min(PaginationParams::MAX_LIMIT)
}
#[must_use]
pub fn resolved_offset(&self) -> u32 {
self.offset.unwrap_or(0)
}
}
#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
pub struct ProductFilterParams {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub after: Option<String>,
pub status: Option<String>,
pub product_type: Option<String>,
pub search: Option<String>,
pub category: Option<String>,
pub min_price: Option<String>,
pub max_price: Option<String>,
pub in_stock: Option<bool>,
}
impl ProductFilterParams {
#[must_use]
pub fn resolved_limit(&self) -> u32 {
self.limit.unwrap_or(PaginationParams::DEFAULT_LIMIT).min(PaginationParams::MAX_LIMIT)
}
#[must_use]
pub fn resolved_offset(&self) -> u32 {
self.offset.unwrap_or(0)
}
}
#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
pub struct ReturnFilterParams {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub after: Option<String>,
pub order_id: Option<String>,
pub customer_id: Option<String>,
pub status: Option<String>,
pub reason: Option<String>,
pub from_date: Option<String>,
pub to_date: Option<String>,
}
impl ReturnFilterParams {
#[must_use]
pub fn resolved_limit(&self) -> u32 {
self.limit.unwrap_or(PaginationParams::DEFAULT_LIMIT).min(PaginationParams::MAX_LIMIT)
}
#[must_use]
pub fn resolved_offset(&self) -> u32 {
self.offset.unwrap_or(0)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct CreateOrderRequest {
#[schema(value_type = String, format = "uuid")]
pub customer_id: CustomerId,
pub items: Vec<CreateOrderItemRequest>,
pub currency: Option<String>,
pub shipping_address: Option<AddressDto>,
pub billing_address: Option<AddressDto>,
pub notes: Option<String>,
pub payment_method: Option<String>,
pub shipping_method: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct CreateOrderItemRequest {
#[schema(value_type = String, format = "uuid")]
pub product_id: ProductId,
pub variant_id: Option<Uuid>,
pub sku: String,
pub name: String,
pub quantity: i32,
#[schema(value_type = String)]
pub unit_price: Decimal,
#[schema(value_type = Option<String>)]
pub discount: Option<Decimal>,
#[schema(value_type = Option<String>)]
pub tax_amount: Option<Decimal>,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct AddressDto {
pub line1: String,
pub line2: Option<String>,
pub city: String,
pub state: Option<String>,
pub postal_code: String,
pub country: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct OrderResponse {
#[schema(value_type = String, format = "uuid")]
pub id: OrderId,
pub order_number: String,
#[schema(value_type = String, format = "uuid")]
pub customer_id: CustomerId,
pub status: String,
#[schema(value_type = String)]
pub total_amount: Decimal,
pub currency: String,
pub payment_status: String,
pub fulfillment_status: String,
pub items: Vec<OrderItemResponse>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct OrderItemResponse {
pub id: Uuid,
#[schema(value_type = String, format = "uuid")]
pub product_id: ProductId,
pub sku: String,
pub name: String,
pub quantity: i32,
#[schema(value_type = String)]
pub unit_price: Decimal,
#[schema(value_type = String)]
pub total: Decimal,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct OrderListResponse {
pub orders: Vec<OrderResponse>,
pub total: usize,
pub limit: u32,
pub offset: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
pub has_more: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct CreateCustomerRequest {
pub email: String,
pub first_name: String,
pub last_name: String,
pub phone: Option<String>,
pub accepts_marketing: Option<bool>,
pub tags: Option<Vec<String>>,
#[schema(value_type = Option<Object>)]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CustomerResponse {
#[schema(value_type = String, format = "uuid")]
pub id: CustomerId,
pub email: String,
pub first_name: String,
pub last_name: String,
pub phone: Option<String>,
pub status: String,
pub accepts_marketing: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct CustomerListResponse {
pub customers: Vec<CustomerResponse>,
pub total: usize,
pub limit: u32,
pub offset: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
pub has_more: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct CreateProductRequest {
pub name: String,
pub slug: Option<String>,
pub description: Option<String>,
pub product_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ProductResponse {
#[schema(value_type = String, format = "uuid")]
pub id: ProductId,
pub name: String,
pub slug: String,
pub description: String,
pub status: String,
pub product_type: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ProductListResponse {
pub products: Vec<ProductResponse>,
pub total: usize,
pub limit: u32,
pub offset: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
pub has_more: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct InventoryAdjustRequest {
#[schema(value_type = String)]
pub quantity: Decimal,
pub reason: String,
pub location_id: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct InventoryResponse {
pub sku: String,
pub name: String,
#[schema(value_type = String)]
pub total_on_hand: Decimal,
#[schema(value_type = String)]
pub total_allocated: Decimal,
#[schema(value_type = String)]
pub total_available: Decimal,
}
#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
pub struct InventoryFilterParams {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub sku: Option<String>,
pub below_reorder_point: Option<bool>,
pub is_active: Option<bool>,
}
impl InventoryFilterParams {
#[must_use]
pub fn resolved_limit(&self) -> u32 {
self.limit.unwrap_or(PaginationParams::DEFAULT_LIMIT).min(PaginationParams::MAX_LIMIT)
}
#[must_use]
pub fn resolved_offset(&self) -> u32 {
self.offset.unwrap_or(0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct InventoryItemResponse {
pub id: i64,
pub sku: String,
pub name: String,
pub description: Option<String>,
pub unit_of_measure: String,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct InventoryListResponse {
pub items: Vec<InventoryItemResponse>,
pub total: usize,
pub limit: u32,
pub offset: u32,
pub has_more: bool,
}
#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
pub struct ShipmentFilterParams {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub order_id: Option<String>,
pub status: Option<String>,
pub carrier: Option<String>,
pub tracking_number: Option<String>,
}
impl ShipmentFilterParams {
#[must_use]
pub fn resolved_limit(&self) -> u32 {
self.limit.unwrap_or(PaginationParams::DEFAULT_LIMIT).min(PaginationParams::MAX_LIMIT)
}
#[must_use]
pub fn resolved_offset(&self) -> u32 {
self.offset.unwrap_or(0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ShipmentResponse {
#[schema(value_type = String, format = "uuid")]
pub id: stateset_primitives::ShipmentId,
pub shipment_number: String,
#[schema(value_type = String, format = "uuid")]
pub order_id: OrderId,
pub status: String,
pub carrier: String,
pub shipping_method: String,
pub tracking_number: Option<String>,
pub tracking_url: Option<String>,
pub recipient_name: String,
#[schema(value_type = Option<String>)]
pub shipping_cost: Option<Decimal>,
pub shipped_at: Option<DateTime<Utc>>,
pub estimated_delivery: Option<DateTime<Utc>>,
pub delivered_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ShipmentListResponse {
pub shipments: Vec<ShipmentResponse>,
pub total: usize,
pub limit: u32,
pub offset: u32,
pub has_more: bool,
}
#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
pub struct PaymentFilterParams {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub order_id: Option<String>,
pub customer_id: Option<String>,
pub status: Option<String>,
pub payment_method: Option<String>,
pub processor: Option<String>,
pub min_amount: Option<String>,
pub max_amount: Option<String>,
pub from_date: Option<String>,
pub to_date: Option<String>,
}
impl PaymentFilterParams {
#[must_use]
pub fn resolved_limit(&self) -> u32 {
self.limit.unwrap_or(PaginationParams::DEFAULT_LIMIT).min(PaginationParams::MAX_LIMIT)
}
#[must_use]
pub fn resolved_offset(&self) -> u32 {
self.offset.unwrap_or(0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PaymentResponse {
#[schema(value_type = String, format = "uuid")]
pub id: stateset_primitives::PaymentId,
pub payment_number: String,
#[schema(value_type = Option<String>, format = "uuid")]
pub order_id: Option<OrderId>,
pub customer_id: Option<String>,
pub status: String,
pub payment_method: String,
#[schema(value_type = String)]
pub amount: Decimal,
pub currency: String,
#[schema(value_type = String)]
pub amount_refunded: Decimal,
pub external_id: Option<String>,
pub processor: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct PaymentListResponse {
pub payments: Vec<PaymentResponse>,
pub total: usize,
pub limit: u32,
pub offset: u32,
pub has_more: bool,
}
#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
pub struct InvoiceFilterParams {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub customer_id: Option<String>,
pub order_id: Option<String>,
pub status: Option<String>,
pub invoice_type: Option<String>,
pub overdue_only: Option<bool>,
pub from_date: Option<String>,
pub to_date: Option<String>,
}
impl InvoiceFilterParams {
#[must_use]
pub fn resolved_limit(&self) -> u32 {
self.limit.unwrap_or(PaginationParams::DEFAULT_LIMIT).min(PaginationParams::MAX_LIMIT)
}
#[must_use]
pub fn resolved_offset(&self) -> u32 {
self.offset.unwrap_or(0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct InvoiceResponse {
#[schema(value_type = String, format = "uuid")]
pub id: stateset_primitives::InvoiceId,
pub invoice_number: String,
#[schema(value_type = String, format = "uuid")]
pub customer_id: CustomerId,
#[schema(value_type = Option<String>, format = "uuid")]
pub order_id: Option<OrderId>,
pub status: String,
pub invoice_type: String,
pub invoice_date: DateTime<Utc>,
pub due_date: DateTime<Utc>,
pub currency: String,
#[schema(value_type = String)]
pub subtotal: Decimal,
#[schema(value_type = String)]
pub tax_amount: Decimal,
#[schema(value_type = String)]
pub total: Decimal,
#[schema(value_type = String)]
pub amount_paid: Decimal,
#[schema(value_type = String)]
pub balance_due: Decimal,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct InvoiceListResponse {
pub invoices: Vec<InvoiceResponse>,
pub total: usize,
pub limit: u32,
pub offset: u32,
pub has_more: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct CreateReturnRequest {
#[schema(value_type = String, format = "uuid")]
pub order_id: OrderId,
pub reason: String,
pub reason_details: Option<String>,
pub items: Vec<CreateReturnItemRequest>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct CreateReturnItemRequest {
pub order_item_id: Uuid,
pub quantity: i32,
pub condition: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ReturnResponse {
#[schema(value_type = String, format = "uuid")]
pub id: ReturnId,
#[schema(value_type = String, format = "uuid")]
pub order_id: OrderId,
#[schema(value_type = String, format = "uuid")]
pub customer_id: CustomerId,
pub status: String,
pub reason: String,
#[schema(value_type = Option<String>)]
pub refund_amount: Option<Decimal>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ReturnListResponse {
pub returns: Vec<ReturnResponse>,
pub total: usize,
pub limit: u32,
pub offset: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
pub has_more: bool,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct TenantCacheResponse {
pub enabled: bool,
pub max_cached_dbs: usize,
pub cached_dbs: usize,
pub in_use_cached_dbs: usize,
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub rejections: u64,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct HealthResponse {
pub status: &'static str,
pub tenant_cache: TenantCacheResponse,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ReadyResponse {
pub status: &'static str,
pub database: &'static str,
pub tenant_cache: TenantCacheResponse,
}
#[derive(Debug, Clone, Deserialize, Default, IntoParams)]
pub struct EventStreamParams {
pub filter: Option<String>,
}
impl From<stateset_core::Order> for OrderResponse {
fn from(o: stateset_core::Order) -> Self {
Self {
id: o.id,
order_number: o.order_number,
customer_id: o.customer_id,
status: o.status.to_string(),
total_amount: o.total_amount,
currency: o.currency.to_string(),
payment_status: o.payment_status.to_string(),
fulfillment_status: o.fulfillment_status.to_string(),
items: o.items.into_iter().map(OrderItemResponse::from).collect(),
created_at: o.created_at,
updated_at: o.updated_at,
}
}
}
impl From<stateset_core::OrderItem> for OrderItemResponse {
fn from(i: stateset_core::OrderItem) -> Self {
Self {
id: *i.id.as_uuid(),
product_id: i.product_id,
sku: i.sku,
name: i.name,
quantity: i.quantity,
unit_price: i.unit_price,
total: i.total,
}
}
}
impl From<stateset_core::Customer> for CustomerResponse {
fn from(c: stateset_core::Customer) -> Self {
Self {
id: c.id,
email: c.email,
first_name: c.first_name,
last_name: c.last_name,
phone: c.phone,
status: c.status.to_string(),
accepts_marketing: c.accepts_marketing,
created_at: c.created_at,
updated_at: c.updated_at,
}
}
}
impl From<stateset_core::Product> for ProductResponse {
fn from(p: stateset_core::Product) -> Self {
Self {
id: p.id,
name: p.name,
slug: p.slug,
description: p.description,
status: p.status.to_string(),
product_type: p.product_type.to_string(),
created_at: p.created_at,
updated_at: p.updated_at,
}
}
}
impl From<stateset_core::StockLevel> for InventoryResponse {
fn from(s: stateset_core::StockLevel) -> Self {
Self {
sku: s.sku,
name: s.name,
total_on_hand: s.total_on_hand,
total_allocated: s.total_allocated,
total_available: s.total_available,
}
}
}
impl From<stateset_core::InventoryItem> for InventoryItemResponse {
fn from(i: stateset_core::InventoryItem) -> Self {
Self {
id: i.id,
sku: i.sku,
name: i.name,
description: i.description,
unit_of_measure: i.unit_of_measure,
is_active: i.is_active,
created_at: i.created_at,
updated_at: i.updated_at,
}
}
}
impl From<stateset_core::Shipment> for ShipmentResponse {
fn from(s: stateset_core::Shipment) -> Self {
Self {
id: s.id,
shipment_number: s.shipment_number,
order_id: s.order_id,
status: s.status.to_string(),
carrier: s.carrier.to_string(),
shipping_method: s.shipping_method.to_string(),
tracking_number: s.tracking_number,
tracking_url: s.tracking_url,
recipient_name: s.recipient_name,
shipping_cost: s.shipping_cost,
shipped_at: s.shipped_at,
estimated_delivery: s.estimated_delivery,
delivered_at: s.delivered_at,
created_at: s.created_at,
updated_at: s.updated_at,
}
}
}
impl From<stateset_core::Payment> for PaymentResponse {
fn from(p: stateset_core::Payment) -> Self {
Self {
id: p.id,
payment_number: p.payment_number,
order_id: p.order_id,
customer_id: p.customer_id.map(|c| c.to_string()),
status: p.status.to_string(),
payment_method: p.payment_method.to_string(),
amount: p.amount,
currency: p.currency.to_string(),
amount_refunded: p.amount_refunded,
external_id: p.external_id,
processor: p.processor,
created_at: p.created_at,
updated_at: p.updated_at,
}
}
}
impl From<stateset_core::Invoice> for InvoiceResponse {
fn from(i: stateset_core::Invoice) -> Self {
Self {
id: i.id,
invoice_number: i.invoice_number,
customer_id: i.customer_id,
order_id: i.order_id,
status: i.status.to_string(),
invoice_type: i.invoice_type.to_string(),
invoice_date: i.invoice_date,
due_date: i.due_date,
currency: i.currency.to_string(),
subtotal: i.subtotal,
tax_amount: i.tax_amount,
total: i.total,
amount_paid: i.amount_paid,
balance_due: i.balance_due,
created_at: i.created_at,
updated_at: i.updated_at,
}
}
}
impl From<stateset_core::Return> for ReturnResponse {
fn from(r: stateset_core::Return) -> Self {
Self {
id: r.id,
order_id: r.order_id,
customer_id: r.customer_id,
status: r.status.to_string(),
reason: r.reason.to_string(),
refund_amount: r.refund_amount,
created_at: r.created_at,
updated_at: r.updated_at,
}
}
}
impl From<AddressDto> for stateset_core::Address {
fn from(a: AddressDto) -> Self {
Self {
line1: a.line1,
line2: a.line2,
city: a.city,
state: a.state,
postal_code: a.postal_code,
country: a.country,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn pagination_default_limit() {
let p = PaginationParams::default();
assert_eq!(p.resolved_limit(), PaginationParams::DEFAULT_LIMIT);
assert_eq!(p.resolved_offset(), 0);
}
#[test]
fn pagination_custom_limit() {
let p = PaginationParams { limit: Some(10), offset: Some(20) };
assert_eq!(p.resolved_limit(), 10);
assert_eq!(p.resolved_offset(), 20);
}
#[test]
fn pagination_clamps_to_max() {
let p = PaginationParams { limit: Some(999), offset: None };
assert_eq!(p.resolved_limit(), PaginationParams::MAX_LIMIT);
}
#[test]
fn create_order_request_roundtrip() {
let req = CreateOrderRequest {
customer_id: CustomerId::new(),
items: vec![CreateOrderItemRequest {
product_id: ProductId::new(),
variant_id: None,
sku: "SKU-1".into(),
name: "Widget".into(),
quantity: 2,
unit_price: dec!(29.99),
discount: None,
tax_amount: None,
}],
currency: Some("USD".into()),
shipping_address: Some(AddressDto {
line1: "123 Main".into(),
line2: None,
city: "NYC".into(),
state: Some("NY".into()),
postal_code: "10001".into(),
country: "US".into(),
}),
billing_address: None,
notes: None,
payment_method: None,
shipping_method: None,
};
let json = serde_json::to_string(&req).unwrap();
let deser: CreateOrderRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deser.items.len(), 1);
assert_eq!(deser.items[0].sku, "SKU-1");
}
#[test]
fn create_customer_request_roundtrip() {
let req = CreateCustomerRequest {
email: "test@example.com".into(),
first_name: "John".into(),
last_name: "Doe".into(),
phone: None,
accepts_marketing: Some(true),
tags: None,
metadata: None,
};
let json = serde_json::to_string(&req).unwrap();
let deser: CreateCustomerRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deser.email, "test@example.com");
}
#[test]
fn create_product_request_roundtrip() {
let req = CreateProductRequest {
name: "Widget".into(),
slug: Some("widget".into()),
description: Some("A fine widget".into()),
product_type: None,
};
let json = serde_json::to_string(&req).unwrap();
let deser: CreateProductRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deser.name, "Widget");
}
#[test]
fn inventory_adjust_request_roundtrip() {
let req = InventoryAdjustRequest {
quantity: dec!(-5),
reason: "Damaged".into(),
location_id: Some(1),
};
let json = serde_json::to_string(&req).unwrap();
let deser: InventoryAdjustRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deser.quantity, dec!(-5));
}
#[test]
fn create_return_request_roundtrip() {
let req = CreateReturnRequest {
order_id: OrderId::new(),
reason: "defective".into(),
reason_details: None,
items: vec![CreateReturnItemRequest {
order_item_id: Uuid::new_v4(),
quantity: 1,
condition: Some("damaged".into()),
}],
notes: None,
};
let json = serde_json::to_string(&req).unwrap();
let deser: CreateReturnRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deser.items.len(), 1);
}
#[test]
fn health_response_serialization() {
let resp = HealthResponse {
status: "ok",
tenant_cache: TenantCacheResponse {
enabled: true,
max_cached_dbs: 256,
cached_dbs: 3,
in_use_cached_dbs: 1,
hits: 20,
misses: 4,
evictions: 2,
rejections: 1,
},
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["status"], "ok");
assert_eq!(json["tenant_cache"]["cached_dbs"], 3);
}
#[test]
fn ready_response_serialization() {
let resp = ReadyResponse {
status: "ok",
database: "connected",
tenant_cache: TenantCacheResponse {
enabled: false,
max_cached_dbs: 256,
cached_dbs: 0,
in_use_cached_dbs: 0,
hits: 0,
misses: 0,
evictions: 0,
rejections: 0,
},
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["database"], "connected");
assert_eq!(json["tenant_cache"]["enabled"], false);
}
#[test]
fn order_response_serialization() {
let resp = OrderResponse {
id: OrderId::new(),
order_number: "ORD-001".into(),
customer_id: CustomerId::new(),
status: "pending".into(),
total_amount: dec!(59.98),
currency: "USD".into(),
payment_status: "pending".into(),
fulfillment_status: "unfulfilled".into(),
items: vec![],
created_at: Utc::now(),
updated_at: Utc::now(),
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["status"], "pending");
}
#[test]
fn customer_response_serialization() {
let resp = CustomerResponse {
id: CustomerId::new(),
email: "a@b.com".into(),
first_name: "A".into(),
last_name: "B".into(),
phone: None,
status: "active".into(),
accepts_marketing: false,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["email"], "a@b.com");
}
#[test]
fn product_response_serialization() {
let resp = ProductResponse {
id: ProductId::new(),
name: "W".into(),
slug: "w".into(),
description: "d".into(),
status: "draft".into(),
product_type: "simple".into(),
created_at: Utc::now(),
updated_at: Utc::now(),
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["name"], "W");
}
#[test]
fn inventory_response_serialization() {
let resp = InventoryResponse {
sku: "SKU-1".into(),
name: "Widget".into(),
total_on_hand: dec!(100),
total_allocated: dec!(10),
total_available: dec!(90),
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["sku"], "SKU-1");
}
#[test]
fn return_response_serialization() {
let resp = ReturnResponse {
id: ReturnId::new(),
order_id: OrderId::new(),
customer_id: CustomerId::new(),
status: "requested".into(),
reason: "defective".into(),
refund_amount: Some(dec!(29.99)),
created_at: Utc::now(),
updated_at: Utc::now(),
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["status"], "requested");
}
#[test]
fn address_dto_converts_to_core() {
let dto = AddressDto {
line1: "123 Main".into(),
line2: None,
city: "NYC".into(),
state: Some("NY".into()),
postal_code: "10001".into(),
country: "US".into(),
};
let core: stateset_core::Address = dto.into();
assert_eq!(core.city, "NYC");
}
#[test]
fn event_stream_params_default() {
let p = EventStreamParams::default();
assert!(p.filter.is_none());
}
#[test]
fn order_list_response_serialization() {
let resp = OrderListResponse {
orders: vec![],
total: 0,
limit: 50,
offset: 0,
next_cursor: None,
has_more: false,
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["total"], 0);
assert_eq!(json["has_more"], false);
assert!(json.get("next_cursor").is_none()); }
#[test]
fn customer_list_response_serialization() {
let resp = CustomerListResponse {
customers: vec![],
total: 0,
limit: 50,
offset: 0,
next_cursor: None,
has_more: false,
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["total"], 0);
assert_eq!(json["has_more"], false);
}
#[test]
fn product_list_response_serialization() {
let resp = ProductListResponse {
products: vec![],
total: 0,
limit: 50,
offset: 0,
next_cursor: None,
has_more: false,
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["total"], 0);
assert_eq!(json["has_more"], false);
}
#[test]
fn return_list_response_serialization() {
let resp = ReturnListResponse {
returns: vec![],
total: 0,
limit: 50,
offset: 0,
next_cursor: None,
has_more: false,
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["total"], 0);
assert_eq!(json["has_more"], false);
}
#[test]
fn cursor_encode_decode_roundtrip() {
let cursor = encode_cursor("2024-01-15T10:30:00Z", "550e8400-e29b-41d4-a716-446655440000");
let (sort_key, id) = decode_cursor(&cursor).unwrap();
assert_eq!(sort_key, "2024-01-15T10:30:00Z");
assert_eq!(id, "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn cursor_decode_invalid_base64() {
assert!(decode_cursor("not-valid-base64!!!").is_none());
}
#[test]
fn cursor_decode_missing_separator() {
let encoded = URL_SAFE_NO_PAD.encode(b"no-separator-here");
assert!(decode_cursor(&encoded).is_none());
}
#[test]
fn overfetch_limit_adds_one_row() {
assert_eq!(overfetch_limit(10), 11);
assert_eq!(overfetch_limit(PaginationParams::MAX_LIMIT), PaginationParams::MAX_LIMIT + 1);
}
#[test]
fn finalize_page_marks_exact_boundary_as_not_has_more() {
let mut items = vec![1, 2];
let has_more = finalize_page(&mut items, 2);
assert!(!has_more);
assert_eq!(items, vec![1, 2]);
}
#[test]
fn finalize_page_trims_overfetch_and_sets_has_more() {
let mut items = vec![1, 2, 3];
let has_more = finalize_page(&mut items, 2);
assert!(has_more);
assert_eq!(items, vec![1, 2]);
}
#[test]
fn cursor_next_cursor_serialized_when_present() {
let resp = OrderListResponse {
orders: vec![],
total: 100,
limit: 10,
offset: 0,
next_cursor: Some("abc123".into()),
has_more: true,
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["next_cursor"], "abc123");
assert_eq!(json["has_more"], true);
}
#[test]
fn order_filter_params_default() {
let p = OrderFilterParams::default();
assert_eq!(p.resolved_limit(), PaginationParams::DEFAULT_LIMIT);
assert_eq!(p.resolved_offset(), 0);
assert!(p.customer_id.is_none());
assert!(p.status.is_none());
}
#[test]
fn order_filter_params_deserialization() {
let p: OrderFilterParams = serde_json::from_value(serde_json::json!({
"limit": 10, "offset": 5, "status": "pending", "customer_id": "abc"
}))
.unwrap();
assert_eq!(p.resolved_limit(), 10);
assert_eq!(p.resolved_offset(), 5);
assert_eq!(p.status.as_deref(), Some("pending"));
assert_eq!(p.customer_id.as_deref(), Some("abc"));
}
#[test]
fn customer_filter_params_deserialization() {
let p: CustomerFilterParams = serde_json::from_value(serde_json::json!({
"email": "test@example.com", "accepts_marketing": true
}))
.unwrap();
assert_eq!(p.email.as_deref(), Some("test@example.com"));
assert_eq!(p.accepts_marketing, Some(true));
}
#[test]
fn product_filter_params_deserialization() {
let p: ProductFilterParams = serde_json::from_value(serde_json::json!({
"search": "widget", "min_price": "10.00", "max_price": "100", "in_stock": true
}))
.unwrap();
assert_eq!(p.search.as_deref(), Some("widget"));
assert_eq!(p.min_price.as_deref(), Some("10.00"));
assert_eq!(p.max_price.as_deref(), Some("100"));
assert_eq!(p.in_stock, Some(true));
}
#[test]
fn return_filter_params_deserialization() {
let p: ReturnFilterParams = serde_json::from_value(serde_json::json!({
"status": "requested", "reason": "defective", "limit": 5
}))
.unwrap();
assert_eq!(p.status.as_deref(), Some("requested"));
assert_eq!(p.reason.as_deref(), Some("defective"));
assert_eq!(p.resolved_limit(), 5);
}
#[test]
fn filter_params_limit_clamps_to_max() {
let p = OrderFilterParams { limit: Some(999), ..Default::default() };
assert_eq!(p.resolved_limit(), PaginationParams::MAX_LIMIT);
}
}