1use std::collections::HashMap;
38
39use chrono::{DateTime, Utc};
40use serde::{Deserialize, Serialize};
41
42use crate::clients::RestClient;
43use crate::rest::{ReadOnlyResource, ResourceError, ResourceOperation, ResourcePath, RestResource};
44use crate::HttpMethod;
45
46#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
73pub struct Location {
74 #[serde(skip_serializing)]
76 pub id: Option<u64>,
77
78 #[serde(skip_serializing)]
80 pub name: Option<String>,
81
82 #[serde(skip_serializing)]
84 pub address1: Option<String>,
85
86 #[serde(skip_serializing)]
88 pub address2: Option<String>,
89
90 #[serde(skip_serializing)]
92 pub city: Option<String>,
93
94 #[serde(skip_serializing)]
96 pub province: Option<String>,
97
98 #[serde(skip_serializing)]
100 pub province_code: Option<String>,
101
102 #[serde(skip_serializing)]
104 pub country: Option<String>,
105
106 #[serde(skip_serializing)]
108 pub country_code: Option<String>,
109
110 #[serde(skip_serializing)]
112 pub localized_country_name: Option<String>,
113
114 #[serde(skip_serializing)]
116 pub localized_province_name: Option<String>,
117
118 #[serde(skip_serializing)]
120 pub zip: Option<String>,
121
122 #[serde(skip_serializing)]
124 pub phone: Option<String>,
125
126 #[serde(skip_serializing)]
128 pub active: Option<bool>,
129
130 #[serde(skip_serializing)]
132 pub legacy: Option<bool>,
133
134 #[serde(skip_serializing)]
136 pub created_at: Option<DateTime<Utc>>,
137
138 #[serde(skip_serializing)]
140 pub updated_at: Option<DateTime<Utc>>,
141
142 #[serde(skip_serializing)]
144 pub admin_graphql_api_id: Option<String>,
145}
146
147impl Location {
148 pub async fn inventory_levels(
175 &self,
176 client: &RestClient,
177 params: Option<LocationInventoryLevelsParams>,
178 ) -> Result<Vec<super::InventoryLevel>, ResourceError> {
179 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
180 resource: Self::NAME,
181 operation: "inventory_levels",
182 })?;
183
184 let path = format!("locations/{id}/inventory_levels");
185
186 let query = params
188 .map(|p| {
189 let value = serde_json::to_value(&p).map_err(|e| {
190 ResourceError::Http(crate::clients::HttpError::Response(
191 crate::clients::HttpResponseError {
192 code: 400,
193 message: format!("Failed to serialize params: {e}"),
194 error_reference: None,
195 },
196 ))
197 })?;
198
199 let mut query = HashMap::new();
200 if let serde_json::Value::Object(map) = value {
201 for (key, val) in map {
202 match val {
203 serde_json::Value::String(s) => {
204 query.insert(key, s);
205 }
206 serde_json::Value::Number(n) => {
207 query.insert(key, n.to_string());
208 }
209 serde_json::Value::Bool(b) => {
210 query.insert(key, b.to_string());
211 }
212 _ => {}
213 }
214 }
215 }
216 Ok::<_, ResourceError>(query)
217 })
218 .transpose()?
219 .filter(|q| !q.is_empty());
220
221 let response = client.get(&path, query).await?;
222
223 if !response.is_ok() {
224 return Err(ResourceError::from_http_response(
225 response.code,
226 &response.body,
227 Self::NAME,
228 Some(&id.to_string()),
229 response.request_id(),
230 ));
231 }
232
233 let levels: Vec<super::InventoryLevel> = response
235 .body
236 .get("inventory_levels")
237 .ok_or_else(|| {
238 ResourceError::Http(crate::clients::HttpError::Response(
239 crate::clients::HttpResponseError {
240 code: response.code,
241 message: "Missing 'inventory_levels' in response".to_string(),
242 error_reference: response.request_id().map(ToString::to_string),
243 },
244 ))
245 })
246 .and_then(|v| {
247 serde_json::from_value(v.clone()).map_err(|e| {
248 ResourceError::Http(crate::clients::HttpError::Response(
249 crate::clients::HttpResponseError {
250 code: response.code,
251 message: format!("Failed to deserialize inventory levels: {e}"),
252 error_reference: response.request_id().map(ToString::to_string),
253 },
254 ))
255 })
256 })?;
257
258 Ok(levels)
259 }
260}
261
262impl RestResource for Location {
263 type Id = u64;
264 type FindParams = LocationFindParams;
265 type AllParams = LocationListParams;
266 type CountParams = LocationCountParams;
267
268 const NAME: &'static str = "Location";
269 const PLURAL: &'static str = "locations";
270
271 const PATHS: &'static [ResourcePath] = &[
280 ResourcePath::new(
281 HttpMethod::Get,
282 ResourceOperation::Find,
283 &["id"],
284 "locations/{id}",
285 ),
286 ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "locations"),
287 ResourcePath::new(
288 HttpMethod::Get,
289 ResourceOperation::Count,
290 &[],
291 "locations/count",
292 ),
293 ];
295
296 fn get_id(&self) -> Option<Self::Id> {
297 self.id
298 }
299}
300
301impl ReadOnlyResource for Location {}
303
304#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
306pub struct LocationFindParams {
307 }
309
310#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
312pub struct LocationListParams {
313 }
315
316#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
318pub struct LocationCountParams {
319 }
321
322#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
324pub struct LocationInventoryLevelsParams {
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub limit: Option<u32>,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub page_info: Option<String>,
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use crate::rest::{get_path, ResourceOperation};
338
339 #[test]
340 fn test_location_implements_read_only_resource() {
341 fn assert_read_only<T: ReadOnlyResource>() {}
343 assert_read_only::<Location>();
344 }
345
346 #[test]
347 fn test_location_has_only_get_paths() {
348 assert!(get_path(Location::PATHS, ResourceOperation::Find, &["id"]).is_some());
350 assert!(get_path(Location::PATHS, ResourceOperation::All, &[]).is_some());
351 assert!(get_path(Location::PATHS, ResourceOperation::Count, &[]).is_some());
352
353 assert!(get_path(Location::PATHS, ResourceOperation::Create, &[]).is_none());
355 assert!(get_path(Location::PATHS, ResourceOperation::Update, &["id"]).is_none());
356 assert!(get_path(Location::PATHS, ResourceOperation::Delete, &["id"]).is_none());
357 }
358
359 #[test]
360 fn test_location_deserialization() {
361 let json = r#"{
362 "id": 655441491,
363 "name": "Main Warehouse",
364 "address1": "123 Main St",
365 "address2": "Suite 100",
366 "city": "New York",
367 "province": "New York",
368 "province_code": "NY",
369 "country": "United States",
370 "country_code": "US",
371 "localized_country_name": "United States",
372 "localized_province_name": "New York",
373 "zip": "10001",
374 "phone": "555-555-5555",
375 "active": true,
376 "legacy": false,
377 "created_at": "2024-01-15T10:30:00Z",
378 "updated_at": "2024-06-20T15:45:00Z",
379 "admin_graphql_api_id": "gid://shopify/Location/655441491"
380 }"#;
381
382 let location: Location = serde_json::from_str(json).unwrap();
383
384 assert_eq!(location.id, Some(655441491));
385 assert_eq!(location.name, Some("Main Warehouse".to_string()));
386 assert_eq!(location.address1, Some("123 Main St".to_string()));
387 assert_eq!(location.address2, Some("Suite 100".to_string()));
388 assert_eq!(location.city, Some("New York".to_string()));
389 assert_eq!(location.province, Some("New York".to_string()));
390 assert_eq!(location.province_code, Some("NY".to_string()));
391 assert_eq!(location.country, Some("United States".to_string()));
392 assert_eq!(location.country_code, Some("US".to_string()));
393 assert_eq!(
394 location.localized_country_name,
395 Some("United States".to_string())
396 );
397 assert_eq!(
398 location.localized_province_name,
399 Some("New York".to_string())
400 );
401 assert_eq!(location.zip, Some("10001".to_string()));
402 assert_eq!(location.phone, Some("555-555-5555".to_string()));
403 assert_eq!(location.active, Some(true));
404 assert_eq!(location.legacy, Some(false));
405 assert!(location.created_at.is_some());
406 assert!(location.updated_at.is_some());
407 assert_eq!(
408 location.admin_graphql_api_id,
409 Some("gid://shopify/Location/655441491".to_string())
410 );
411 }
412
413 #[test]
414 fn test_location_serialization_is_empty() {
415 let location = Location {
417 id: Some(655441491),
418 name: Some("Main Warehouse".to_string()),
419 city: Some("New York".to_string()),
420 active: Some(true),
421 ..Default::default()
422 };
423
424 let json = serde_json::to_string(&location).unwrap();
425 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
426
427 assert_eq!(parsed, serde_json::json!({}));
429 }
430
431 #[test]
432 fn test_location_get_id_returns_correct_value() {
433 let location_with_id = Location {
434 id: Some(655441491),
435 name: Some("Warehouse".to_string()),
436 ..Default::default()
437 };
438 assert_eq!(location_with_id.get_id(), Some(655441491));
439
440 let location_without_id = Location {
441 id: None,
442 name: Some("New Location".to_string()),
443 ..Default::default()
444 };
445 assert_eq!(location_without_id.get_id(), None);
446 }
447
448 #[test]
449 fn test_location_path_constants() {
450 let find_path = get_path(Location::PATHS, ResourceOperation::Find, &["id"]);
451 assert!(find_path.is_some());
452 assert_eq!(find_path.unwrap().template, "locations/{id}");
453 assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
454
455 let all_path = get_path(Location::PATHS, ResourceOperation::All, &[]);
456 assert!(all_path.is_some());
457 assert_eq!(all_path.unwrap().template, "locations");
458
459 let count_path = get_path(Location::PATHS, ResourceOperation::Count, &[]);
460 assert!(count_path.is_some());
461 assert_eq!(count_path.unwrap().template, "locations/count");
462 }
463
464 #[test]
465 fn test_location_constants() {
466 assert_eq!(Location::NAME, "Location");
467 assert_eq!(Location::PLURAL, "locations");
468 }
469}