1use chrono::{DateTime, Utc};
11use rust_decimal::Decimal;
12use serde::{Deserialize, Serialize};
13use strum::{Display, EnumString};
14use uuid::Uuid;
15
16use crate::{CommerceError, Result, validate_required_text, validate_sku};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, Serialize, Deserialize)]
20#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
21#[serde(rename_all = "snake_case")]
22#[non_exhaustive]
23pub enum CustomFieldType {
24 String,
25 #[strum(serialize = "integer", serialize = "int")]
26 Integer,
27 #[strum(serialize = "decimal", serialize = "number")]
28 Decimal,
29 #[strum(serialize = "boolean", serialize = "bool")]
30 Boolean,
31 #[strum(serialize = "date_time", serialize = "datetime")]
32 DateTime,
33 Uuid,
34 Json,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct CustomFieldDefinition {
40 pub key: String,
42 pub field_type: CustomFieldType,
44 pub required: bool,
46 pub list: bool,
48 pub description: Option<String>,
50}
51
52impl CustomFieldDefinition {
53 pub fn validate(&self) -> Result<()> {
55 validate_sku(&self.key)?;
57 Ok(())
58 }
59
60 pub fn validate_value(&self, value: &serde_json::Value) -> Result<()> {
62 if value.is_null() {
63 if self.required {
64 return Err(CommerceError::InvalidInput {
65 field: format!("custom_object.values.{}", self.key),
66 message: "is required".into(),
67 });
68 }
69 return Ok(());
70 }
71
72 if self.list {
73 let arr = value.as_array().ok_or_else(|| CommerceError::InvalidInput {
74 field: format!("custom_object.values.{}", self.key),
75 message: "must be an array".into(),
76 })?;
77 for (idx, item) in arr.iter().enumerate() {
78 if item.is_null() {
79 return Err(CommerceError::InvalidInput {
80 field: format!("custom_object.values.{}[{}]", self.key, idx),
81 message: "cannot be null".into(),
82 });
83 }
84 validate_scalar(self.field_type, item).map_err(|mut e| {
85 if let CommerceError::InvalidInput { ref mut field, .. } = e {
87 *field = format!("custom_object.values.{}[{}]", self.key, idx);
88 }
89 e
90 })?;
91 }
92 return Ok(());
93 }
94
95 if value.is_array() {
97 return Err(CommerceError::InvalidInput {
98 field: format!("custom_object.values.{}", self.key),
99 message: "must be a scalar value".into(),
100 });
101 }
102 validate_scalar(self.field_type, value).map_err(|mut e| {
103 if let CommerceError::InvalidInput { ref mut field, .. } = e {
104 *field = format!("custom_object.values.{}", self.key);
105 }
106 e
107 })
108 }
109}
110
111fn validate_scalar(field_type: CustomFieldType, value: &serde_json::Value) -> Result<()> {
112 match field_type {
113 CustomFieldType::String => match value {
114 serde_json::Value::String(s) => {
115 if s.trim().is_empty() {
116 return Err(CommerceError::InvalidInput {
117 field: "custom_object.value".into(),
118 message: "cannot be empty".into(),
119 });
120 }
121 Ok(())
122 }
123 _ => Err(CommerceError::InvalidInput {
124 field: "custom_object.value".into(),
125 message: "must be a string".into(),
126 }),
127 },
128 CustomFieldType::Integer => match value {
129 serde_json::Value::Number(n) => {
130 if n.is_i64() || n.is_u64() {
131 Ok(())
132 } else {
133 Err(CommerceError::InvalidInput {
134 field: "custom_object.value".into(),
135 message: "must be an integer".into(),
136 })
137 }
138 }
139 serde_json::Value::String(s) => {
140 s.parse::<i64>().map_err(|_| CommerceError::InvalidInput {
141 field: "custom_object.value".into(),
142 message: "must be an integer".into(),
143 })?;
144 Ok(())
145 }
146 _ => Err(CommerceError::InvalidInput {
147 field: "custom_object.value".into(),
148 message: "must be an integer".into(),
149 }),
150 },
151 CustomFieldType::Decimal => match value {
152 serde_json::Value::Number(n) => {
153 let s = n.to_string();
154 s.parse::<Decimal>().map_err(|_| CommerceError::InvalidInput {
155 field: "custom_object.value".into(),
156 message: "must be a decimal".into(),
157 })?;
158 Ok(())
159 }
160 serde_json::Value::String(s) => {
161 s.parse::<Decimal>().map_err(|_| CommerceError::InvalidInput {
162 field: "custom_object.value".into(),
163 message: "must be a decimal".into(),
164 })?;
165 Ok(())
166 }
167 _ => Err(CommerceError::InvalidInput {
168 field: "custom_object.value".into(),
169 message: "must be a decimal".into(),
170 }),
171 },
172 CustomFieldType::Boolean => match value {
173 serde_json::Value::Bool(_) => Ok(()),
174 _ => Err(CommerceError::InvalidInput {
175 field: "custom_object.value".into(),
176 message: "must be a boolean".into(),
177 }),
178 },
179 CustomFieldType::DateTime => match value {
180 serde_json::Value::String(s) => {
181 chrono::DateTime::parse_from_rfc3339(s).map_err(|_| {
182 CommerceError::InvalidInput {
183 field: "custom_object.value".into(),
184 message: "must be an RFC3339 datetime string".into(),
185 }
186 })?;
187 Ok(())
188 }
189 _ => Err(CommerceError::InvalidInput {
190 field: "custom_object.value".into(),
191 message: "must be an RFC3339 datetime string".into(),
192 }),
193 },
194 CustomFieldType::Uuid => match value {
195 serde_json::Value::String(s) => {
196 Uuid::parse_str(s).map_err(|_| CommerceError::InvalidInput {
197 field: "custom_object.value".into(),
198 message: "must be a UUID string".into(),
199 })?;
200 Ok(())
201 }
202 _ => Err(CommerceError::InvalidInput {
203 field: "custom_object.value".into(),
204 message: "must be a UUID string".into(),
205 }),
206 },
207 CustomFieldType::Json => Ok(()),
208 }
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213pub struct CustomObjectType {
214 pub id: Uuid,
215 pub handle: String,
217 pub display_name: String,
219 pub description: String,
220 pub fields: Vec<CustomFieldDefinition>,
221 pub created_at: DateTime<Utc>,
222 pub updated_at: DateTime<Utc>,
223 pub version: i32,
225}
226
227impl CustomObjectType {
228 pub fn validate_values(&self, values: &serde_json::Value) -> Result<()> {
230 let obj = values.as_object().ok_or_else(|| CommerceError::InvalidInput {
231 field: "custom_object.values".into(),
232 message: "must be a JSON object".into(),
233 })?;
234
235 for key in obj.keys() {
237 if !self.fields.iter().any(|f| f.key == *key) {
238 return Err(CommerceError::InvalidInput {
239 field: format!("custom_object.values.{key}"),
240 message: "unknown field".into(),
241 });
242 }
243 }
244
245 for field in &self.fields {
247 field.validate()?;
248 match obj.get(&field.key) {
249 Some(v) => field.validate_value(v)?,
250 None if field.required => {
251 return Err(CommerceError::InvalidInput {
252 field: format!("custom_object.values.{}", field.key),
253 message: "is required".into(),
254 });
255 }
256 None => {}
257 }
258 }
259
260 Ok(())
261 }
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, Default)]
266pub struct CreateCustomObjectType {
267 pub handle: String,
268 pub display_name: String,
269 pub description: Option<String>,
270 pub fields: Vec<CustomFieldDefinition>,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, Default)]
275pub struct UpdateCustomObjectType {
276 pub display_name: Option<String>,
277 pub description: Option<String>,
278 pub fields: Option<Vec<CustomFieldDefinition>>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, Default)]
284pub struct CustomObjectTypeFilter {
285 pub search: Option<String>,
286 pub limit: Option<u32>,
287 pub offset: Option<u32>,
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292pub struct CustomObject {
293 pub id: Uuid,
294 pub type_id: Uuid,
295 pub type_handle: String,
296 pub handle: Option<String>,
298 pub owner_type: Option<String>,
300 pub owner_id: Option<String>,
301 pub values: serde_json::Value,
303 pub created_at: DateTime<Utc>,
304 pub updated_at: DateTime<Utc>,
305 pub version: i32,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct CreateCustomObject {
312 pub type_handle: String,
313 pub handle: Option<String>,
314 pub owner_type: Option<String>,
315 pub owner_id: Option<String>,
316 pub values: serde_json::Value,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, Default)]
321pub struct UpdateCustomObject {
322 pub handle: Option<String>,
323 pub owner_type: Option<String>,
324 pub owner_id: Option<String>,
325 pub values: Option<serde_json::Value>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize, Default)]
330pub struct CustomObjectFilter {
331 pub type_handle: Option<String>,
332 pub owner_type: Option<String>,
333 pub owner_id: Option<String>,
334 pub handle: Option<String>,
335 pub limit: Option<u32>,
336 pub offset: Option<u32>,
337}
338
339pub fn validate_custom_object_type_input(input: &CreateCustomObjectType) -> Result<()> {
341 validate_sku(&input.handle)?;
342 validate_required_text("custom_object_type.display_name", &input.display_name, 128)?;
343
344 let mut keys = std::collections::HashSet::new();
346 for field in &input.fields {
347 field.validate()?;
348 if !keys.insert(field.key.clone()) {
349 return Err(CommerceError::InvalidInput {
350 field: "custom_object_type.fields".into(),
351 message: format!("duplicate field key: {}", field.key),
352 });
353 }
354 }
355
356 Ok(())
357}