use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::clients::RestClient;
use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
use crate::HttpMethod;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Shop {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing)]
pub name: Option<String>,
#[serde(skip_serializing)]
pub email: Option<String>,
#[serde(skip_serializing)]
pub domain: Option<String>,
#[serde(skip_serializing)]
pub myshopify_domain: Option<String>,
#[serde(skip_serializing)]
pub plan_name: Option<String>,
#[serde(skip_serializing)]
pub plan_display_name: Option<String>,
#[serde(skip_serializing)]
pub password_enabled: Option<bool>,
#[serde(skip_serializing)]
pub address1: Option<String>,
#[serde(skip_serializing)]
pub address2: Option<String>,
#[serde(skip_serializing)]
pub city: Option<String>,
#[serde(skip_serializing)]
pub province: Option<String>,
#[serde(skip_serializing)]
pub province_code: Option<String>,
#[serde(skip_serializing)]
pub country: Option<String>,
#[serde(skip_serializing)]
pub country_code: Option<String>,
#[serde(skip_serializing)]
pub country_name: Option<String>,
#[serde(skip_serializing)]
pub zip: Option<String>,
#[serde(skip_serializing)]
pub phone: Option<String>,
#[serde(skip_serializing)]
pub latitude: Option<f64>,
#[serde(skip_serializing)]
pub longitude: Option<f64>,
#[serde(skip_serializing)]
pub currency: Option<String>,
#[serde(skip_serializing)]
pub money_format: Option<String>,
#[serde(skip_serializing)]
pub money_with_currency_format: Option<String>,
#[serde(skip_serializing)]
pub timezone: Option<String>,
#[serde(skip_serializing)]
pub iana_timezone: Option<String>,
#[serde(skip_serializing)]
pub checkout_api_supported: Option<bool>,
#[serde(skip_serializing)]
pub multi_location_enabled: Option<bool>,
#[serde(skip_serializing)]
pub taxes_included: Option<bool>,
#[serde(skip_serializing)]
pub tax_shipping: Option<bool>,
#[serde(skip_serializing)]
pub transactional_sms_disabled: Option<bool>,
#[serde(skip_serializing)]
pub has_storefront_api: Option<bool>,
#[serde(skip_serializing)]
pub has_discounts: Option<bool>,
#[serde(skip_serializing)]
pub has_gift_cards: Option<bool>,
#[serde(skip_serializing)]
pub eligible_for_payments: Option<bool>,
#[serde(skip_serializing)]
pub requires_extra_payments_agreement: Option<bool>,
#[serde(skip_serializing)]
pub setup_required: Option<bool>,
#[serde(skip_serializing)]
pub pre_launch_enabled: Option<bool>,
#[serde(skip_serializing)]
pub cookie_consent_level: Option<String>,
#[serde(skip_serializing)]
pub shop_owner: Option<String>,
#[serde(skip_serializing)]
pub source: Option<String>,
#[serde(skip_serializing)]
pub weight_unit: Option<String>,
#[serde(skip_serializing)]
pub primary_locale: Option<String>,
#[serde(skip_serializing)]
pub enabled_presentment_currencies: Option<Vec<String>>,
#[serde(skip_serializing)]
pub created_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub updated_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub admin_graphql_api_id: Option<String>,
}
impl RestResource for Shop {
type Id = u64;
type FindParams = ();
type AllParams = ();
type CountParams = ();
const NAME: &'static str = "Shop";
const PLURAL: &'static str = "shop";
const PATHS: &'static [ResourcePath] = &[ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&[],
"shop",
)];
fn get_id(&self) -> Option<Self::Id> {
None
}
}
impl Shop {
pub async fn current(client: &RestClient) -> Result<Self, ResourceError> {
let path = "shop";
let response = client.get(path, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
None,
response.request_id(),
));
}
let shop: Self = response
.body
.get("shop")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'shop' in response".to_string(),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
.and_then(|v| {
serde_json::from_value(v.clone()).map_err(|e| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: format!("Failed to deserialize shop: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(shop)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_shop_struct_deserialization_from_api_response() {
let json_str = r#"{
"id": 548380009,
"name": "John Smith Test Store",
"email": "j.smith@example.com",
"domain": "shop.example.com",
"myshopify_domain": "john-smith-test-store.myshopify.com",
"plan_name": "partner_test",
"plan_display_name": "Partner Test",
"password_enabled": false,
"address1": "123 Main St",
"address2": "Suite 100",
"city": "Ottawa",
"province": "Ontario",
"province_code": "ON",
"country": "Canada",
"country_code": "CA",
"country_name": "Canada",
"zip": "K1A 0B1",
"phone": "1234567890",
"latitude": 45.4215,
"longitude": -75.6972,
"currency": "CAD",
"money_format": "${{amount}}",
"money_with_currency_format": "${{amount}} CAD",
"timezone": "(GMT-05:00) Eastern Time (US & Canada)",
"iana_timezone": "America/Toronto",
"checkout_api_supported": true,
"multi_location_enabled": true,
"taxes_included": false,
"tax_shipping": null,
"weight_unit": "kg",
"primary_locale": "en",
"created_at": "2023-01-01T12:00:00-05:00",
"updated_at": "2024-06-01T12:00:00-05:00"
}"#;
let shop: Shop = serde_json::from_str(json_str).unwrap();
assert_eq!(shop.id, Some(548380009));
assert_eq!(shop.name.as_deref(), Some("John Smith Test Store"));
assert_eq!(shop.email.as_deref(), Some("j.smith@example.com"));
assert_eq!(shop.domain.as_deref(), Some("shop.example.com"));
assert_eq!(
shop.myshopify_domain.as_deref(),
Some("john-smith-test-store.myshopify.com")
);
assert_eq!(shop.plan_name.as_deref(), Some("partner_test"));
assert_eq!(shop.plan_display_name.as_deref(), Some("Partner Test"));
assert_eq!(shop.password_enabled, Some(false));
assert_eq!(shop.address1.as_deref(), Some("123 Main St"));
assert_eq!(shop.address2.as_deref(), Some("Suite 100"));
assert_eq!(shop.city.as_deref(), Some("Ottawa"));
assert_eq!(shop.province.as_deref(), Some("Ontario"));
assert_eq!(shop.province_code.as_deref(), Some("ON"));
assert_eq!(shop.country.as_deref(), Some("Canada"));
assert_eq!(shop.country_code.as_deref(), Some("CA"));
assert_eq!(shop.zip.as_deref(), Some("K1A 0B1"));
assert_eq!(shop.currency.as_deref(), Some("CAD"));
assert_eq!(
shop.timezone.as_deref(),
Some("(GMT-05:00) Eastern Time (US & Canada)")
);
assert_eq!(shop.iana_timezone.as_deref(), Some("America/Toronto"));
assert_eq!(shop.checkout_api_supported, Some(true));
assert_eq!(shop.multi_location_enabled, Some(true));
assert_eq!(shop.taxes_included, Some(false));
assert_eq!(shop.weight_unit.as_deref(), Some("kg"));
assert_eq!(shop.primary_locale.as_deref(), Some("en"));
assert!(shop.created_at.is_some());
assert!(shop.updated_at.is_some());
}
#[test]
fn test_shop_current_method_signature_exists() {
fn _assert_current_signature<F, Fut>(f: F)
where
F: Fn(&RestClient) -> Fut,
Fut: std::future::Future<Output = Result<Shop, ResourceError>>,
{
let _ = f;
}
}
#[test]
fn test_shop_does_not_have_standard_crud_paths() {
let find_path = get_path(Shop::PATHS, ResourceOperation::Find, &[]);
assert!(find_path.is_some());
assert_eq!(find_path.unwrap().template, "shop");
let create_path = get_path(Shop::PATHS, ResourceOperation::Create, &[]);
assert!(create_path.is_none());
let update_path = get_path(Shop::PATHS, ResourceOperation::Update, &["id"]);
assert!(update_path.is_none());
let delete_path = get_path(Shop::PATHS, ResourceOperation::Delete, &["id"]);
assert!(delete_path.is_none());
let all_path = get_path(Shop::PATHS, ResourceOperation::All, &[]);
assert!(all_path.is_none());
let count_path = get_path(Shop::PATHS, ResourceOperation::Count, &[]);
assert!(count_path.is_none());
}
#[test]
fn test_shop_read_only_fields_are_not_serialized() {
let shop = Shop {
id: Some(548380009),
name: Some("Test Store".to_string()),
email: Some("test@example.com".to_string()),
domain: Some("shop.example.com".to_string()),
myshopify_domain: Some("test-store.myshopify.com".to_string()),
plan_name: Some("basic".to_string()),
currency: Some("USD".to_string()),
timezone: Some("(GMT-05:00) Eastern Time".to_string()),
..Default::default()
};
let json = serde_json::to_string(&shop).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(
parsed.as_object().unwrap().is_empty(),
"Shop serialization should produce empty object since all fields have skip_serializing"
);
}
#[test]
fn test_shop_all_major_fields_are_captured() {
let json_str = r#"{
"id": 548380009,
"name": "Complete Test Store",
"email": "complete@example.com",
"domain": "complete.example.com",
"myshopify_domain": "complete-test.myshopify.com",
"plan_name": "unlimited",
"plan_display_name": "Shopify Plus",
"password_enabled": true,
"address1": "123 Commerce St",
"address2": "Floor 2",
"city": "New York",
"province": "New York",
"province_code": "NY",
"country": "United States",
"country_code": "US",
"country_name": "United States",
"zip": "10001",
"phone": "+1-555-555-5555",
"latitude": 40.7128,
"longitude": -74.0060,
"currency": "USD",
"money_format": "${{amount}}",
"money_with_currency_format": "${{amount}} USD",
"timezone": "(GMT-05:00) Eastern Time (US & Canada)",
"iana_timezone": "America/New_York",
"checkout_api_supported": true,
"multi_location_enabled": true,
"taxes_included": false,
"tax_shipping": true,
"transactional_sms_disabled": false,
"has_storefront_api": true,
"has_discounts": true,
"has_gift_cards": true,
"eligible_for_payments": true,
"requires_extra_payments_agreement": false,
"setup_required": false,
"pre_launch_enabled": false,
"cookie_consent_level": "implicit",
"shop_owner": "John Smith",
"source": null,
"weight_unit": "lb",
"primary_locale": "en",
"enabled_presentment_currencies": ["USD", "CAD", "EUR"],
"created_at": "2022-01-15T10:30:00-05:00",
"updated_at": "2024-11-20T14:45:00-05:00",
"admin_graphql_api_id": "gid://shopify/Shop/548380009"
}"#;
let shop: Shop = serde_json::from_str(json_str).unwrap();
assert_eq!(shop.id, Some(548380009));
assert_eq!(shop.name.as_deref(), Some("Complete Test Store"));
assert_eq!(shop.email.as_deref(), Some("complete@example.com"));
assert_eq!(shop.domain.as_deref(), Some("complete.example.com"));
assert_eq!(
shop.myshopify_domain.as_deref(),
Some("complete-test.myshopify.com")
);
assert_eq!(shop.plan_name.as_deref(), Some("unlimited"));
assert_eq!(shop.plan_display_name.as_deref(), Some("Shopify Plus"));
assert_eq!(shop.password_enabled, Some(true));
assert_eq!(shop.address1.as_deref(), Some("123 Commerce St"));
assert_eq!(shop.address2.as_deref(), Some("Floor 2"));
assert_eq!(shop.city.as_deref(), Some("New York"));
assert_eq!(shop.province.as_deref(), Some("New York"));
assert_eq!(shop.province_code.as_deref(), Some("NY"));
assert_eq!(shop.country.as_deref(), Some("United States"));
assert_eq!(shop.country_code.as_deref(), Some("US"));
assert_eq!(shop.country_name.as_deref(), Some("United States"));
assert_eq!(shop.zip.as_deref(), Some("10001"));
assert_eq!(shop.phone.as_deref(), Some("+1-555-555-5555"));
assert_eq!(shop.latitude, Some(40.7128));
assert_eq!(shop.longitude, Some(-74.0060));
assert_eq!(shop.currency.as_deref(), Some("USD"));
assert_eq!(shop.money_format.as_deref(), Some("${{amount}}"));
assert_eq!(
shop.money_with_currency_format.as_deref(),
Some("${{amount}} USD")
);
assert_eq!(
shop.timezone.as_deref(),
Some("(GMT-05:00) Eastern Time (US & Canada)")
);
assert_eq!(shop.iana_timezone.as_deref(), Some("America/New_York"));
assert_eq!(shop.checkout_api_supported, Some(true));
assert_eq!(shop.multi_location_enabled, Some(true));
assert_eq!(shop.taxes_included, Some(false));
assert_eq!(shop.tax_shipping, Some(true));
assert_eq!(shop.has_storefront_api, Some(true));
assert_eq!(shop.has_discounts, Some(true));
assert_eq!(shop.has_gift_cards, Some(true));
assert_eq!(shop.eligible_for_payments, Some(true));
assert_eq!(shop.setup_required, Some(false));
assert_eq!(shop.pre_launch_enabled, Some(false));
assert_eq!(shop.shop_owner.as_deref(), Some("John Smith"));
assert_eq!(shop.weight_unit.as_deref(), Some("lb"));
assert_eq!(shop.primary_locale.as_deref(), Some("en"));
assert_eq!(
shop.enabled_presentment_currencies,
Some(vec![
"USD".to_string(),
"CAD".to_string(),
"EUR".to_string()
])
);
assert!(shop.created_at.is_some());
assert!(shop.updated_at.is_some());
assert_eq!(
shop.admin_graphql_api_id.as_deref(),
Some("gid://shopify/Shop/548380009")
);
}
#[test]
fn test_shop_get_id_returns_none_for_singleton() {
let shop = Shop {
id: Some(548380009),
name: Some("Test Store".to_string()),
..Default::default()
};
assert!(shop.get_id().is_none());
}
#[test]
fn test_shop_resource_constants() {
assert_eq!(Shop::NAME, "Shop");
assert_eq!(Shop::PLURAL, "shop");
assert_eq!(Shop::PATHS.len(), 1);
}
}