Skip to main content

shopify_sdk/rest/resources/v2025_10/
shop.rs

1//! Shop resource implementation.
2//!
3//! This module provides the [`Shop`] resource for retrieving shop information from Shopify.
4//! The Shop resource is a singleton, read-only resource that represents the current shop.
5//!
6//! # Singleton Pattern
7//!
8//! Unlike other resources, Shop does not have standard CRUD operations. There is only
9//! one shop per API session, accessed via the `current()` method.
10//!
11//! # Example
12//!
13//! ```rust,ignore
14//! use shopify_sdk::rest::resources::v2025_10::Shop;
15//!
16//! // Get current shop information
17//! let shop = Shop::current(&client).await?;
18//! println!("Shop: {}", shop.name.as_deref().unwrap_or(""));
19//! println!("Domain: {}", shop.domain.as_deref().unwrap_or(""));
20//! println!("Plan: {}", shop.plan_name.as_deref().unwrap_or(""));
21//! ```
22
23use chrono::{DateTime, Utc};
24use serde::{Deserialize, Serialize};
25
26use crate::clients::RestClient;
27use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
28use crate::HttpMethod;
29
30/// A Shopify shop.
31///
32/// The Shop resource contains information about the store including its name,
33/// domain, contact information, address, and configuration settings.
34///
35/// # Read-Only Resource
36///
37/// The Shop resource is read-only and cannot be created, updated, or deleted
38/// through the REST API. All fields are read-only and will not be serialized
39/// in requests.
40///
41/// # Singleton Pattern
42///
43/// There is only one Shop per API session. Use [`Shop::current()`] to retrieve it.
44///
45/// # Example
46///
47/// ```rust,ignore
48/// use shopify_sdk::rest::resources::v2025_10::Shop;
49///
50/// let shop = Shop::current(&client).await?;
51/// println!("Shop name: {}", shop.name.as_deref().unwrap_or("Unknown"));
52/// println!("Currency: {}", shop.currency.as_deref().unwrap_or("USD"));
53/// ```
54#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
55pub struct Shop {
56    // --- Read-only identification fields ---
57    /// The unique identifier of the shop.
58    #[serde(skip_serializing)]
59    pub id: Option<u64>,
60
61    /// The name of the shop.
62    #[serde(skip_serializing)]
63    pub name: Option<String>,
64
65    /// The contact email address for the shop.
66    #[serde(skip_serializing)]
67    pub email: Option<String>,
68
69    /// The shop's custom domain (e.g., "www.example.com").
70    #[serde(skip_serializing)]
71    pub domain: Option<String>,
72
73    /// The shop's myshopify.com domain (e.g., "example.myshopify.com").
74    #[serde(skip_serializing)]
75    pub myshopify_domain: Option<String>,
76
77    // --- Plan information ---
78    /// The name of the Shopify plan the shop is on (e.g., "basic", "professional").
79    #[serde(skip_serializing)]
80    pub plan_name: Option<String>,
81
82    /// The display-friendly name of the plan.
83    #[serde(skip_serializing)]
84    pub plan_display_name: Option<String>,
85
86    /// Whether the shop has a storefront password enabled.
87    #[serde(skip_serializing)]
88    pub password_enabled: Option<bool>,
89
90    // --- Address fields ---
91    /// The primary address line.
92    #[serde(skip_serializing)]
93    pub address1: Option<String>,
94
95    /// The secondary address line.
96    #[serde(skip_serializing)]
97    pub address2: Option<String>,
98
99    /// The city.
100    #[serde(skip_serializing)]
101    pub city: Option<String>,
102
103    /// The province or state.
104    #[serde(skip_serializing)]
105    pub province: Option<String>,
106
107    /// The two-letter province code.
108    #[serde(skip_serializing)]
109    pub province_code: Option<String>,
110
111    /// The country name.
112    #[serde(skip_serializing)]
113    pub country: Option<String>,
114
115    /// The two-letter country code (ISO 3166-1 alpha-2).
116    #[serde(skip_serializing)]
117    pub country_code: Option<String>,
118
119    /// The full country name.
120    #[serde(skip_serializing)]
121    pub country_name: Option<String>,
122
123    /// The postal/ZIP code.
124    #[serde(skip_serializing)]
125    pub zip: Option<String>,
126
127    /// The shop's phone number.
128    #[serde(skip_serializing)]
129    pub phone: Option<String>,
130
131    /// The latitude coordinate of the shop's location.
132    #[serde(skip_serializing)]
133    pub latitude: Option<f64>,
134
135    /// The longitude coordinate of the shop's location.
136    #[serde(skip_serializing)]
137    pub longitude: Option<f64>,
138
139    // --- Currency and money formatting ---
140    /// The three-letter currency code (e.g., "USD", "EUR").
141    #[serde(skip_serializing)]
142    pub currency: Option<String>,
143
144    /// The format for displaying money without currency symbol.
145    #[serde(skip_serializing)]
146    pub money_format: Option<String>,
147
148    /// The format for displaying money with currency symbol.
149    #[serde(skip_serializing)]
150    pub money_with_currency_format: Option<String>,
151
152    // --- Timezone ---
153    /// The timezone in display format (e.g., "(GMT-05:00) Eastern Time (US & Canada)").
154    #[serde(skip_serializing)]
155    pub timezone: Option<String>,
156
157    /// The IANA timezone identifier (e.g., `America/New_York`).
158    #[serde(skip_serializing)]
159    pub iana_timezone: Option<String>,
160
161    // --- Feature flags ---
162    /// Whether the checkout API is supported.
163    #[serde(skip_serializing)]
164    pub checkout_api_supported: Option<bool>,
165
166    /// Whether multi-location is enabled.
167    #[serde(skip_serializing)]
168    pub multi_location_enabled: Option<bool>,
169
170    /// Whether prices include taxes.
171    #[serde(skip_serializing)]
172    pub taxes_included: Option<bool>,
173
174    /// Whether shipping is taxed.
175    #[serde(skip_serializing)]
176    pub tax_shipping: Option<bool>,
177
178    /// Whether transactional SMS is disabled.
179    #[serde(skip_serializing)]
180    pub transactional_sms_disabled: Option<bool>,
181
182    /// Whether the storefront access token has been enabled for checkout.
183    #[serde(skip_serializing)]
184    pub has_storefront_api: Option<bool>,
185
186    /// Whether the shop has discounts enabled.
187    #[serde(skip_serializing)]
188    pub has_discounts: Option<bool>,
189
190    /// Whether the shop has gift cards enabled.
191    #[serde(skip_serializing)]
192    pub has_gift_cards: Option<bool>,
193
194    /// Whether the shop is eligible for payments.
195    #[serde(skip_serializing)]
196    pub eligible_for_payments: Option<bool>,
197
198    /// Whether the shop requires extra payments agreement.
199    #[serde(skip_serializing)]
200    pub requires_extra_payments_agreement: Option<bool>,
201
202    /// Whether the shop is set up.
203    #[serde(skip_serializing)]
204    pub setup_required: Option<bool>,
205
206    /// Whether pre-launch mode is enabled.
207    #[serde(skip_serializing)]
208    pub pre_launch_enabled: Option<bool>,
209
210    /// Whether the shop is enabled for cookie consent.
211    #[serde(skip_serializing)]
212    pub cookie_consent_level: Option<String>,
213
214    // --- Shop details ---
215    /// The shop owner's name.
216    #[serde(skip_serializing)]
217    pub shop_owner: Option<String>,
218
219    /// The source for the shop creation.
220    #[serde(skip_serializing)]
221    pub source: Option<String>,
222
223    /// The weight unit used by the shop ("kg", "lb", "oz", "g").
224    #[serde(skip_serializing)]
225    pub weight_unit: Option<String>,
226
227    /// The primary locale of the shop.
228    #[serde(skip_serializing)]
229    pub primary_locale: Option<String>,
230
231    /// Additional enabled locales.
232    #[serde(skip_serializing)]
233    pub enabled_presentment_currencies: Option<Vec<String>>,
234
235    // --- Timestamps ---
236    /// When the shop was created.
237    #[serde(skip_serializing)]
238    pub created_at: Option<DateTime<Utc>>,
239
240    /// When the shop was last updated.
241    #[serde(skip_serializing)]
242    pub updated_at: Option<DateTime<Utc>>,
243
244    /// The admin GraphQL API ID.
245    #[serde(skip_serializing)]
246    pub admin_graphql_api_id: Option<String>,
247}
248
249impl RestResource for Shop {
250    type Id = u64;
251    type FindParams = ();
252    type AllParams = ();
253    type CountParams = ();
254
255    const NAME: &'static str = "Shop";
256    const PLURAL: &'static str = "shop";
257
258    // Shop only has a Find operation at the `shop` path (no ID parameter).
259    // It does not support Create, Update, Delete, All, or Count operations.
260    const PATHS: &'static [ResourcePath] = &[ResourcePath::new(
261        HttpMethod::Get,
262        ResourceOperation::Find,
263        &[],
264        "shop",
265    )];
266
267    /// Returns None since Shop is a singleton and doesn't use ID-based operations.
268    fn get_id(&self) -> Option<Self::Id> {
269        None
270    }
271}
272
273impl Shop {
274    /// Retrieves the current shop.
275    ///
276    /// Sends a GET request to `/admin/api/{version}/shop.json`.
277    ///
278    /// # Arguments
279    ///
280    /// * `client` - The REST client to use for the request
281    ///
282    /// # Returns
283    ///
284    /// Returns the Shop directly (not wrapped in `ResourceResponse`) since
285    /// pagination is not applicable to singleton resources.
286    ///
287    /// # Errors
288    ///
289    /// Returns [`ResourceError::Http`] if the request fails.
290    ///
291    /// # Example
292    ///
293    /// ```rust,ignore
294    /// use shopify_sdk::rest::resources::v2025_10::Shop;
295    ///
296    /// let shop = Shop::current(&client).await?;
297    /// println!("Shop: {}", shop.name.as_deref().unwrap_or(""));
298    /// println!("Domain: {}", shop.myshopify_domain.as_deref().unwrap_or(""));
299    /// println!("Plan: {}", shop.plan_name.as_deref().unwrap_or(""));
300    /// println!("Currency: {}", shop.currency.as_deref().unwrap_or(""));
301    /// ```
302    pub async fn current(client: &RestClient) -> Result<Self, ResourceError> {
303        let path = "shop";
304
305        let response = client.get(path, None).await?;
306
307        if !response.is_ok() {
308            return Err(ResourceError::from_http_response(
309                response.code,
310                &response.body,
311                Self::NAME,
312                None,
313                response.request_id(),
314            ));
315        }
316
317        // Parse the response - Shopify returns the shop wrapped in "shop" key
318        let shop: Self = response
319            .body
320            .get("shop")
321            .ok_or_else(|| {
322                ResourceError::Http(crate::clients::HttpError::Response(
323                    crate::clients::HttpResponseError {
324                        code: response.code,
325                        message: "Missing 'shop' in response".to_string(),
326                        error_reference: response.request_id().map(ToString::to_string),
327                    },
328                ))
329            })
330            .and_then(|v| {
331                serde_json::from_value(v.clone()).map_err(|e| {
332                    ResourceError::Http(crate::clients::HttpError::Response(
333                        crate::clients::HttpResponseError {
334                            code: response.code,
335                            message: format!("Failed to deserialize shop: {e}"),
336                            error_reference: response.request_id().map(ToString::to_string),
337                        },
338                    ))
339                })
340            })?;
341
342        Ok(shop)
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use crate::rest::{get_path, ResourceOperation};
350
351    #[test]
352    fn test_shop_struct_deserialization_from_api_response() {
353        let json_str = r#"{
354            "id": 548380009,
355            "name": "John Smith Test Store",
356            "email": "j.smith@example.com",
357            "domain": "shop.example.com",
358            "myshopify_domain": "john-smith-test-store.myshopify.com",
359            "plan_name": "partner_test",
360            "plan_display_name": "Partner Test",
361            "password_enabled": false,
362            "address1": "123 Main St",
363            "address2": "Suite 100",
364            "city": "Ottawa",
365            "province": "Ontario",
366            "province_code": "ON",
367            "country": "Canada",
368            "country_code": "CA",
369            "country_name": "Canada",
370            "zip": "K1A 0B1",
371            "phone": "1234567890",
372            "latitude": 45.4215,
373            "longitude": -75.6972,
374            "currency": "CAD",
375            "money_format": "${{amount}}",
376            "money_with_currency_format": "${{amount}} CAD",
377            "timezone": "(GMT-05:00) Eastern Time (US & Canada)",
378            "iana_timezone": "America/Toronto",
379            "checkout_api_supported": true,
380            "multi_location_enabled": true,
381            "taxes_included": false,
382            "tax_shipping": null,
383            "weight_unit": "kg",
384            "primary_locale": "en",
385            "created_at": "2023-01-01T12:00:00-05:00",
386            "updated_at": "2024-06-01T12:00:00-05:00"
387        }"#;
388
389        let shop: Shop = serde_json::from_str(json_str).unwrap();
390
391        assert_eq!(shop.id, Some(548380009));
392        assert_eq!(shop.name.as_deref(), Some("John Smith Test Store"));
393        assert_eq!(shop.email.as_deref(), Some("j.smith@example.com"));
394        assert_eq!(shop.domain.as_deref(), Some("shop.example.com"));
395        assert_eq!(
396            shop.myshopify_domain.as_deref(),
397            Some("john-smith-test-store.myshopify.com")
398        );
399        assert_eq!(shop.plan_name.as_deref(), Some("partner_test"));
400        assert_eq!(shop.plan_display_name.as_deref(), Some("Partner Test"));
401        assert_eq!(shop.password_enabled, Some(false));
402        assert_eq!(shop.address1.as_deref(), Some("123 Main St"));
403        assert_eq!(shop.address2.as_deref(), Some("Suite 100"));
404        assert_eq!(shop.city.as_deref(), Some("Ottawa"));
405        assert_eq!(shop.province.as_deref(), Some("Ontario"));
406        assert_eq!(shop.province_code.as_deref(), Some("ON"));
407        assert_eq!(shop.country.as_deref(), Some("Canada"));
408        assert_eq!(shop.country_code.as_deref(), Some("CA"));
409        assert_eq!(shop.zip.as_deref(), Some("K1A 0B1"));
410        assert_eq!(shop.currency.as_deref(), Some("CAD"));
411        assert_eq!(
412            shop.timezone.as_deref(),
413            Some("(GMT-05:00) Eastern Time (US & Canada)")
414        );
415        assert_eq!(shop.iana_timezone.as_deref(), Some("America/Toronto"));
416        assert_eq!(shop.checkout_api_supported, Some(true));
417        assert_eq!(shop.multi_location_enabled, Some(true));
418        assert_eq!(shop.taxes_included, Some(false));
419        assert_eq!(shop.weight_unit.as_deref(), Some("kg"));
420        assert_eq!(shop.primary_locale.as_deref(), Some("en"));
421        assert!(shop.created_at.is_some());
422        assert!(shop.updated_at.is_some());
423    }
424
425    #[test]
426    fn test_shop_current_method_signature_exists() {
427        // Verify the method signature is correct by referencing it
428        fn _assert_current_signature<F, Fut>(f: F)
429        where
430            F: Fn(&RestClient) -> Fut,
431            Fut: std::future::Future<Output = Result<Shop, ResourceError>>,
432        {
433            let _ = f;
434        }
435
436        // This test verifies the method exists and has the correct signature.
437        // The actual HTTP call would require a mock client.
438    }
439
440    #[test]
441    fn test_shop_does_not_have_standard_crud_paths() {
442        // Shop should only have a Find path
443        let find_path = get_path(Shop::PATHS, ResourceOperation::Find, &[]);
444        assert!(find_path.is_some());
445        assert_eq!(find_path.unwrap().template, "shop");
446
447        // Shop should NOT have Create, Update, Delete, All, or Count paths
448        let create_path = get_path(Shop::PATHS, ResourceOperation::Create, &[]);
449        assert!(create_path.is_none());
450
451        let update_path = get_path(Shop::PATHS, ResourceOperation::Update, &["id"]);
452        assert!(update_path.is_none());
453
454        let delete_path = get_path(Shop::PATHS, ResourceOperation::Delete, &["id"]);
455        assert!(delete_path.is_none());
456
457        let all_path = get_path(Shop::PATHS, ResourceOperation::All, &[]);
458        assert!(all_path.is_none());
459
460        let count_path = get_path(Shop::PATHS, ResourceOperation::Count, &[]);
461        assert!(count_path.is_none());
462    }
463
464    #[test]
465    fn test_shop_read_only_fields_are_not_serialized() {
466        let shop = Shop {
467            id: Some(548380009),
468            name: Some("Test Store".to_string()),
469            email: Some("test@example.com".to_string()),
470            domain: Some("shop.example.com".to_string()),
471            myshopify_domain: Some("test-store.myshopify.com".to_string()),
472            plan_name: Some("basic".to_string()),
473            currency: Some("USD".to_string()),
474            timezone: Some("(GMT-05:00) Eastern Time".to_string()),
475            ..Default::default()
476        };
477
478        let json = serde_json::to_string(&shop).unwrap();
479        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
480
481        // All fields have skip_serializing, so the JSON should be empty
482        assert!(
483            parsed.as_object().unwrap().is_empty(),
484            "Shop serialization should produce empty object since all fields have skip_serializing"
485        );
486    }
487
488    #[test]
489    fn test_shop_all_major_fields_are_captured() {
490        // This test verifies that a complete Shop response can be deserialized
491        let json_str = r#"{
492            "id": 548380009,
493            "name": "Complete Test Store",
494            "email": "complete@example.com",
495            "domain": "complete.example.com",
496            "myshopify_domain": "complete-test.myshopify.com",
497            "plan_name": "unlimited",
498            "plan_display_name": "Shopify Plus",
499            "password_enabled": true,
500            "address1": "123 Commerce St",
501            "address2": "Floor 2",
502            "city": "New York",
503            "province": "New York",
504            "province_code": "NY",
505            "country": "United States",
506            "country_code": "US",
507            "country_name": "United States",
508            "zip": "10001",
509            "phone": "+1-555-555-5555",
510            "latitude": 40.7128,
511            "longitude": -74.0060,
512            "currency": "USD",
513            "money_format": "${{amount}}",
514            "money_with_currency_format": "${{amount}} USD",
515            "timezone": "(GMT-05:00) Eastern Time (US & Canada)",
516            "iana_timezone": "America/New_York",
517            "checkout_api_supported": true,
518            "multi_location_enabled": true,
519            "taxes_included": false,
520            "tax_shipping": true,
521            "transactional_sms_disabled": false,
522            "has_storefront_api": true,
523            "has_discounts": true,
524            "has_gift_cards": true,
525            "eligible_for_payments": true,
526            "requires_extra_payments_agreement": false,
527            "setup_required": false,
528            "pre_launch_enabled": false,
529            "cookie_consent_level": "implicit",
530            "shop_owner": "John Smith",
531            "source": null,
532            "weight_unit": "lb",
533            "primary_locale": "en",
534            "enabled_presentment_currencies": ["USD", "CAD", "EUR"],
535            "created_at": "2022-01-15T10:30:00-05:00",
536            "updated_at": "2024-11-20T14:45:00-05:00",
537            "admin_graphql_api_id": "gid://shopify/Shop/548380009"
538        }"#;
539
540        let shop: Shop = serde_json::from_str(json_str).unwrap();
541
542        // Verify all major field groups are captured
543        // Identification
544        assert_eq!(shop.id, Some(548380009));
545        assert_eq!(shop.name.as_deref(), Some("Complete Test Store"));
546        assert_eq!(shop.email.as_deref(), Some("complete@example.com"));
547        assert_eq!(shop.domain.as_deref(), Some("complete.example.com"));
548        assert_eq!(
549            shop.myshopify_domain.as_deref(),
550            Some("complete-test.myshopify.com")
551        );
552
553        // Plan
554        assert_eq!(shop.plan_name.as_deref(), Some("unlimited"));
555        assert_eq!(shop.plan_display_name.as_deref(), Some("Shopify Plus"));
556        assert_eq!(shop.password_enabled, Some(true));
557
558        // Address
559        assert_eq!(shop.address1.as_deref(), Some("123 Commerce St"));
560        assert_eq!(shop.address2.as_deref(), Some("Floor 2"));
561        assert_eq!(shop.city.as_deref(), Some("New York"));
562        assert_eq!(shop.province.as_deref(), Some("New York"));
563        assert_eq!(shop.province_code.as_deref(), Some("NY"));
564        assert_eq!(shop.country.as_deref(), Some("United States"));
565        assert_eq!(shop.country_code.as_deref(), Some("US"));
566        assert_eq!(shop.country_name.as_deref(), Some("United States"));
567        assert_eq!(shop.zip.as_deref(), Some("10001"));
568        assert_eq!(shop.phone.as_deref(), Some("+1-555-555-5555"));
569        assert_eq!(shop.latitude, Some(40.7128));
570        assert_eq!(shop.longitude, Some(-74.0060));
571
572        // Currency
573        assert_eq!(shop.currency.as_deref(), Some("USD"));
574        assert_eq!(shop.money_format.as_deref(), Some("${{amount}}"));
575        assert_eq!(
576            shop.money_with_currency_format.as_deref(),
577            Some("${{amount}} USD")
578        );
579
580        // Timezone
581        assert_eq!(
582            shop.timezone.as_deref(),
583            Some("(GMT-05:00) Eastern Time (US & Canada)")
584        );
585        assert_eq!(shop.iana_timezone.as_deref(), Some("America/New_York"));
586
587        // Features
588        assert_eq!(shop.checkout_api_supported, Some(true));
589        assert_eq!(shop.multi_location_enabled, Some(true));
590        assert_eq!(shop.taxes_included, Some(false));
591        assert_eq!(shop.tax_shipping, Some(true));
592        assert_eq!(shop.has_storefront_api, Some(true));
593        assert_eq!(shop.has_discounts, Some(true));
594        assert_eq!(shop.has_gift_cards, Some(true));
595        assert_eq!(shop.eligible_for_payments, Some(true));
596        assert_eq!(shop.setup_required, Some(false));
597        assert_eq!(shop.pre_launch_enabled, Some(false));
598
599        // Details
600        assert_eq!(shop.shop_owner.as_deref(), Some("John Smith"));
601        assert_eq!(shop.weight_unit.as_deref(), Some("lb"));
602        assert_eq!(shop.primary_locale.as_deref(), Some("en"));
603        assert_eq!(
604            shop.enabled_presentment_currencies,
605            Some(vec![
606                "USD".to_string(),
607                "CAD".to_string(),
608                "EUR".to_string()
609            ])
610        );
611
612        // Timestamps
613        assert!(shop.created_at.is_some());
614        assert!(shop.updated_at.is_some());
615
616        // GraphQL ID
617        assert_eq!(
618            shop.admin_graphql_api_id.as_deref(),
619            Some("gid://shopify/Shop/548380009")
620        );
621    }
622
623    #[test]
624    fn test_shop_get_id_returns_none_for_singleton() {
625        let shop = Shop {
626            id: Some(548380009),
627            name: Some("Test Store".to_string()),
628            ..Default::default()
629        };
630
631        // Even though the shop has an id field, get_id() returns None
632        // because Shop is a singleton and doesn't use ID-based operations
633        assert!(shop.get_id().is_none());
634    }
635
636    #[test]
637    fn test_shop_resource_constants() {
638        assert_eq!(Shop::NAME, "Shop");
639        assert_eq!(Shop::PLURAL, "shop");
640        assert_eq!(Shop::PATHS.len(), 1);
641    }
642}