mod customer;
mod inventory;
mod order;
mod payment;
mod product;
mod returns;
mod shipping;
pub mod transition;
pub use customer::CustomerError;
pub use inventory::InventoryError;
pub use order::OrderError;
pub use payment::PaymentError;
pub use product::ProductError;
pub use returns::ReturnError;
pub use shipping::ShippingError;
pub use transition::{GotExpected, StateTransitionError};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
#[cfg(target_pointer_width = "64")]
macro_rules! static_assert_size {
($ty:ty, $size:expr) => {
const _: [(); $size] = [(); ::std::mem::size_of::<$ty>()];
};
}
#[cfg(target_pointer_width = "64")]
mod _size_assertions {
use super::{
CommerceError, CustomerError, DbError, InventoryError, OrderError, PaymentError,
ProductError, ReturnError, ShippingError,
};
static_assert_size!(CommerceError, 80);
static_assert_size!(DbError, 64);
static_assert_size!(OrderError, 48);
static_assert_size!(InventoryError, 72);
static_assert_size!(CustomerError, 24);
static_assert_size!(ProductError, 24);
static_assert_size!(ReturnError, 48);
static_assert_size!(PaymentError, 56);
static_assert_size!(ShippingError, 48);
}
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum DbError {
#[error("Connection failed to {url}: {message}")]
ConnectionFailed {
url: String,
message: String,
},
#[error("Query failed on {table}: {message}")]
QueryFailed {
table: &'static str,
operation: &'static str,
message: String,
},
#[error("Constraint violation on {table}: {constraint} - {message}")]
ConstraintViolation {
table: &'static str,
constraint: String,
message: String,
},
#[error("Migration {version} failed: {message}")]
MigrationFailed {
version: i32,
message: String,
},
#[error("Transaction failed: {message}")]
TransactionFailed {
message: String,
},
#[error("Connection pool exhausted after {timeout_ms}ms")]
PoolExhausted {
timeout_ms: u64,
},
#[error("Serialization error for {field}: {message}")]
SerializationError {
field: String,
message: String,
},
#[error("Database error: {0}")]
Other(String),
}
impl DbError {
#[track_caller]
pub fn query_failed(
table: &'static str,
operation: &'static str,
message: impl Into<String>,
) -> Self {
Self::QueryFailed { table, operation, message: message.into() }
}
#[track_caller]
pub fn constraint_violation(
table: &'static str,
constraint: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::ConstraintViolation { table, constraint: constraint.into(), message: message.into() }
}
#[track_caller]
pub fn connection_failed(url: impl Into<String>, message: impl Into<String>) -> Self {
Self::ConnectionFailed { url: url.into(), message: message.into() }
}
#[track_caller]
pub fn transaction_failed(message: impl Into<String>) -> Self {
Self::TransactionFailed { message: message.into() }
}
#[track_caller]
pub fn serialization_error(field: impl Into<String>, message: impl Into<String>) -> Self {
Self::SerializationError { field: field.into(), message: message.into() }
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum CommerceError {
#[error("Order not found: {0}")]
OrderNotFound(Uuid),
#[error("Order cannot be cancelled in status: {0}")]
OrderCannotBeCancelled(String),
#[error("Order cannot be refunded: {0}")]
OrderCannotBeRefunded(String),
#[error("Invalid order status transition from {from} to {to}")]
InvalidOrderStatusTransition {
from: String,
to: String,
},
#[error("Inventory item not found: {0}")]
InventoryItemNotFound(String),
#[error("Insufficient stock for SKU {sku}: requested {requested}, available {available}")]
InsufficientStock {
sku: String,
requested: String,
available: String,
},
#[error("Inventory reservation not found: {0}")]
ReservationNotFound(Uuid),
#[error("Inventory reservation expired: {0}")]
ReservationExpired(Uuid),
#[error("Duplicate SKU: {0}")]
DuplicateSku(String),
#[error("Customer not found: {0}")]
CustomerNotFound(Uuid),
#[error("Email already exists: {0}")]
EmailAlreadyExists(String),
#[error("Customer is not active")]
CustomerNotActive,
#[error("Product not found: {0}")]
ProductNotFound(Uuid),
#[error("Product variant not found: {0}")]
ProductVariantNotFound(Uuid),
#[error("Duplicate product slug: {0}")]
DuplicateSlug(String),
#[error("Product is not purchasable")]
ProductNotPurchasable,
#[error("Return not found: {0}")]
ReturnNotFound(Uuid),
#[error("Return cannot be approved in status: {0}")]
ReturnCannotBeApproved(String),
#[error("Return period expired")]
ReturnPeriodExpired,
#[error("Item not eligible for return")]
ItemNotEligibleForReturn,
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Invalid input: {field} - {message}")]
InvalidInput {
field: String,
message: String,
},
#[error("Database error: {0}")]
DatabaseError(String),
#[error(transparent)]
Database(#[from] DbError),
#[error("Record not found")]
NotFound,
#[error("Conflict: {0}")]
Conflict(String),
#[error("Optimistic lock failure: record was modified")]
OptimisticLockFailure,
#[error("Version conflict on {entity} {id}: expected version {expected_version}")]
VersionConflict {
entity: String,
id: String,
expected_version: i32,
},
#[error("External service error: {0}")]
ExternalServiceError(String),
#[error(transparent)]
Order(#[from] OrderError),
#[error(transparent)]
Inventory(#[from] InventoryError),
#[error(transparent)]
Customer(#[from] CustomerError),
#[error(transparent)]
Product(#[from] ProductError),
#[error(transparent)]
Return(#[from] ReturnError),
#[error(transparent)]
Payment(#[from] PaymentError),
#[error(transparent)]
Shipping(#[from] ShippingError),
#[error("Internal error: {0}")]
Internal(String),
#[error("Operation not permitted: {0}")]
NotPermitted(String),
}
pub type Result<T> = std::result::Result<T, CommerceError>;
pub mod result {
pub type OrderResult<T> = std::result::Result<T, super::OrderError>;
pub type InventoryResult<T> = std::result::Result<T, super::InventoryError>;
pub type CustomerResult<T> = std::result::Result<T, super::CustomerError>;
pub type ProductResult<T> = std::result::Result<T, super::ProductError>;
pub type ReturnResult<T> = std::result::Result<T, super::ReturnError>;
pub type PaymentResult<T> = std::result::Result<T, super::PaymentError>;
pub type ShippingResult<T> = std::result::Result<T, super::ShippingError>;
pub type DbResult<T> = std::result::Result<T, super::DbError>;
}
impl CommerceError {
#[must_use]
pub const fn is_not_found(&self) -> bool {
match self {
Self::NotFound
| Self::OrderNotFound(_)
| Self::CustomerNotFound(_)
| Self::ProductNotFound(_)
| Self::ProductVariantNotFound(_)
| Self::ReturnNotFound(_)
| Self::InventoryItemNotFound(_)
| Self::ReservationNotFound(_) => true,
Self::Order(OrderError::NotFound(_)) => true,
Self::Inventory(
InventoryError::ItemNotFound(_) | InventoryError::ReservationNotFound(_),
) => true,
Self::Customer(CustomerError::NotFound(_)) => true,
Self::Product(ProductError::NotFound(_) | ProductError::VariantNotFound(_)) => true,
Self::Return(ReturnError::NotFound(_)) => true,
Self::Payment(PaymentError::NotFound(_)) => true,
Self::Shipping(ShippingError::NotFound(_)) => true,
_ => false,
}
}
#[must_use]
pub const fn is_validation(&self) -> bool {
matches!(self, Self::ValidationError(_) | Self::InvalidInput { .. })
}
#[must_use]
pub const fn is_conflict(&self) -> bool {
match self {
Self::Conflict(_)
| Self::OptimisticLockFailure
| Self::VersionConflict { .. }
| Self::DuplicateSku(_)
| Self::DuplicateSlug(_)
| Self::EmailAlreadyExists(_) => true,
Self::Inventory(InventoryError::DuplicateSku(_)) => true,
Self::Customer(CustomerError::EmailAlreadyExists(_)) => true,
Self::Product(ProductError::DuplicateSlug(_)) => true,
_ => false,
}
}
#[must_use]
pub const fn is_database(&self) -> bool {
matches!(self, Self::DatabaseError(_) | Self::Database(_))
}
#[must_use]
pub const fn is_external_service(&self) -> bool {
matches!(self, Self::ExternalServiceError(_))
}
#[must_use]
pub const fn is_retryable(&self) -> bool {
match self {
Self::OptimisticLockFailure => true,
Self::Database(db_err) => matches!(
db_err,
DbError::ConnectionFailed { .. }
| DbError::PoolExhausted { .. }
| DbError::TransactionFailed { .. }
),
_ => false,
}
}
#[must_use]
pub const fn is_transient(&self) -> bool {
self.is_retryable() || self.is_external_service()
}
#[must_use]
pub const fn is_client_error(&self) -> bool {
self.is_not_found() || self.is_validation() || self.is_conflict() || self.is_not_permitted()
}
#[must_use]
pub const fn is_server_error(&self) -> bool {
self.is_database() || matches!(self, Self::Internal(_)) || self.is_external_service()
}
#[must_use]
pub const fn is_not_permitted(&self) -> bool {
matches!(self, Self::NotPermitted(_))
}
#[must_use]
pub const fn suggested_status_code(&self) -> u16 {
if self.is_not_found() {
404
} else if self.is_validation() {
400
} else if self.is_conflict() {
409
} else if self.is_not_permitted() {
403
} else if self.is_external_service() {
502
} else {
500
}
}
#[must_use]
pub const fn as_db_error(&self) -> Option<&DbError> {
match self {
Self::Database(e) => Some(e),
_ => None,
}
}
#[must_use]
pub const fn as_order_error(&self) -> Option<&OrderError> {
match self {
Self::Order(e) => Some(e),
_ => None,
}
}
#[must_use]
pub const fn as_inventory_error(&self) -> Option<&InventoryError> {
match self {
Self::Inventory(e) => Some(e),
_ => None,
}
}
#[must_use]
pub const fn as_customer_error(&self) -> Option<&CustomerError> {
match self {
Self::Customer(e) => Some(e),
_ => None,
}
}
#[must_use]
pub const fn as_product_error(&self) -> Option<&ProductError> {
match self {
Self::Product(e) => Some(e),
_ => None,
}
}
#[track_caller]
#[must_use]
pub const fn db(error: DbError) -> Self {
Self::Database(error)
}
#[track_caller]
pub fn query_failed(
table: &'static str,
operation: &'static str,
message: impl Into<String>,
) -> Self {
Self::Database(DbError::query_failed(table, operation, message))
}
#[track_caller]
pub fn constraint_violation(
table: &'static str,
constraint: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::Database(DbError::constraint_violation(table, constraint, message))
}
#[track_caller]
pub fn connection_failed(url: impl Into<String>, message: impl Into<String>) -> Self {
Self::Database(DbError::connection_failed(url, message))
}
}
pub const MAX_BATCH_SIZE: usize = 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum BatchErrorCode {
NotFound,
ValidationError,
DuplicateKey,
VersionConflict,
DatabaseError,
InternalError,
}
impl From<&CommerceError> for BatchErrorCode {
fn from(err: &CommerceError) -> Self {
if err.is_not_found() {
return Self::NotFound;
}
if err.is_validation() {
return Self::ValidationError;
}
if err.is_conflict() {
return Self::DuplicateKey;
}
match err {
CommerceError::VersionConflict { .. } | CommerceError::OptimisticLockFailure => {
Self::VersionConflict
}
CommerceError::DatabaseError(_) | CommerceError::Database(_) => Self::DatabaseError,
_ if matches!(err.as_db_error(), Some(DbError::ConstraintViolation { .. })) => {
Self::DuplicateKey
}
_ => Self::InternalError,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchError {
pub index: usize,
pub id: Option<String>,
pub error: String,
pub code: BatchErrorCode,
}
impl BatchError {
#[must_use]
pub fn from_error(index: usize, id: Option<String>, err: &CommerceError) -> Self {
Self { index, id, error: err.to_string(), code: BatchErrorCode::from(err) }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchResult<T> {
pub succeeded: Vec<T>,
pub failed: Vec<BatchError>,
pub total_attempted: usize,
pub success_count: usize,
pub failure_count: usize,
}
impl<T> BatchResult<T> {
#[must_use]
pub const fn new() -> Self {
Self {
succeeded: Vec::new(),
failed: Vec::new(),
total_attempted: 0,
success_count: 0,
failure_count: 0,
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
succeeded: Vec::with_capacity(capacity),
failed: Vec::new(),
total_attempted: 0,
success_count: 0,
failure_count: 0,
}
}
pub fn record_success(&mut self, item: T) {
self.succeeded.push(item);
self.success_count += 1;
self.total_attempted += 1;
}
pub fn record_failure(&mut self, index: usize, id: Option<String>, err: &CommerceError) {
self.failed.push(BatchError::from_error(index, id, err));
self.failure_count += 1;
self.total_attempted += 1;
}
#[must_use]
pub const fn all_succeeded(&self) -> bool {
self.failure_count == 0
}
#[must_use]
pub const fn all_failed(&self) -> bool {
self.success_count == 0 && self.total_attempted > 0
}
#[must_use]
pub const fn partial_success(&self) -> bool {
self.success_count > 0 && self.failure_count > 0
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.total_attempted == 0
}
}
impl<T> Default for BatchResult<T> {
fn default() -> Self {
Self::new()
}
}
pub fn validate_batch_size<T>(items: &[T]) -> Result<()> {
if items.len() > MAX_BATCH_SIZE {
return Err(CommerceError::ValidationError(format!(
"Batch size {} exceeds maximum of {}",
items.len(),
MAX_BATCH_SIZE
)));
}
Ok(())
}
pub fn validate_required_text(field: &str, value: &str, max_len: usize) -> Result<()> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(CommerceError::InvalidInput {
field: field.to_string(),
message: "cannot be empty".into(),
});
}
if trimmed.len() > max_len {
return Err(CommerceError::InvalidInput {
field: field.to_string(),
message: format!("cannot exceed {max_len} characters"),
});
}
Ok(())
}
pub fn validate_required_uuid(field: &str, value: Uuid) -> Result<()> {
if value.is_nil() {
return Err(CommerceError::InvalidInput {
field: field.to_string(),
message: "cannot be nil".into(),
});
}
Ok(())
}
pub fn validate_email(email: &str) -> Result<()> {
let email = email.trim();
if email.is_empty() {
return Err(CommerceError::ValidationError("Email cannot be empty".into()));
}
if email.len() > 254 {
return Err(CommerceError::ValidationError("Email cannot exceed 254 characters".into()));
}
if email.contains(char::is_whitespace) {
return Err(CommerceError::ValidationError("Email cannot contain whitespace".into()));
}
if email.chars().any(char::is_control) {
return Err(CommerceError::ValidationError(
"Email cannot contain control characters".into(),
));
}
if !email.is_ascii() {
return Err(CommerceError::ValidationError(
"Email must use ASCII or punycode characters".into(),
));
}
let Some((local, domain)) = email.rsplit_once('@') else {
return Err(CommerceError::ValidationError(
"Email must contain exactly one @ symbol".into(),
));
};
if local.contains('@') || domain.contains('@') {
return Err(CommerceError::ValidationError(
"Email must contain exactly one @ symbol".into(),
));
}
if local.is_empty() {
return Err(CommerceError::ValidationError(
"Email local part (before @) cannot be empty".into(),
));
}
if domain.is_empty() {
return Err(CommerceError::ValidationError(
"Email domain (after @) cannot be empty".into(),
));
}
if local.len() > 64 {
return Err(CommerceError::ValidationError(
"Email local part cannot exceed 64 characters".into(),
));
}
if domain.len() > 253 {
return Err(CommerceError::ValidationError(
"Email domain cannot exceed 253 characters".into(),
));
}
if local.starts_with('.') || local.ends_with('.') {
return Err(CommerceError::ValidationError(
"Email local part cannot start or end with a dot".into(),
));
}
if local.contains("..") {
return Err(CommerceError::ValidationError(
"Email local part cannot contain consecutive dots".into(),
));
}
if !local.chars().all(|ch| {
ch.is_ascii_alphanumeric()
|| matches!(
ch,
'!' | '#'
| '$'
| '%'
| '&'
| '\''
| '*'
| '+'
| '-'
| '/'
| '='
| '?'
| '^'
| '_'
| '`'
| '{'
| '|'
| '}'
| '~'
| '.'
)
}) {
return Err(CommerceError::ValidationError(
"Email local part contains unsupported characters".into(),
));
}
if !domain.contains('.') {
return Err(CommerceError::ValidationError(
"Email domain must contain at least one dot".into(),
));
}
if domain.starts_with('.') || domain.ends_with('.') {
return Err(CommerceError::ValidationError(
"Email domain cannot start or end with a dot".into(),
));
}
if domain.contains("..") {
return Err(CommerceError::ValidationError(
"Email domain cannot contain consecutive dots".into(),
));
}
for label in domain.split('.') {
if label.is_empty() {
return Err(CommerceError::ValidationError(
"Email domain cannot contain empty labels".into(),
));
}
if label.len() > 63 {
return Err(CommerceError::ValidationError(
"Email domain labels cannot exceed 63 characters".into(),
));
}
if label.starts_with('-') || label.ends_with('-') {
return Err(CommerceError::ValidationError(
"Email domain labels cannot start or end with a hyphen".into(),
));
}
if !label.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-') {
return Err(CommerceError::ValidationError(
"Email domain contains unsupported characters".into(),
));
}
}
Ok(())
}
pub fn validate_sku(sku: &str) -> Result<()> {
let sku = sku.trim();
if sku.is_empty() {
return Err(CommerceError::ValidationError("SKU cannot be empty".into()));
}
if sku.len() > 100 {
return Err(CommerceError::ValidationError("SKU cannot exceed 100 characters".into()));
}
if !sku.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
return Err(CommerceError::ValidationError(
"SKU can only contain alphanumeric characters, hyphens, and underscores".into(),
));
}
Ok(())
}
pub fn validate_phone(phone: &str) -> Result<()> {
let phone = phone.trim();
if phone.is_empty() {
return Err(CommerceError::ValidationError("Phone number cannot be empty".into()));
}
if !phone
.chars()
.all(|c| c.is_ascii_digit() || c == ' ' || c == '-' || c == '(' || c == ')' || c == '+')
{
return Err(CommerceError::ValidationError(
"Phone number contains invalid characters".into(),
));
}
let digit_count = phone.chars().filter(char::is_ascii_digit).count();
if digit_count < 7 {
return Err(CommerceError::ValidationError(
"Phone number must have at least 7 digits".into(),
));
}
if digit_count > 15 {
return Err(CommerceError::ValidationError("Phone number cannot exceed 15 digits".into()));
}
Ok(())
}
pub fn validate_currency_code(code: &str) -> Result<()> {
if code.len() != 3 {
return Err(CommerceError::ValidationError(
"Currency code must be exactly 3 characters".into(),
));
}
if !code.chars().all(|c| c.is_ascii_uppercase()) {
return Err(CommerceError::ValidationError(
"Currency code must be uppercase letters only".into(),
));
}
Ok(())
}
pub fn validate_postal_code(code: &str) -> Result<()> {
let code = code.trim();
if code.is_empty() {
return Err(CommerceError::ValidationError("Postal code cannot be empty".into()));
}
if code.len() < 3 {
return Err(CommerceError::ValidationError(
"Postal code must be at least 3 characters".into(),
));
}
if code.len() > 10 {
return Err(CommerceError::ValidationError(
"Postal code cannot exceed 10 characters".into(),
));
}
if !code.chars().all(|c| c.is_alphanumeric() || c == ' ' || c == '-') {
return Err(CommerceError::ValidationError(
"Postal code contains invalid characters".into(),
));
}
Ok(())
}
pub fn validate_quantity(qty: rust_decimal::Decimal) -> Result<()> {
if qty <= rust_decimal::Decimal::ZERO {
return Err(CommerceError::ValidationError("Quantity must be greater than zero".into()));
}
Ok(())
}
pub fn validate_price(price: rust_decimal::Decimal) -> Result<()> {
if price < rust_decimal::Decimal::ZERO {
return Err(CommerceError::ValidationError("Price cannot be negative".into()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_required_text_rejects_empty() {
let result = validate_required_text("field", " ", 10);
assert!(result.is_err());
}
#[test]
fn validate_required_text_rejects_too_long() {
let result = validate_required_text("field", "toolong", 3);
assert!(result.is_err());
}
#[test]
fn validate_required_text_accepts_trimmed() {
let result = validate_required_text("field", " ok ", 10);
assert!(result.is_ok());
}
#[test]
fn validate_required_uuid_rejects_nil() {
let result = validate_required_uuid("id", Uuid::nil());
assert!(result.is_err());
}
#[test]
fn validate_required_uuid_accepts_non_nil() {
let result = validate_required_uuid("id", Uuid::new_v4());
assert!(result.is_ok());
}
#[test]
fn validate_email_accepts_common_production_formats() {
assert!(validate_email("user@example.com").is_ok());
assert!(validate_email("user.name+tag@example.co.uk").is_ok());
assert!(validate_email("ops_team-42@xn--bcher-kva.example").is_ok());
}
#[test]
fn validate_email_rejects_common_invalid_formats() {
assert!(validate_email("alice..bob@example.com").is_err());
assert!(validate_email(".alice@example.com").is_err());
assert!(validate_email("alice@example..com").is_err());
assert!(validate_email("alice@-example.com").is_err());
assert!(validate_email("alice@example-.com").is_err());
assert!(validate_email("alice@[127.0.0.1]").is_err());
}
#[test]
fn order_error_converts_to_commerce_error() {
let err: CommerceError = OrderError::not_found(Uuid::nil()).into();
assert!(err.is_not_found());
assert!(err.as_order_error().is_some());
}
#[test]
fn inventory_error_converts_to_commerce_error() {
let err: CommerceError = InventoryError::item_not_found("SKU").into();
assert!(err.is_not_found());
assert!(err.as_inventory_error().is_some());
}
#[test]
fn customer_error_converts_to_commerce_error() {
let err: CommerceError = CustomerError::not_found(Uuid::nil()).into();
assert!(err.is_not_found());
assert!(err.as_customer_error().is_some());
}
#[test]
fn product_error_converts_to_commerce_error() {
let err: CommerceError = ProductError::not_found(Uuid::nil()).into();
assert!(err.is_not_found());
assert!(err.as_product_error().is_some());
}
#[test]
fn return_error_converts_to_commerce_error() {
let err: CommerceError = ReturnError::not_found(Uuid::nil()).into();
assert!(err.is_not_found());
}
#[test]
fn payment_error_converts_to_commerce_error() {
let err: CommerceError = PaymentError::not_found(Uuid::nil()).into();
assert!(err.is_not_found());
}
#[test]
fn shipping_error_converts_to_commerce_error() {
let err: CommerceError = ShippingError::not_found(Uuid::nil()).into();
assert!(err.is_not_found());
}
#[test]
fn conflict_detection_domain_errors() {
let err: CommerceError = InventoryError::duplicate_sku("X").into();
assert!(err.is_conflict());
let err: CommerceError = CustomerError::email_already_exists("x@y.com").into();
assert!(err.is_conflict());
let err: CommerceError = ProductError::duplicate_slug("slug").into();
assert!(err.is_conflict());
}
#[test]
fn batch_error_code_from_domain_errors() {
let err: CommerceError = OrderError::not_found(Uuid::nil()).into();
assert_eq!(BatchErrorCode::from(&err), BatchErrorCode::NotFound);
let err: CommerceError = InventoryError::duplicate_sku("X").into();
assert_eq!(BatchErrorCode::from(&err), BatchErrorCode::DuplicateKey);
}
#[test]
fn state_transition_error_in_order() {
let err = OrderError::invalid_transition("pending", "delivered");
let commerce_err: CommerceError = err.into();
let msg = commerce_err.to_string();
assert!(msg.contains("pending"));
assert!(msg.contains("delivered"));
}
#[test]
fn got_expected_basic() {
let ge = GotExpected { got: 2_i32, expected: 5_i32 };
assert_eq!(ge.to_string(), "expected 5, got 2");
}
#[test]
fn non_exhaustive_allows_future_variants() {
let err = CommerceError::NotFound;
let _ = match err {
CommerceError::NotFound => "ok",
_ => "other",
};
}
#[test]
fn print_error_enum_sizes() {
use std::mem::size_of;
println!("--- Error Enum Sizes (bytes) ---");
println!("CommerceError: {}", size_of::<CommerceError>());
println!("DbError: {}", size_of::<DbError>());
println!("OrderError: {}", size_of::<OrderError>());
println!("InventoryError: {}", size_of::<InventoryError>());
println!("CustomerError: {}", size_of::<CustomerError>());
println!("ProductError: {}", size_of::<ProductError>());
println!("ReturnError: {}", size_of::<ReturnError>());
println!("PaymentError: {}", size_of::<PaymentError>());
println!("ShippingError: {}", size_of::<ShippingError>());
}
#[test]
fn is_transient_includes_retryable() {
let err = CommerceError::OptimisticLockFailure;
assert!(err.is_transient());
assert!(err.is_retryable());
}
#[test]
fn is_transient_includes_external_service() {
let err = CommerceError::ExternalServiceError("timeout".into());
assert!(err.is_transient());
assert!(!err.is_retryable()); }
#[test]
fn is_client_error_variants() {
assert!(CommerceError::NotFound.is_client_error());
assert!(CommerceError::ValidationError("bad".into()).is_client_error());
assert!(CommerceError::DuplicateSku("X".into()).is_client_error());
assert!(CommerceError::NotPermitted("denied".into()).is_client_error());
}
#[test]
fn is_server_error_variants() {
assert!(CommerceError::Internal("oops".into()).is_server_error());
assert!(CommerceError::DatabaseError("fail".into()).is_server_error());
assert!(CommerceError::ExternalServiceError("timeout".into()).is_server_error());
}
#[test]
fn is_not_permitted() {
assert!(CommerceError::NotPermitted("admin only".into()).is_not_permitted());
assert!(!CommerceError::NotFound.is_not_permitted());
}
#[test]
fn suggested_status_codes() {
assert_eq!(CommerceError::NotFound.suggested_status_code(), 404);
assert_eq!(CommerceError::OrderNotFound(Uuid::nil()).suggested_status_code(), 404);
assert_eq!(CommerceError::ValidationError("bad".into()).suggested_status_code(), 400);
assert_eq!(CommerceError::DuplicateSku("X".into()).suggested_status_code(), 409);
assert_eq!(CommerceError::NotPermitted("no".into()).suggested_status_code(), 403);
assert_eq!(CommerceError::ExternalServiceError("t".into()).suggested_status_code(), 502);
assert_eq!(CommerceError::Internal("x".into()).suggested_status_code(), 500);
}
#[test]
fn client_vs_server_mutually_exclusive_for_not_found() {
let err = CommerceError::NotFound;
assert!(err.is_client_error());
assert!(!err.is_server_error());
}
#[test]
fn got_expected_const_new() {
let a = GotExpected::new(2, 5);
let b = GotExpected { got: 2, expected: 5 };
assert_eq!(a, b);
}
}