1use crate::identifiers::CurrencyCode;
2use crate::model::{CheckoutStatus, OrderStatus, ProductStatus};
3use coil_data::DataModelError;
4use std::error::Error;
5use std::fmt;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum CommerceModelError {
9 EmptyField {
10 field: &'static str,
11 },
12 InvalidToken {
13 field: &'static str,
14 value: String,
15 },
16 InvalidRoute {
17 field: &'static str,
18 value: String,
19 },
20 DataPlan {
21 error: DataModelError,
22 },
23 NegativeAmount {
24 field: &'static str,
25 amount_minor: i64,
26 },
27 ZeroQuantity {
28 field: &'static str,
29 },
30 AmountOverflow {
31 field: &'static str,
32 },
33 BasisPointsOutOfRange {
34 field: &'static str,
35 basis_points: u32,
36 },
37 CurrencyMismatch {
38 expected: CurrencyCode,
39 actual: CurrencyCode,
40 },
41 DuplicateVariant {
42 sku: String,
43 },
44 MissingVariant {
45 sku: String,
46 },
47 DuplicateProduct {
48 product_id: String,
49 },
50 MissingProduct {
51 product_id: String,
52 },
53 DuplicateCollection {
54 collection_id: String,
55 },
56 MissingCollection {
57 collection_id: String,
58 },
59 ProductNotSellable {
60 product_id: String,
61 status: ProductStatus,
62 },
63 MissingLine {
64 sku: String,
65 },
66 EmptyCheckout,
67 CheckoutNotReady {
68 status: CheckoutStatus,
69 },
70 InvalidStatusTransition {
71 from: CheckoutStatus,
72 to: CheckoutStatus,
73 },
74 TotalWouldBecomeNegative {
75 total_minor: i64,
76 },
77 OrderNotRefundable {
78 order_id: String,
79 status: OrderStatus,
80 },
81 RefundExceedsCaptured {
82 order_id: String,
83 captured_minor: i64,
84 refunded_minor: i64,
85 requested_minor: i64,
86 },
87 MissingModuleSetting {
88 module: String,
89 field: String,
90 },
91 InvalidModuleSetting {
92 module: String,
93 field: String,
94 reason: String,
95 },
96 UnsupportedModuleSetting {
97 module: String,
98 field: String,
99 value: String,
100 },
101}
102
103impl fmt::Display for CommerceModelError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::EmptyField { field } => write!(f, "`{field}` cannot be empty"),
107 Self::InvalidToken { field, value } => {
108 write!(f, "`{field}` contains an invalid token `{value}`")
109 }
110 Self::InvalidRoute { field, value } => {
111 write!(f, "`{field}` must start with `/`, got `{value}`")
112 }
113 Self::DataPlan { error } => write!(f, "{error}"),
114 Self::NegativeAmount {
115 field,
116 amount_minor,
117 } => write!(f, "`{field}` cannot be negative, got `{amount_minor}`"),
118 Self::ZeroQuantity { field } => write!(f, "`{field}` must be greater than zero"),
119 Self::AmountOverflow { field } => {
120 write!(f, "arithmetic overflow while calculating `{field}`")
121 }
122 Self::BasisPointsOutOfRange {
123 field,
124 basis_points,
125 } => {
126 write!(
127 f,
128 "`{field}` must be between 0 and 10000, got `{basis_points}`"
129 )
130 }
131 Self::CurrencyMismatch { expected, actual } => {
132 write!(
133 f,
134 "currency mismatch: expected `{expected}`, got `{actual}`"
135 )
136 }
137 Self::DuplicateVariant { sku } => write!(f, "variant `{sku}` is duplicated"),
138 Self::MissingVariant { sku } => write!(f, "variant `{sku}` was not found"),
139 Self::DuplicateProduct { product_id } => {
140 write!(f, "catalog product `{product_id}` is duplicated")
141 }
142 Self::MissingProduct { product_id } => {
143 write!(f, "catalog product `{product_id}` was not found")
144 }
145 Self::DuplicateCollection { collection_id } => {
146 write!(f, "catalog collection `{collection_id}` is duplicated")
147 }
148 Self::MissingCollection { collection_id } => {
149 write!(f, "catalog collection `{collection_id}` was not found")
150 }
151 Self::ProductNotSellable { product_id, status } => {
152 write!(f, "product `{product_id}` is not sellable while `{status}`")
153 }
154 Self::MissingLine { sku } => write!(f, "checkout line for `{sku}` was not found"),
155 Self::EmptyCheckout => f.write_str("checkout must contain at least one line"),
156 Self::CheckoutNotReady { status } => {
157 write!(f, "checkout cannot advance from status `{status}`")
158 }
159 Self::InvalidStatusTransition { from, to } => {
160 write!(f, "cannot transition checkout from `{from}` to `{to}`")
161 }
162 Self::TotalWouldBecomeNegative { total_minor } => {
163 write!(f, "priced total would become negative: `{total_minor}`")
164 }
165 Self::OrderNotRefundable { order_id, status } => {
166 write!(f, "order `{order_id}` cannot be refunded while `{status}`")
167 }
168 Self::RefundExceedsCaptured {
169 order_id,
170 captured_minor,
171 refunded_minor,
172 requested_minor,
173 } => write!(
174 f,
175 "refund for order `{order_id}` exceeds captured amount: captured={captured_minor} refunded={refunded_minor} requested={requested_minor}"
176 ),
177 Self::MissingModuleSetting { module, field } => {
178 write!(f, "module `{module}` requires setting `{field}`")
179 }
180 Self::InvalidModuleSetting {
181 module,
182 field,
183 reason,
184 } => {
185 write!(
186 f,
187 "module `{module}` has invalid setting `{field}`: {reason}"
188 )
189 }
190 Self::UnsupportedModuleSetting {
191 module,
192 field,
193 value,
194 } => {
195 write!(
196 f,
197 "module `{module}` does not support `{field} = {value}` in the current checkout contract"
198 )
199 }
200 }
201 }
202}
203
204impl Error for CommerceModelError {}
205
206impl From<DataModelError> for CommerceModelError {
207 fn from(error: DataModelError) -> Self {
208 Self::DataPlan { error }
209 }
210}