Skip to main content

stateset_core/errors/
mod.rs

1//! Error types for commerce operations.
2//!
3//! This module provides comprehensive error handling for all commerce operations.
4//! Errors are organized into a two-level hierarchy:
5//!
6//! - **[`CommerceError`]** — top-level error that all operations return. It composes
7//!   domain-specific sub-errors via `#[from]` conversions.
8//! - **Domain errors** — [`OrderError`], [`InventoryError`], [`CustomerError`],
9//!   [`ProductError`], [`ReturnError`], [`PaymentError`], [`ShippingError`] —
10//!   finer-grained errors for use within domain-specific code.
11//!
12//! # Using domain errors
13//!
14//! New code should prefer returning domain-specific errors when possible. They
15//! convert into `CommerceError` automatically via `From` impls:
16//!
17//! ```rust
18//! use stateset_core::errors::{OrderError, CommerceError};
19//! use uuid::Uuid;
20//!
21//! fn find_order(id: Uuid) -> Result<(), CommerceError> {
22//!     Err(OrderError::not_found(id).into())
23//! }
24//! ```
25//!
26//! # Generic state transition errors
27//!
28//! All state machines share [`StateTransitionError<S>`] for invalid transitions:
29//!
30//! ```rust
31//! use stateset_core::errors::StateTransitionError;
32//!
33//! #[derive(Debug, Clone, Copy)]
34//! enum Light { Red, Green }
35//! impl std::fmt::Display for Light {
36//!     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37//!         write!(f, "{:?}", self)
38//!     }
39//! }
40//!
41//! let err = StateTransitionError::new(Light::Red, Light::Green);
42//! assert!(err.to_string().contains("Red"));
43//! ```
44//!
45//! # Error categorization
46//!
47//! ```rust
48//! use stateset_core::{CommerceError, Result};
49//!
50//! fn process_order(id: uuid::Uuid) -> Result<()> {
51//!     Err(CommerceError::OrderNotFound(id))
52//! }
53//!
54//! match process_order(uuid::Uuid::new_v4()) {
55//!     Err(e) if e.is_not_found() => println!("Not found: {}", e),
56//!     Err(e) if e.is_database() => println!("Database error: {}", e),
57//!     Err(e) => println!("Other error: {}", e),
58//!     Ok(()) => println!("Success"),
59//! }
60//! ```
61
62// Domain sub-error modules (kept private; types are re-exported below).
63mod customer;
64mod inventory;
65mod order;
66mod payment;
67mod product;
68mod returns;
69mod shipping;
70pub mod transition;
71
72pub use customer::CustomerError;
73pub use inventory::InventoryError;
74pub use order::OrderError;
75pub use payment::PaymentError;
76pub use product::ProductError;
77pub use returns::ReturnError;
78pub use shipping::ShippingError;
79pub use transition::{GotExpected, StateTransitionError};
80
81use serde::{Deserialize, Serialize};
82use thiserror::Error;
83use uuid::Uuid;
84
85// ============================================================================
86// Compile-Time Size Assertions (reth pattern)
87// ============================================================================
88
89/// Assert at compile time that a type has the expected size in bytes.
90///
91/// Adapted from the reth project. A size mismatch causes a compile error,
92/// preventing accidental error enum bloat. Only active on 64-bit targets.
93#[cfg(target_pointer_width = "64")]
94macro_rules! static_assert_size {
95    ($ty:ty, $size:expr) => {
96        const _: [(); $size] = [(); ::std::mem::size_of::<$ty>()];
97    };
98}
99
100// Pin error enum sizes to detect accidental bloat.
101// If you intentionally add large variants, update the expected size here.
102#[cfg(target_pointer_width = "64")]
103mod _size_assertions {
104    use super::{
105        CommerceError, CustomerError, DbError, InventoryError, OrderError, PaymentError,
106        ProductError, ReturnError, ShippingError,
107    };
108    static_assert_size!(CommerceError, 80);
109    static_assert_size!(DbError, 64);
110    static_assert_size!(OrderError, 48);
111    static_assert_size!(InventoryError, 72);
112    static_assert_size!(CustomerError, 24);
113    static_assert_size!(ProductError, 24);
114    static_assert_size!(ReturnError, 48);
115    static_assert_size!(PaymentError, 56);
116    static_assert_size!(ShippingError, 48);
117}
118
119// ============================================================================
120// Database Error Types (Enhanced)
121// ============================================================================
122
123/// Specific database error types with context.
124///
125/// This enum provides detailed, typed database errors that preserve context
126/// about what operation failed and why. Use [`CommerceError::as_db_error()`]
127/// to extract the underlying database error for detailed handling.
128#[derive(Error, Debug, Clone)]
129#[non_exhaustive]
130pub enum DbError {
131    /// Failed to establish database connection.
132    #[error("Connection failed to {url}: {message}")]
133    ConnectionFailed {
134        /// The database URL or connection string.
135        url: String,
136        /// Detailed error message.
137        message: String,
138    },
139
140    /// Query execution failed.
141    #[error("Query failed on {table}: {message}")]
142    QueryFailed {
143        /// The table being queried.
144        table: &'static str,
145        /// The operation (e.g., "insert", "update", "select").
146        operation: &'static str,
147        /// Detailed error message.
148        message: String,
149    },
150
151    /// Constraint violation (unique, foreign key, check).
152    #[error("Constraint violation on {table}: {constraint} - {message}")]
153    ConstraintViolation {
154        /// The table with the constraint.
155        table: &'static str,
156        /// The constraint name or type.
157        constraint: String,
158        /// Detailed error message.
159        message: String,
160    },
161
162    /// Database migration failed.
163    #[error("Migration {version} failed: {message}")]
164    MigrationFailed {
165        /// The migration version that failed.
166        version: i32,
167        /// Detailed error message.
168        message: String,
169    },
170
171    /// Transaction error.
172    #[error("Transaction failed: {message}")]
173    TransactionFailed {
174        /// Detailed error message.
175        message: String,
176    },
177
178    /// Connection pool exhausted.
179    #[error("Connection pool exhausted after {timeout_ms}ms")]
180    PoolExhausted {
181        /// The timeout in milliseconds before giving up.
182        timeout_ms: u64,
183    },
184
185    /// Serialization/deserialization error.
186    #[error("Serialization error for {field}: {message}")]
187    SerializationError {
188        /// The field being serialized/deserialized.
189        field: String,
190        /// Detailed error message.
191        message: String,
192    },
193
194    /// Generic database error (fallback).
195    #[error("Database error: {0}")]
196    Other(String),
197}
198
199impl DbError {
200    /// Create a query failed error.
201    #[track_caller]
202    pub fn query_failed(
203        table: &'static str,
204        operation: &'static str,
205        message: impl Into<String>,
206    ) -> Self {
207        Self::QueryFailed { table, operation, message: message.into() }
208    }
209
210    /// Create a constraint violation error.
211    #[track_caller]
212    pub fn constraint_violation(
213        table: &'static str,
214        constraint: impl Into<String>,
215        message: impl Into<String>,
216    ) -> Self {
217        Self::ConstraintViolation { table, constraint: constraint.into(), message: message.into() }
218    }
219
220    /// Create a connection failed error.
221    #[track_caller]
222    pub fn connection_failed(url: impl Into<String>, message: impl Into<String>) -> Self {
223        Self::ConnectionFailed { url: url.into(), message: message.into() }
224    }
225
226    /// Create a transaction failed error.
227    #[track_caller]
228    pub fn transaction_failed(message: impl Into<String>) -> Self {
229        Self::TransactionFailed { message: message.into() }
230    }
231
232    /// Create a serialization error.
233    #[track_caller]
234    pub fn serialization_error(field: impl Into<String>, message: impl Into<String>) -> Self {
235        Self::SerializationError { field: field.into(), message: message.into() }
236    }
237}
238
239// ============================================================================
240// Main Commerce Error Type
241// ============================================================================
242
243/// Main error type for commerce operations.
244///
245/// This is the top-level error that all public APIs return. Domain-specific
246/// sub-errors ([`OrderError`], [`InventoryError`], etc.) convert into this
247/// type via `From` impls, so callers can work with a uniform result type
248/// while domain code uses precise error types internally.
249#[derive(Error, Debug)]
250#[non_exhaustive]
251pub enum CommerceError {
252    // ========================================================================
253    // Order errors
254    // ========================================================================
255    /// Order with given ID was not found.
256    #[error("Order not found: {0}")]
257    OrderNotFound(Uuid),
258
259    /// Order cannot be cancelled in its current status.
260    #[error("Order cannot be cancelled in status: {0}")]
261    OrderCannotBeCancelled(String),
262
263    /// Order cannot be refunded.
264    #[error("Order cannot be refunded: {0}")]
265    OrderCannotBeRefunded(String),
266
267    /// Invalid status transition for an order.
268    #[error("Invalid order status transition from {from} to {to}")]
269    InvalidOrderStatusTransition {
270        /// The current status.
271        from: String,
272        /// The requested new status.
273        to: String,
274    },
275
276    // ========================================================================
277    // Inventory errors
278    // ========================================================================
279    /// Inventory item not found by SKU or ID.
280    #[error("Inventory item not found: {0}")]
281    InventoryItemNotFound(String),
282
283    /// Insufficient stock for the requested quantity.
284    #[error("Insufficient stock for SKU {sku}: requested {requested}, available {available}")]
285    InsufficientStock {
286        /// The SKU that has insufficient stock.
287        sku: String,
288        /// The quantity that was requested.
289        requested: String,
290        /// The quantity that is available.
291        available: String,
292    },
293
294    /// Inventory reservation not found.
295    #[error("Inventory reservation not found: {0}")]
296    ReservationNotFound(Uuid),
297
298    /// Inventory reservation has expired.
299    #[error("Inventory reservation expired: {0}")]
300    ReservationExpired(Uuid),
301
302    /// Duplicate SKU already exists.
303    #[error("Duplicate SKU: {0}")]
304    DuplicateSku(String),
305
306    // ========================================================================
307    // Customer errors
308    // ========================================================================
309    /// Customer with given ID was not found.
310    #[error("Customer not found: {0}")]
311    CustomerNotFound(Uuid),
312
313    /// Email address is already registered.
314    #[error("Email already exists: {0}")]
315    EmailAlreadyExists(String),
316
317    /// Customer account is not active.
318    #[error("Customer is not active")]
319    CustomerNotActive,
320
321    // ========================================================================
322    // Product errors
323    // ========================================================================
324    /// Product with given ID was not found.
325    #[error("Product not found: {0}")]
326    ProductNotFound(Uuid),
327
328    /// Product variant with given ID was not found.
329    #[error("Product variant not found: {0}")]
330    ProductVariantNotFound(Uuid),
331
332    /// Duplicate product slug already exists.
333    #[error("Duplicate product slug: {0}")]
334    DuplicateSlug(String),
335
336    /// Product is not available for purchase.
337    #[error("Product is not purchasable")]
338    ProductNotPurchasable,
339
340    // ========================================================================
341    // Return errors
342    // ========================================================================
343    /// Return with given ID was not found.
344    #[error("Return not found: {0}")]
345    ReturnNotFound(Uuid),
346
347    /// Return cannot be approved in its current status.
348    #[error("Return cannot be approved in status: {0}")]
349    ReturnCannotBeApproved(String),
350
351    /// Return period has expired.
352    #[error("Return period expired")]
353    ReturnPeriodExpired,
354
355    /// Item is not eligible for return.
356    #[error("Item not eligible for return")]
357    ItemNotEligibleForReturn,
358
359    // ========================================================================
360    // Validation errors
361    // ========================================================================
362    /// General validation error.
363    #[error("Validation error: {0}")]
364    ValidationError(String),
365
366    /// Invalid input for a specific field.
367    #[error("Invalid input: {field} - {message}")]
368    InvalidInput {
369        /// The field that has invalid input.
370        field: String,
371        /// The validation error message.
372        message: String,
373    },
374
375    // ========================================================================
376    // Database/storage errors
377    // ========================================================================
378    /// Legacy database error (for backwards compatibility).
379    #[error("Database error: {0}")]
380    DatabaseError(String),
381
382    /// Typed database error with context.
383    #[error(transparent)]
384    Database(#[from] DbError),
385
386    /// Generic record not found.
387    #[error("Record not found")]
388    NotFound,
389
390    /// Conflict during operation.
391    #[error("Conflict: {0}")]
392    Conflict(String),
393
394    /// Optimistic locking failure.
395    #[error("Optimistic lock failure: record was modified")]
396    OptimisticLockFailure,
397
398    /// Version conflict during update.
399    #[error("Version conflict on {entity} {id}: expected version {expected_version}")]
400    VersionConflict {
401        /// The entity type (e.g., "order", "customer").
402        entity: String,
403        /// The entity ID.
404        id: String,
405        /// The expected version that was not found.
406        expected_version: i32,
407    },
408
409    // ========================================================================
410    // External service errors
411    // ========================================================================
412    /// External service (payment, shipping, etc.) failed.
413    #[error("External service error: {0}")]
414    ExternalServiceError(String),
415
416    // ========================================================================
417    // Domain sub-errors (composed via #[from])
418    // ========================================================================
419    /// Order-domain error.
420    #[error(transparent)]
421    Order(#[from] OrderError),
422
423    /// Inventory-domain error.
424    #[error(transparent)]
425    Inventory(#[from] InventoryError),
426
427    /// Customer-domain error.
428    #[error(transparent)]
429    Customer(#[from] CustomerError),
430
431    /// Product-domain error.
432    #[error(transparent)]
433    Product(#[from] ProductError),
434
435    /// Return-domain error.
436    #[error(transparent)]
437    Return(#[from] ReturnError),
438
439    /// Payment-domain error.
440    #[error(transparent)]
441    Payment(#[from] PaymentError),
442
443    /// Shipping-domain error.
444    #[error(transparent)]
445    Shipping(#[from] ShippingError),
446
447    // ========================================================================
448    // General errors
449    // ========================================================================
450    /// Internal error.
451    #[error("Internal error: {0}")]
452    Internal(String),
453
454    /// Operation not permitted.
455    #[error("Operation not permitted: {0}")]
456    NotPermitted(String),
457}
458
459/// Result type alias for commerce operations.
460pub type Result<T> = std::result::Result<T, CommerceError>;
461
462/// Per-domain result type aliases for focused error handling.
463pub mod result {
464    /// Result alias for order operations.
465    pub type OrderResult<T> = std::result::Result<T, super::OrderError>;
466    /// Result alias for inventory operations.
467    pub type InventoryResult<T> = std::result::Result<T, super::InventoryError>;
468    /// Result alias for customer operations.
469    pub type CustomerResult<T> = std::result::Result<T, super::CustomerError>;
470    /// Result alias for product operations.
471    pub type ProductResult<T> = std::result::Result<T, super::ProductError>;
472    /// Result alias for return operations.
473    pub type ReturnResult<T> = std::result::Result<T, super::ReturnError>;
474    /// Result alias for payment operations.
475    pub type PaymentResult<T> = std::result::Result<T, super::PaymentError>;
476    /// Result alias for shipping operations.
477    pub type ShippingResult<T> = std::result::Result<T, super::ShippingError>;
478    /// Result alias for database operations.
479    pub type DbResult<T> = std::result::Result<T, super::DbError>;
480}
481
482impl CommerceError {
483    /// Check if error is a not found error.
484    #[must_use]
485    pub const fn is_not_found(&self) -> bool {
486        match self {
487            Self::NotFound
488            | Self::OrderNotFound(_)
489            | Self::CustomerNotFound(_)
490            | Self::ProductNotFound(_)
491            | Self::ProductVariantNotFound(_)
492            | Self::ReturnNotFound(_)
493            | Self::InventoryItemNotFound(_)
494            | Self::ReservationNotFound(_) => true,
495            // Domain sub-errors
496            Self::Order(OrderError::NotFound(_)) => true,
497            Self::Inventory(
498                InventoryError::ItemNotFound(_) | InventoryError::ReservationNotFound(_),
499            ) => true,
500            Self::Customer(CustomerError::NotFound(_)) => true,
501            Self::Product(ProductError::NotFound(_) | ProductError::VariantNotFound(_)) => true,
502            Self::Return(ReturnError::NotFound(_)) => true,
503            Self::Payment(PaymentError::NotFound(_)) => true,
504            Self::Shipping(ShippingError::NotFound(_)) => true,
505            _ => false,
506        }
507    }
508
509    /// Check if error is a validation error.
510    #[must_use]
511    pub const fn is_validation(&self) -> bool {
512        matches!(self, Self::ValidationError(_) | Self::InvalidInput { .. })
513    }
514
515    /// Check if error is a conflict error.
516    #[must_use]
517    pub const fn is_conflict(&self) -> bool {
518        match self {
519            Self::Conflict(_)
520            | Self::OptimisticLockFailure
521            | Self::VersionConflict { .. }
522            | Self::DuplicateSku(_)
523            | Self::DuplicateSlug(_)
524            | Self::EmailAlreadyExists(_) => true,
525            // Domain sub-errors
526            Self::Inventory(InventoryError::DuplicateSku(_)) => true,
527            Self::Customer(CustomerError::EmailAlreadyExists(_)) => true,
528            Self::Product(ProductError::DuplicateSlug(_)) => true,
529            _ => false,
530        }
531    }
532
533    /// Check if error is a database error.
534    #[must_use]
535    pub const fn is_database(&self) -> bool {
536        matches!(self, Self::DatabaseError(_) | Self::Database(_))
537    }
538
539    /// Check if error is an external service error.
540    #[must_use]
541    pub const fn is_external_service(&self) -> bool {
542        matches!(self, Self::ExternalServiceError(_))
543    }
544
545    /// Check if error is retryable.
546    ///
547    /// Retryable errors include:
548    /// - Connection failures
549    /// - Pool exhaustion
550    /// - Transaction failures (some)
551    /// - Optimistic lock failures
552    #[must_use]
553    pub const fn is_retryable(&self) -> bool {
554        match self {
555            Self::OptimisticLockFailure => true,
556            Self::Database(db_err) => matches!(
557                db_err,
558                DbError::ConnectionFailed { .. }
559                    | DbError::PoolExhausted { .. }
560                    | DbError::TransactionFailed { .. }
561            ),
562            _ => false,
563        }
564    }
565
566    /// Check if error is transient (temporary failures that may resolve on retry).
567    ///
568    /// This is a superset of [`is_retryable`](Self::is_retryable) — it also includes
569    /// external service failures which may recover after a delay.
570    #[must_use]
571    pub const fn is_transient(&self) -> bool {
572        self.is_retryable() || self.is_external_service()
573    }
574
575    /// Check if error is a client error (bad input from the caller).
576    ///
577    /// Client errors include not-found, validation, conflict, and permission errors.
578    #[must_use]
579    pub const fn is_client_error(&self) -> bool {
580        self.is_not_found() || self.is_validation() || self.is_conflict() || self.is_not_permitted()
581    }
582
583    /// Check if error is a server error (internal / infrastructure failures).
584    ///
585    /// Server errors include database errors, internal errors, and external service
586    /// failures.
587    #[must_use]
588    pub const fn is_server_error(&self) -> bool {
589        self.is_database() || matches!(self, Self::Internal(_)) || self.is_external_service()
590    }
591
592    /// Check if this is a permission-denied error.
593    #[must_use]
594    pub const fn is_not_permitted(&self) -> bool {
595        matches!(self, Self::NotPermitted(_))
596    }
597
598    /// Suggest an HTTP status code for this error.
599    ///
600    /// Useful for API layers that need to map domain errors to HTTP responses.
601    ///
602    /// # Example
603    ///
604    /// ```rust
605    /// use stateset_core::CommerceError;
606    ///
607    /// let err = CommerceError::NotFound;
608    /// assert_eq!(err.suggested_status_code(), 404);
609    ///
610    /// let err = CommerceError::ValidationError("bad".into());
611    /// assert_eq!(err.suggested_status_code(), 400);
612    /// ```
613    #[must_use]
614    pub const fn suggested_status_code(&self) -> u16 {
615        if self.is_not_found() {
616            404
617        } else if self.is_validation() {
618            400
619        } else if self.is_conflict() {
620            409
621        } else if self.is_not_permitted() {
622            403
623        } else if self.is_external_service() {
624            502
625        } else {
626            500
627        }
628    }
629
630    /// Get the underlying database error if this is a database error.
631    #[must_use]
632    pub const fn as_db_error(&self) -> Option<&DbError> {
633        match self {
634            Self::Database(e) => Some(e),
635            _ => None,
636        }
637    }
638
639    /// Get the underlying order error if this is an order error.
640    #[must_use]
641    pub const fn as_order_error(&self) -> Option<&OrderError> {
642        match self {
643            Self::Order(e) => Some(e),
644            _ => None,
645        }
646    }
647
648    /// Get the underlying inventory error if this is an inventory error.
649    #[must_use]
650    pub const fn as_inventory_error(&self) -> Option<&InventoryError> {
651        match self {
652            Self::Inventory(e) => Some(e),
653            _ => None,
654        }
655    }
656
657    /// Get the underlying customer error if this is a customer error.
658    #[must_use]
659    pub const fn as_customer_error(&self) -> Option<&CustomerError> {
660        match self {
661            Self::Customer(e) => Some(e),
662            _ => None,
663        }
664    }
665
666    /// Get the underlying product error if this is a product error.
667    #[must_use]
668    pub const fn as_product_error(&self) -> Option<&ProductError> {
669        match self {
670            Self::Product(e) => Some(e),
671            _ => None,
672        }
673    }
674
675    /// Create a database error from a typed `DbError`.
676    #[track_caller]
677    #[must_use]
678    pub const fn db(error: DbError) -> Self {
679        Self::Database(error)
680    }
681
682    /// Create a query failed error with context.
683    #[track_caller]
684    pub fn query_failed(
685        table: &'static str,
686        operation: &'static str,
687        message: impl Into<String>,
688    ) -> Self {
689        Self::Database(DbError::query_failed(table, operation, message))
690    }
691
692    /// Create a constraint violation error.
693    #[track_caller]
694    pub fn constraint_violation(
695        table: &'static str,
696        constraint: impl Into<String>,
697        message: impl Into<String>,
698    ) -> Self {
699        Self::Database(DbError::constraint_violation(table, constraint, message))
700    }
701
702    /// Create a connection failed error.
703    #[track_caller]
704    pub fn connection_failed(url: impl Into<String>, message: impl Into<String>) -> Self {
705        Self::Database(DbError::connection_failed(url, message))
706    }
707}
708
709// ============================================================================
710// Batch Operation Types
711// ============================================================================
712
713/// Maximum items allowed per batch operation.
714pub const MAX_BATCH_SIZE: usize = 1000;
715
716/// Categorized batch error codes for programmatic handling.
717#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize)]
718#[strum(serialize_all = "snake_case")]
719#[serde(rename_all = "snake_case")]
720#[non_exhaustive]
721pub enum BatchErrorCode {
722    /// Entity was not found.
723    NotFound,
724    /// Input validation failed.
725    ValidationError,
726    /// Duplicate key constraint violation.
727    DuplicateKey,
728    /// Optimistic locking version conflict.
729    VersionConflict,
730    /// Database-level error.
731    DatabaseError,
732    /// Unclassified internal error.
733    InternalError,
734}
735
736impl From<&CommerceError> for BatchErrorCode {
737    fn from(err: &CommerceError) -> Self {
738        if err.is_not_found() {
739            return Self::NotFound;
740        }
741        if err.is_validation() {
742            return Self::ValidationError;
743        }
744        if err.is_conflict() {
745            return Self::DuplicateKey;
746        }
747
748        match err {
749            CommerceError::VersionConflict { .. } | CommerceError::OptimisticLockFailure => {
750                Self::VersionConflict
751            }
752
753            CommerceError::DatabaseError(_) | CommerceError::Database(_) => Self::DatabaseError,
754
755            // Also catch constraint violations from DbError as duplicate key
756            _ if matches!(err.as_db_error(), Some(DbError::ConstraintViolation { .. })) => {
757                Self::DuplicateKey
758            }
759
760            _ => Self::InternalError,
761        }
762    }
763}
764
765/// Error information for a single item in a batch operation.
766#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct BatchError {
768    /// Index in the original batch (for create/update operations).
769    pub index: usize,
770    /// ID of the entity (for update/delete/get operations, if available).
771    pub id: Option<String>,
772    /// Human-readable error message.
773    pub error: String,
774    /// Error code for programmatic handling.
775    pub code: BatchErrorCode,
776}
777
778impl BatchError {
779    /// Create a new `BatchError` from an index and `CommerceError`.
780    #[must_use]
781    pub fn from_error(index: usize, id: Option<String>, err: &CommerceError) -> Self {
782        Self { index, id, error: err.to_string(), code: BatchErrorCode::from(err) }
783    }
784}
785
786/// Result of a batch operation that allows partial success.
787#[derive(Debug, Clone, Serialize, Deserialize)]
788pub struct BatchResult<T> {
789    /// Successfully processed items.
790    pub succeeded: Vec<T>,
791    /// Failed operations with their errors.
792    pub failed: Vec<BatchError>,
793    /// Total items attempted.
794    pub total_attempted: usize,
795    /// Count of successful operations.
796    pub success_count: usize,
797    /// Count of failed operations.
798    pub failure_count: usize,
799}
800
801impl<T> BatchResult<T> {
802    /// Create a new empty `BatchResult`.
803    #[must_use]
804    pub const fn new() -> Self {
805        Self {
806            succeeded: Vec::new(),
807            failed: Vec::new(),
808            total_attempted: 0,
809            success_count: 0,
810            failure_count: 0,
811        }
812    }
813
814    /// Create a `BatchResult` with pre-allocated capacity.
815    #[must_use]
816    pub fn with_capacity(capacity: usize) -> Self {
817        Self {
818            succeeded: Vec::with_capacity(capacity),
819            failed: Vec::new(),
820            total_attempted: 0,
821            success_count: 0,
822            failure_count: 0,
823        }
824    }
825
826    /// Record a successful operation.
827    pub fn record_success(&mut self, item: T) {
828        self.succeeded.push(item);
829        self.success_count += 1;
830        self.total_attempted += 1;
831    }
832
833    /// Record a failed operation.
834    pub fn record_failure(&mut self, index: usize, id: Option<String>, err: &CommerceError) {
835        self.failed.push(BatchError::from_error(index, id, err));
836        self.failure_count += 1;
837        self.total_attempted += 1;
838    }
839
840    /// Check if all operations succeeded.
841    #[must_use]
842    pub const fn all_succeeded(&self) -> bool {
843        self.failure_count == 0
844    }
845
846    /// Check if all operations failed.
847    #[must_use]
848    pub const fn all_failed(&self) -> bool {
849        self.success_count == 0 && self.total_attempted > 0
850    }
851
852    /// Check if some operations succeeded and some failed.
853    #[must_use]
854    pub const fn partial_success(&self) -> bool {
855        self.success_count > 0 && self.failure_count > 0
856    }
857
858    /// Check if the batch was empty.
859    #[must_use]
860    pub const fn is_empty(&self) -> bool {
861        self.total_attempted == 0
862    }
863}
864
865impl<T> Default for BatchResult<T> {
866    fn default() -> Self {
867        Self::new()
868    }
869}
870
871/// Validate batch size against maximum limit.
872pub fn validate_batch_size<T>(items: &[T]) -> Result<()> {
873    if items.len() > MAX_BATCH_SIZE {
874        return Err(CommerceError::ValidationError(format!(
875            "Batch size {} exceeds maximum of {}",
876            items.len(),
877            MAX_BATCH_SIZE
878        )));
879    }
880    Ok(())
881}
882
883/// Validate a required text field for non-empty content and length.
884pub fn validate_required_text(field: &str, value: &str, max_len: usize) -> Result<()> {
885    let trimmed = value.trim();
886    if trimmed.is_empty() {
887        return Err(CommerceError::InvalidInput {
888            field: field.to_string(),
889            message: "cannot be empty".into(),
890        });
891    }
892
893    if trimmed.len() > max_len {
894        return Err(CommerceError::InvalidInput {
895            field: field.to_string(),
896            message: format!("cannot exceed {max_len} characters"),
897        });
898    }
899
900    Ok(())
901}
902
903/// Validate that a UUID is not the nil (all-zero) value.
904pub fn validate_required_uuid(field: &str, value: Uuid) -> Result<()> {
905    if value.is_nil() {
906        return Err(CommerceError::InvalidInput {
907            field: field.to_string(),
908            message: "cannot be nil".into(),
909        });
910    }
911
912    Ok(())
913}
914
915/// Validate an email address format.
916///
917/// Performs basic email validation checking for:
918/// - Non-empty string
919/// - Contains exactly one @ symbol
920/// - Has non-empty local and domain parts
921/// - Domain contains at least one dot
922/// - No whitespace characters
923///
924/// # Example
925///
926/// ```
927/// use stateset_core::validate_email;
928///
929/// assert!(validate_email("user@example.com").is_ok());
930/// assert!(validate_email("invalid").is_err());
931/// assert!(validate_email("").is_err());
932/// ```
933pub fn validate_email(email: &str) -> Result<()> {
934    let email = email.trim();
935
936    if email.is_empty() {
937        return Err(CommerceError::ValidationError("Email cannot be empty".into()));
938    }
939
940    if email.len() > 254 {
941        return Err(CommerceError::ValidationError("Email cannot exceed 254 characters".into()));
942    }
943
944    if email.contains(char::is_whitespace) {
945        return Err(CommerceError::ValidationError("Email cannot contain whitespace".into()));
946    }
947
948    if email.chars().any(char::is_control) {
949        return Err(CommerceError::ValidationError(
950            "Email cannot contain control characters".into(),
951        ));
952    }
953
954    if !email.is_ascii() {
955        return Err(CommerceError::ValidationError(
956            "Email must use ASCII or punycode characters".into(),
957        ));
958    }
959
960    let Some((local, domain)) = email.rsplit_once('@') else {
961        return Err(CommerceError::ValidationError(
962            "Email must contain exactly one @ symbol".into(),
963        ));
964    };
965    if local.contains('@') || domain.contains('@') {
966        return Err(CommerceError::ValidationError(
967            "Email must contain exactly one @ symbol".into(),
968        ));
969    }
970
971    if local.is_empty() {
972        return Err(CommerceError::ValidationError(
973            "Email local part (before @) cannot be empty".into(),
974        ));
975    }
976
977    if domain.is_empty() {
978        return Err(CommerceError::ValidationError(
979            "Email domain (after @) cannot be empty".into(),
980        ));
981    }
982
983    if local.len() > 64 {
984        return Err(CommerceError::ValidationError(
985            "Email local part cannot exceed 64 characters".into(),
986        ));
987    }
988
989    if domain.len() > 253 {
990        return Err(CommerceError::ValidationError(
991            "Email domain cannot exceed 253 characters".into(),
992        ));
993    }
994
995    if local.starts_with('.') || local.ends_with('.') {
996        return Err(CommerceError::ValidationError(
997            "Email local part cannot start or end with a dot".into(),
998        ));
999    }
1000
1001    if local.contains("..") {
1002        return Err(CommerceError::ValidationError(
1003            "Email local part cannot contain consecutive dots".into(),
1004        ));
1005    }
1006
1007    if !local.chars().all(|ch| {
1008        ch.is_ascii_alphanumeric()
1009            || matches!(
1010                ch,
1011                '!' | '#'
1012                    | '$'
1013                    | '%'
1014                    | '&'
1015                    | '\''
1016                    | '*'
1017                    | '+'
1018                    | '-'
1019                    | '/'
1020                    | '='
1021                    | '?'
1022                    | '^'
1023                    | '_'
1024                    | '`'
1025                    | '{'
1026                    | '|'
1027                    | '}'
1028                    | '~'
1029                    | '.'
1030            )
1031    }) {
1032        return Err(CommerceError::ValidationError(
1033            "Email local part contains unsupported characters".into(),
1034        ));
1035    }
1036
1037    if !domain.contains('.') {
1038        return Err(CommerceError::ValidationError(
1039            "Email domain must contain at least one dot".into(),
1040        ));
1041    }
1042
1043    // Check domain doesn't start or end with a dot
1044    if domain.starts_with('.') || domain.ends_with('.') {
1045        return Err(CommerceError::ValidationError(
1046            "Email domain cannot start or end with a dot".into(),
1047        ));
1048    }
1049
1050    if domain.contains("..") {
1051        return Err(CommerceError::ValidationError(
1052            "Email domain cannot contain consecutive dots".into(),
1053        ));
1054    }
1055
1056    for label in domain.split('.') {
1057        if label.is_empty() {
1058            return Err(CommerceError::ValidationError(
1059                "Email domain cannot contain empty labels".into(),
1060            ));
1061        }
1062        if label.len() > 63 {
1063            return Err(CommerceError::ValidationError(
1064                "Email domain labels cannot exceed 63 characters".into(),
1065            ));
1066        }
1067        if label.starts_with('-') || label.ends_with('-') {
1068            return Err(CommerceError::ValidationError(
1069                "Email domain labels cannot start or end with a hyphen".into(),
1070            ));
1071        }
1072        if !label.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-') {
1073            return Err(CommerceError::ValidationError(
1074                "Email domain contains unsupported characters".into(),
1075            ));
1076        }
1077    }
1078
1079    Ok(())
1080}
1081
1082/// Validate a SKU format.
1083///
1084/// SKUs must:
1085/// - Be non-empty
1086/// - Be 1-100 characters
1087/// - Contain only alphanumeric characters, hyphens, and underscores
1088///
1089/// # Example
1090///
1091/// ```
1092/// use stateset_core::validate_sku;
1093///
1094/// assert!(validate_sku("SKU-001").is_ok());
1095/// assert!(validate_sku("WIDGET_BLUE_XL").is_ok());
1096/// assert!(validate_sku("").is_err());
1097/// assert!(validate_sku("sku with spaces").is_err());
1098/// ```
1099pub fn validate_sku(sku: &str) -> Result<()> {
1100    let sku = sku.trim();
1101
1102    if sku.is_empty() {
1103        return Err(CommerceError::ValidationError("SKU cannot be empty".into()));
1104    }
1105
1106    if sku.len() > 100 {
1107        return Err(CommerceError::ValidationError("SKU cannot exceed 100 characters".into()));
1108    }
1109
1110    if !sku.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
1111        return Err(CommerceError::ValidationError(
1112            "SKU can only contain alphanumeric characters, hyphens, and underscores".into(),
1113        ));
1114    }
1115
1116    Ok(())
1117}
1118
1119/// Validate a phone number format (basic validation).
1120///
1121/// This performs basic phone number validation:
1122/// - Non-empty
1123/// - Contains only digits, spaces, parentheses, hyphens, and plus sign
1124/// - Has at least 7 digits (minimum for local numbers)
1125/// - Has at most 15 digits (ITU-T E.164 standard)
1126///
1127/// # Example
1128///
1129/// ```
1130/// use stateset_core::validate_phone;
1131///
1132/// assert!(validate_phone("+1 (555) 123-4567").is_ok());
1133/// assert!(validate_phone("5551234567").is_ok());
1134/// assert!(validate_phone("123").is_err()); // Too short
1135/// assert!(validate_phone("").is_err());
1136/// ```
1137pub fn validate_phone(phone: &str) -> Result<()> {
1138    let phone = phone.trim();
1139
1140    if phone.is_empty() {
1141        return Err(CommerceError::ValidationError("Phone number cannot be empty".into()));
1142    }
1143
1144    // Check for valid characters
1145    if !phone
1146        .chars()
1147        .all(|c| c.is_ascii_digit() || c == ' ' || c == '-' || c == '(' || c == ')' || c == '+')
1148    {
1149        return Err(CommerceError::ValidationError(
1150            "Phone number contains invalid characters".into(),
1151        ));
1152    }
1153
1154    // Count digits
1155    let digit_count = phone.chars().filter(char::is_ascii_digit).count();
1156
1157    if digit_count < 7 {
1158        return Err(CommerceError::ValidationError(
1159            "Phone number must have at least 7 digits".into(),
1160        ));
1161    }
1162
1163    if digit_count > 15 {
1164        return Err(CommerceError::ValidationError("Phone number cannot exceed 15 digits".into()));
1165    }
1166
1167    Ok(())
1168}
1169
1170/// Validate a currency code (ISO 4217 format).
1171///
1172/// Currency codes must be exactly 3 uppercase letters.
1173///
1174/// # Example
1175///
1176/// ```
1177/// use stateset_core::validate_currency_code;
1178///
1179/// assert!(validate_currency_code("USD").is_ok());
1180/// assert!(validate_currency_code("EUR").is_ok());
1181/// assert!(validate_currency_code("usd").is_err()); // lowercase
1182/// assert!(validate_currency_code("US").is_err()); // too short
1183/// assert!(validate_currency_code("USDD").is_err()); // too long
1184/// ```
1185pub fn validate_currency_code(code: &str) -> Result<()> {
1186    if code.len() != 3 {
1187        return Err(CommerceError::ValidationError(
1188            "Currency code must be exactly 3 characters".into(),
1189        ));
1190    }
1191
1192    if !code.chars().all(|c| c.is_ascii_uppercase()) {
1193        return Err(CommerceError::ValidationError(
1194            "Currency code must be uppercase letters only".into(),
1195        ));
1196    }
1197
1198    Ok(())
1199}
1200
1201/// Validate a postal/ZIP code format (basic validation).
1202///
1203/// This performs basic postal code validation:
1204/// - Non-empty
1205/// - 3-10 characters
1206/// - Contains only alphanumeric characters, spaces, and hyphens
1207///
1208/// Note: This is a generic validator. For country-specific validation,
1209/// use dedicated validators.
1210///
1211/// # Example
1212///
1213/// ```
1214/// use stateset_core::validate_postal_code;
1215///
1216/// assert!(validate_postal_code("12345").is_ok());
1217/// assert!(validate_postal_code("12345-6789").is_ok());
1218/// assert!(validate_postal_code("SW1A 1AA").is_ok()); // UK format
1219/// assert!(validate_postal_code("").is_err());
1220/// ```
1221pub fn validate_postal_code(code: &str) -> Result<()> {
1222    let code = code.trim();
1223
1224    if code.is_empty() {
1225        return Err(CommerceError::ValidationError("Postal code cannot be empty".into()));
1226    }
1227
1228    if code.len() < 3 {
1229        return Err(CommerceError::ValidationError(
1230            "Postal code must be at least 3 characters".into(),
1231        ));
1232    }
1233
1234    if code.len() > 10 {
1235        return Err(CommerceError::ValidationError(
1236            "Postal code cannot exceed 10 characters".into(),
1237        ));
1238    }
1239
1240    if !code.chars().all(|c| c.is_alphanumeric() || c == ' ' || c == '-') {
1241        return Err(CommerceError::ValidationError(
1242            "Postal code contains invalid characters".into(),
1243        ));
1244    }
1245
1246    Ok(())
1247}
1248
1249/// Validate a quantity value.
1250///
1251/// Quantities must be positive (greater than zero).
1252///
1253/// # Example
1254///
1255/// ```
1256/// use stateset_core::validate_quantity;
1257/// use rust_decimal_macros::dec;
1258///
1259/// assert!(validate_quantity(dec!(1)).is_ok());
1260/// assert!(validate_quantity(dec!(0.5)).is_ok());
1261/// assert!(validate_quantity(dec!(0)).is_err());
1262/// assert!(validate_quantity(dec!(-1)).is_err());
1263/// ```
1264pub fn validate_quantity(qty: rust_decimal::Decimal) -> Result<()> {
1265    if qty <= rust_decimal::Decimal::ZERO {
1266        return Err(CommerceError::ValidationError("Quantity must be greater than zero".into()));
1267    }
1268    Ok(())
1269}
1270
1271/// Validate a price/amount value.
1272///
1273/// Prices must be non-negative (zero or greater).
1274///
1275/// # Example
1276///
1277/// ```
1278/// use stateset_core::validate_price;
1279/// use rust_decimal_macros::dec;
1280///
1281/// assert!(validate_price(dec!(0)).is_ok());
1282/// assert!(validate_price(dec!(99.99)).is_ok());
1283/// assert!(validate_price(dec!(-1)).is_err());
1284/// ```
1285pub fn validate_price(price: rust_decimal::Decimal) -> Result<()> {
1286    if price < rust_decimal::Decimal::ZERO {
1287        return Err(CommerceError::ValidationError("Price cannot be negative".into()));
1288    }
1289    Ok(())
1290}
1291
1292#[cfg(test)]
1293mod tests {
1294    use super::*;
1295
1296    #[test]
1297    fn validate_required_text_rejects_empty() {
1298        let result = validate_required_text("field", "   ", 10);
1299        assert!(result.is_err());
1300    }
1301
1302    #[test]
1303    fn validate_required_text_rejects_too_long() {
1304        let result = validate_required_text("field", "toolong", 3);
1305        assert!(result.is_err());
1306    }
1307
1308    #[test]
1309    fn validate_required_text_accepts_trimmed() {
1310        let result = validate_required_text("field", "  ok  ", 10);
1311        assert!(result.is_ok());
1312    }
1313
1314    #[test]
1315    fn validate_required_uuid_rejects_nil() {
1316        let result = validate_required_uuid("id", Uuid::nil());
1317        assert!(result.is_err());
1318    }
1319
1320    #[test]
1321    fn validate_required_uuid_accepts_non_nil() {
1322        let result = validate_required_uuid("id", Uuid::new_v4());
1323        assert!(result.is_ok());
1324    }
1325
1326    #[test]
1327    fn validate_email_accepts_common_production_formats() {
1328        assert!(validate_email("user@example.com").is_ok());
1329        assert!(validate_email("user.name+tag@example.co.uk").is_ok());
1330        assert!(validate_email("ops_team-42@xn--bcher-kva.example").is_ok());
1331    }
1332
1333    #[test]
1334    fn validate_email_rejects_common_invalid_formats() {
1335        assert!(validate_email("alice..bob@example.com").is_err());
1336        assert!(validate_email(".alice@example.com").is_err());
1337        assert!(validate_email("alice@example..com").is_err());
1338        assert!(validate_email("alice@-example.com").is_err());
1339        assert!(validate_email("alice@example-.com").is_err());
1340        assert!(validate_email("alice@[127.0.0.1]").is_err());
1341    }
1342
1343    // ====================================================================
1344    // Domain sub-error conversion tests
1345    // ====================================================================
1346
1347    #[test]
1348    fn order_error_converts_to_commerce_error() {
1349        let err: CommerceError = OrderError::not_found(Uuid::nil()).into();
1350        assert!(err.is_not_found());
1351        assert!(err.as_order_error().is_some());
1352    }
1353
1354    #[test]
1355    fn inventory_error_converts_to_commerce_error() {
1356        let err: CommerceError = InventoryError::item_not_found("SKU").into();
1357        assert!(err.is_not_found());
1358        assert!(err.as_inventory_error().is_some());
1359    }
1360
1361    #[test]
1362    fn customer_error_converts_to_commerce_error() {
1363        let err: CommerceError = CustomerError::not_found(Uuid::nil()).into();
1364        assert!(err.is_not_found());
1365        assert!(err.as_customer_error().is_some());
1366    }
1367
1368    #[test]
1369    fn product_error_converts_to_commerce_error() {
1370        let err: CommerceError = ProductError::not_found(Uuid::nil()).into();
1371        assert!(err.is_not_found());
1372        assert!(err.as_product_error().is_some());
1373    }
1374
1375    #[test]
1376    fn return_error_converts_to_commerce_error() {
1377        let err: CommerceError = ReturnError::not_found(Uuid::nil()).into();
1378        assert!(err.is_not_found());
1379    }
1380
1381    #[test]
1382    fn payment_error_converts_to_commerce_error() {
1383        let err: CommerceError = PaymentError::not_found(Uuid::nil()).into();
1384        assert!(err.is_not_found());
1385    }
1386
1387    #[test]
1388    fn shipping_error_converts_to_commerce_error() {
1389        let err: CommerceError = ShippingError::not_found(Uuid::nil()).into();
1390        assert!(err.is_not_found());
1391    }
1392
1393    #[test]
1394    fn conflict_detection_domain_errors() {
1395        let err: CommerceError = InventoryError::duplicate_sku("X").into();
1396        assert!(err.is_conflict());
1397
1398        let err: CommerceError = CustomerError::email_already_exists("x@y.com").into();
1399        assert!(err.is_conflict());
1400
1401        let err: CommerceError = ProductError::duplicate_slug("slug").into();
1402        assert!(err.is_conflict());
1403    }
1404
1405    #[test]
1406    fn batch_error_code_from_domain_errors() {
1407        let err: CommerceError = OrderError::not_found(Uuid::nil()).into();
1408        assert_eq!(BatchErrorCode::from(&err), BatchErrorCode::NotFound);
1409
1410        let err: CommerceError = InventoryError::duplicate_sku("X").into();
1411        assert_eq!(BatchErrorCode::from(&err), BatchErrorCode::DuplicateKey);
1412    }
1413
1414    #[test]
1415    fn state_transition_error_in_order() {
1416        let err = OrderError::invalid_transition("pending", "delivered");
1417        let commerce_err: CommerceError = err.into();
1418        let msg = commerce_err.to_string();
1419        assert!(msg.contains("pending"));
1420        assert!(msg.contains("delivered"));
1421    }
1422
1423    #[test]
1424    fn got_expected_basic() {
1425        let ge = GotExpected { got: 2_i32, expected: 5_i32 };
1426        assert_eq!(ge.to_string(), "expected 5, got 2");
1427    }
1428
1429    #[test]
1430    fn non_exhaustive_allows_future_variants() {
1431        // This test verifies that match arms require a wildcard,
1432        // proving #[non_exhaustive] is in effect.
1433        let err = CommerceError::NotFound;
1434        let _ = match err {
1435            CommerceError::NotFound => "ok",
1436            _ => "other",
1437        };
1438    }
1439
1440    // ====================================================================
1441    // Size reporting (run with `--nocapture` to see sizes)
1442    // ====================================================================
1443
1444    #[test]
1445    fn print_error_enum_sizes() {
1446        use std::mem::size_of;
1447        println!("--- Error Enum Sizes (bytes) ---");
1448        println!("CommerceError:  {}", size_of::<CommerceError>());
1449        println!("DbError:        {}", size_of::<DbError>());
1450        println!("OrderError:     {}", size_of::<OrderError>());
1451        println!("InventoryError: {}", size_of::<InventoryError>());
1452        println!("CustomerError:  {}", size_of::<CustomerError>());
1453        println!("ProductError:   {}", size_of::<ProductError>());
1454        println!("ReturnError:    {}", size_of::<ReturnError>());
1455        println!("PaymentError:   {}", size_of::<PaymentError>());
1456        println!("ShippingError:  {}", size_of::<ShippingError>());
1457    }
1458
1459    // ====================================================================
1460    // Error classification tests (Phase 0D)
1461    // ====================================================================
1462
1463    #[test]
1464    fn is_transient_includes_retryable() {
1465        let err = CommerceError::OptimisticLockFailure;
1466        assert!(err.is_transient());
1467        assert!(err.is_retryable());
1468    }
1469
1470    #[test]
1471    fn is_transient_includes_external_service() {
1472        let err = CommerceError::ExternalServiceError("timeout".into());
1473        assert!(err.is_transient());
1474        assert!(!err.is_retryable()); // external is transient but not auto-retryable
1475    }
1476
1477    #[test]
1478    fn is_client_error_variants() {
1479        assert!(CommerceError::NotFound.is_client_error());
1480        assert!(CommerceError::ValidationError("bad".into()).is_client_error());
1481        assert!(CommerceError::DuplicateSku("X".into()).is_client_error());
1482        assert!(CommerceError::NotPermitted("denied".into()).is_client_error());
1483    }
1484
1485    #[test]
1486    fn is_server_error_variants() {
1487        assert!(CommerceError::Internal("oops".into()).is_server_error());
1488        assert!(CommerceError::DatabaseError("fail".into()).is_server_error());
1489        assert!(CommerceError::ExternalServiceError("timeout".into()).is_server_error());
1490    }
1491
1492    #[test]
1493    fn is_not_permitted() {
1494        assert!(CommerceError::NotPermitted("admin only".into()).is_not_permitted());
1495        assert!(!CommerceError::NotFound.is_not_permitted());
1496    }
1497
1498    #[test]
1499    fn suggested_status_codes() {
1500        assert_eq!(CommerceError::NotFound.suggested_status_code(), 404);
1501        assert_eq!(CommerceError::OrderNotFound(Uuid::nil()).suggested_status_code(), 404);
1502        assert_eq!(CommerceError::ValidationError("bad".into()).suggested_status_code(), 400);
1503        assert_eq!(CommerceError::DuplicateSku("X".into()).suggested_status_code(), 409);
1504        assert_eq!(CommerceError::NotPermitted("no".into()).suggested_status_code(), 403);
1505        assert_eq!(CommerceError::ExternalServiceError("t".into()).suggested_status_code(), 502);
1506        assert_eq!(CommerceError::Internal("x".into()).suggested_status_code(), 500);
1507    }
1508
1509    #[test]
1510    fn client_vs_server_mutually_exclusive_for_not_found() {
1511        let err = CommerceError::NotFound;
1512        assert!(err.is_client_error());
1513        assert!(!err.is_server_error());
1514    }
1515
1516    #[test]
1517    fn got_expected_const_new() {
1518        // Verify the new const constructor works identically to struct literal.
1519        let a = GotExpected::new(2, 5);
1520        let b = GotExpected { got: 2, expected: 5 };
1521        assert_eq!(a, b);
1522    }
1523}