1use chrono::{DateTime, Utc};
24use serde::{Deserialize, Serialize};
25
26use crate::clients::RestClient;
27use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
28use crate::HttpMethod;
29
30#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
55pub struct Shop {
56 #[serde(skip_serializing)]
59 pub id: Option<u64>,
60
61 #[serde(skip_serializing)]
63 pub name: Option<String>,
64
65 #[serde(skip_serializing)]
67 pub email: Option<String>,
68
69 #[serde(skip_serializing)]
71 pub domain: Option<String>,
72
73 #[serde(skip_serializing)]
75 pub myshopify_domain: Option<String>,
76
77 #[serde(skip_serializing)]
80 pub plan_name: Option<String>,
81
82 #[serde(skip_serializing)]
84 pub plan_display_name: Option<String>,
85
86 #[serde(skip_serializing)]
88 pub password_enabled: Option<bool>,
89
90 #[serde(skip_serializing)]
93 pub address1: Option<String>,
94
95 #[serde(skip_serializing)]
97 pub address2: Option<String>,
98
99 #[serde(skip_serializing)]
101 pub city: Option<String>,
102
103 #[serde(skip_serializing)]
105 pub province: Option<String>,
106
107 #[serde(skip_serializing)]
109 pub province_code: Option<String>,
110
111 #[serde(skip_serializing)]
113 pub country: Option<String>,
114
115 #[serde(skip_serializing)]
117 pub country_code: Option<String>,
118
119 #[serde(skip_serializing)]
121 pub country_name: Option<String>,
122
123 #[serde(skip_serializing)]
125 pub zip: Option<String>,
126
127 #[serde(skip_serializing)]
129 pub phone: Option<String>,
130
131 #[serde(skip_serializing)]
133 pub latitude: Option<f64>,
134
135 #[serde(skip_serializing)]
137 pub longitude: Option<f64>,
138
139 #[serde(skip_serializing)]
142 pub currency: Option<String>,
143
144 #[serde(skip_serializing)]
146 pub money_format: Option<String>,
147
148 #[serde(skip_serializing)]
150 pub money_with_currency_format: Option<String>,
151
152 #[serde(skip_serializing)]
155 pub timezone: Option<String>,
156
157 #[serde(skip_serializing)]
159 pub iana_timezone: Option<String>,
160
161 #[serde(skip_serializing)]
164 pub checkout_api_supported: Option<bool>,
165
166 #[serde(skip_serializing)]
168 pub multi_location_enabled: Option<bool>,
169
170 #[serde(skip_serializing)]
172 pub taxes_included: Option<bool>,
173
174 #[serde(skip_serializing)]
176 pub tax_shipping: Option<bool>,
177
178 #[serde(skip_serializing)]
180 pub transactional_sms_disabled: Option<bool>,
181
182 #[serde(skip_serializing)]
184 pub has_storefront_api: Option<bool>,
185
186 #[serde(skip_serializing)]
188 pub has_discounts: Option<bool>,
189
190 #[serde(skip_serializing)]
192 pub has_gift_cards: Option<bool>,
193
194 #[serde(skip_serializing)]
196 pub eligible_for_payments: Option<bool>,
197
198 #[serde(skip_serializing)]
200 pub requires_extra_payments_agreement: Option<bool>,
201
202 #[serde(skip_serializing)]
204 pub setup_required: Option<bool>,
205
206 #[serde(skip_serializing)]
208 pub pre_launch_enabled: Option<bool>,
209
210 #[serde(skip_serializing)]
212 pub cookie_consent_level: Option<String>,
213
214 #[serde(skip_serializing)]
217 pub shop_owner: Option<String>,
218
219 #[serde(skip_serializing)]
221 pub source: Option<String>,
222
223 #[serde(skip_serializing)]
225 pub weight_unit: Option<String>,
226
227 #[serde(skip_serializing)]
229 pub primary_locale: Option<String>,
230
231 #[serde(skip_serializing)]
233 pub enabled_presentment_currencies: Option<Vec<String>>,
234
235 #[serde(skip_serializing)]
238 pub created_at: Option<DateTime<Utc>>,
239
240 #[serde(skip_serializing)]
242 pub updated_at: Option<DateTime<Utc>>,
243
244 #[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 const PATHS: &'static [ResourcePath] = &[ResourcePath::new(
261 HttpMethod::Get,
262 ResourceOperation::Find,
263 &[],
264 "shop",
265 )];
266
267 fn get_id(&self) -> Option<Self::Id> {
269 None
270 }
271}
272
273impl Shop {
274 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 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 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 }
439
440 #[test]
441 fn test_shop_does_not_have_standard_crud_paths() {
442 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 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 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 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 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 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 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 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 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 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 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 assert!(shop.created_at.is_some());
614 assert!(shop.updated_at.is_some());
615
616 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 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}