1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::rest::{ResourceOperation, ResourcePath, RestResource};
7use crate::HttpMethod;
8
9use super::common::CustomerAddress;
10
11#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
12#[serde(rename_all = "lowercase")]
13pub enum CustomerState {
14 Disabled,
15 Invited,
16 #[default]
17 Enabled,
18 Declined,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
22pub struct EmailMarketingConsent {
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub state: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub opt_in_level: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub consent_updated_at: Option<DateTime<Utc>>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
32pub struct SmsMarketingConsent {
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub state: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub opt_in_level: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub consent_updated_at: Option<DateTime<Utc>>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub consent_collected_from: Option<String>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
44pub struct Customer {
45 #[serde(skip_serializing)]
46 pub id: Option<u64>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub email: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub first_name: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub last_name: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub phone: Option<String>,
55 #[serde(skip_serializing)]
56 pub state: Option<CustomerState>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub tags: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub note: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub verified_email: Option<bool>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub tax_exempt: Option<bool>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub tax_exemptions: Option<Vec<String>>,
67 #[serde(skip_serializing)]
68 pub orders_count: Option<u64>,
69 #[serde(skip_serializing)]
70 pub total_spent: Option<String>,
71 #[serde(skip_serializing)]
72 pub last_order_id: Option<u64>,
73 #[serde(skip_serializing)]
74 pub last_order_name: Option<String>,
75 #[serde(skip_serializing)]
76 pub currency: Option<String>,
77 #[serde(skip_serializing)]
78 pub created_at: Option<DateTime<Utc>>,
79 #[serde(skip_serializing)]
80 pub updated_at: Option<DateTime<Utc>>,
81 #[serde(skip_serializing)]
82 pub admin_graphql_api_id: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub addresses: Option<Vec<CustomerAddress>>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub default_address: Option<CustomerAddress>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub email_marketing_consent: Option<EmailMarketingConsent>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub sms_marketing_consent: Option<SmsMarketingConsent>,
91}
92
93impl RestResource for Customer {
94 type Id = u64;
95 type FindParams = CustomerFindParams;
96 type AllParams = CustomerListParams;
97 type CountParams = CustomerCountParams;
98
99 const NAME: &'static str = "Customer";
100 const PLURAL: &'static str = "customers";
101
102 const PATHS: &'static [ResourcePath] = &[
103 ResourcePath::new(
104 HttpMethod::Get,
105 ResourceOperation::Find,
106 &["id"],
107 "customers/{id}",
108 ),
109 ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "customers"),
110 ResourcePath::new(
111 HttpMethod::Get,
112 ResourceOperation::Count,
113 &[],
114 "customers/count",
115 ),
116 ResourcePath::new(
117 HttpMethod::Post,
118 ResourceOperation::Create,
119 &[],
120 "customers",
121 ),
122 ResourcePath::new(
123 HttpMethod::Put,
124 ResourceOperation::Update,
125 &["id"],
126 "customers/{id}",
127 ),
128 ResourcePath::new(
129 HttpMethod::Delete,
130 ResourceOperation::Delete,
131 &["id"],
132 "customers/{id}",
133 ),
134 ];
135
136 fn get_id(&self) -> Option<Self::Id> {
137 self.id
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
142pub struct CustomerFindParams {
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub fields: Option<String>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
148pub struct CustomerListParams {
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub ids: Option<Vec<u64>>,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub limit: Option<u32>,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub since_id: Option<u64>,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub created_at_min: Option<DateTime<Utc>>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub created_at_max: Option<DateTime<Utc>>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub updated_at_min: Option<DateTime<Utc>>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub updated_at_max: Option<DateTime<Utc>>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub fields: Option<String>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub page_info: Option<String>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
170pub struct CustomerCountParams {
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub created_at_min: Option<DateTime<Utc>>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub created_at_max: Option<DateTime<Utc>>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub updated_at_min: Option<DateTime<Utc>>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub updated_at_max: Option<DateTime<Utc>>,
179}
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::rest::{get_path, ResourceOperation};
184
185 #[test]
186 fn test_customer_struct_serialization() {
187 let customer = Customer {
188 id: Some(12345),
189 email: Some("customer@example.com".into()),
190 first_name: Some("John".into()),
191 last_name: Some("Doe".into()),
192 phone: Some("+1-555-555-5555".into()),
193 state: Some(CustomerState::Enabled),
194 tags: Some("vip, loyal".into()),
195 note: Some("Great customer".into()),
196 verified_email: Some(true),
197 tax_exempt: Some(false),
198 tax_exemptions: Some(vec!["CA_STATE_EXEMPTION".into()]),
199 orders_count: Some(10),
200 total_spent: Some("1234.56".into()),
201 last_order_id: Some(99999),
202 last_order_name: Some("#1001".into()),
203 currency: Some("USD".into()),
204 created_at: None,
205 updated_at: None,
206 admin_graphql_api_id: Some("gid://shopify/Customer/12345".into()),
207 addresses: None,
208 default_address: None,
209 email_marketing_consent: None,
210 sms_marketing_consent: None,
211 };
212
213 let json = serde_json::to_string(&customer).unwrap();
214 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
215
216 assert_eq!(parsed["email"], "customer@example.com");
217 assert_eq!(parsed["first_name"], "John");
218 assert!(parsed.get("id").is_none());
219 assert!(parsed.get("state").is_none());
220 }
221
222 #[test]
223 fn test_customer_deserialization_with_nested_addresses() {
224 let json_str = concat!(
225 r#"{"id":207119551,"email":"bob@example.com","first_name":"Bob","#,
226 r#""last_name":"Norman","phone":"+16135551212","state":"enabled","#,
227 r#""tags":"important","note":"VIP","verified_email":true,"tax_exempt":false,"#,
228 r#""orders_count":1,"total_spent":"199.65","currency":"USD","#,
229 r#""addresses":[{"id":111,"customer_id":207119551,"first_name":"Bob","#,
230 r#""last_name":"Norman","city":"Louisville","default":true}]}"#
231 );
232
233 let customer: Customer = serde_json::from_str(json_str).unwrap();
234
235 assert_eq!(customer.id, Some(207119551));
236 assert_eq!(customer.email.as_deref(), Some("bob@example.com"));
237 assert_eq!(customer.first_name.as_deref(), Some("Bob"));
238 assert_eq!(customer.state, Some(CustomerState::Enabled));
239 assert_eq!(customer.note.as_deref(), Some("VIP"));
240
241 let addresses = customer.addresses.unwrap();
242 assert_eq!(addresses.len(), 1);
243 assert_eq!(addresses[0].id, Some(111));
244 assert_eq!(addresses[0].default, Some(true));
245 }
246
247 #[test]
248 fn test_customer_state_enum_serialization() {
249 assert_eq!(
250 serde_json::to_string(&CustomerState::Disabled).unwrap(),
251 "\"disabled\""
252 );
253 assert_eq!(
254 serde_json::to_string(&CustomerState::Invited).unwrap(),
255 "\"invited\""
256 );
257 assert_eq!(
258 serde_json::to_string(&CustomerState::Enabled).unwrap(),
259 "\"enabled\""
260 );
261 assert_eq!(
262 serde_json::to_string(&CustomerState::Declined).unwrap(),
263 "\"declined\""
264 );
265
266 let disabled: CustomerState = serde_json::from_str("\"disabled\"").unwrap();
267 let enabled: CustomerState = serde_json::from_str("\"enabled\"").unwrap();
268
269 assert_eq!(disabled, CustomerState::Disabled);
270 assert_eq!(enabled, CustomerState::Enabled);
271 assert_eq!(CustomerState::default(), CustomerState::Enabled);
272 }
273
274 #[test]
275 fn test_customer_list_params_with_date_filters() {
276 let created_at_min = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
277 .unwrap()
278 .with_timezone(&Utc);
279
280 let params = CustomerListParams {
281 ids: Some(vec![123, 456, 789]),
282 limit: Some(50),
283 since_id: Some(100),
284 created_at_min: Some(created_at_min),
285 created_at_max: None,
286 updated_at_min: None,
287 updated_at_max: None,
288 fields: Some("id,email".into()),
289 page_info: None,
290 };
291
292 let json = serde_json::to_value(¶ms).unwrap();
293
294 assert_eq!(json["ids"], serde_json::json!([123, 456, 789]));
295 assert_eq!(json["limit"], 50);
296 assert!(json["created_at_min"].as_str().is_some());
297
298 let empty_params = CustomerListParams::default();
299 let empty_json = serde_json::to_value(&empty_params).unwrap();
300 assert_eq!(empty_json, serde_json::json!({}));
301 }
302
303 #[test]
304 fn test_marketing_consent_structs() {
305 let email_consent = EmailMarketingConsent {
306 state: Some("subscribed".into()),
307 opt_in_level: Some("single_opt_in".into()),
308 consent_updated_at: None,
309 };
310
311 let email_json = serde_json::to_value(&email_consent).unwrap();
312 assert_eq!(email_json["state"], "subscribed");
313 assert_eq!(email_json["opt_in_level"], "single_opt_in");
314
315 let sms_consent = SmsMarketingConsent {
316 state: Some("subscribed".into()),
317 opt_in_level: Some("single_opt_in".into()),
318 consent_updated_at: None,
319 consent_collected_from: Some("SHOPIFY".into()),
320 };
321
322 let sms_json = serde_json::to_value(&sms_consent).unwrap();
323 assert_eq!(sms_json["consent_collected_from"], "SHOPIFY");
324
325 assert_eq!(EmailMarketingConsent::default().state, None);
326 assert_eq!(SmsMarketingConsent::default().consent_collected_from, None);
327 }
328
329 #[test]
330 fn test_customer_get_id_returns_correct_value() {
331 let customer_with_id = Customer {
332 id: Some(123456789),
333 email: Some("test@example.com".into()),
334 ..Default::default()
335 };
336 assert_eq!(customer_with_id.get_id(), Some(123456789));
337
338 let customer_without_id = Customer {
339 id: None,
340 email: Some("new@example.com".into()),
341 ..Default::default()
342 };
343 assert_eq!(customer_without_id.get_id(), None);
344 }
345
346 #[test]
347 fn test_customer_path_constants_are_correct() {
348 let find_path = get_path(Customer::PATHS, ResourceOperation::Find, &["id"]);
349 assert!(find_path.is_some());
350 assert_eq!(find_path.unwrap().template, "customers/{id}");
351 assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
352
353 let all_path = get_path(Customer::PATHS, ResourceOperation::All, &[]);
354 assert!(all_path.is_some());
355 assert_eq!(all_path.unwrap().template, "customers");
356
357 let count_path = get_path(Customer::PATHS, ResourceOperation::Count, &[]);
358 assert!(count_path.is_some());
359 assert_eq!(count_path.unwrap().template, "customers/count");
360
361 let create_path = get_path(Customer::PATHS, ResourceOperation::Create, &[]);
362 assert!(create_path.is_some());
363 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
364
365 let update_path = get_path(Customer::PATHS, ResourceOperation::Update, &["id"]);
366 assert!(update_path.is_some());
367 assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
368
369 let delete_path = get_path(Customer::PATHS, ResourceOperation::Delete, &["id"]);
370 assert!(delete_path.is_some());
371 assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
372
373 assert_eq!(Customer::NAME, "Customer");
374 assert_eq!(Customer::PLURAL, "customers");
375 }
376}