Skip to main content

shopify_sdk/rest/resources/v2025_10/
customer.rs

1//! Customer resource implementation.
2
3use 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(&params).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}