use std::collections::HashMap;
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, Eq)]
pub struct InventoryLevel {
#[serde(skip_serializing_if = "Option::is_none")]
pub inventory_item_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub available: Option<i64>,
#[serde(skip_serializing)]
pub updated_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub admin_graphql_api_id: Option<String>,
}
impl InventoryLevel {
pub async fn adjust(
client: &RestClient,
inventory_item_id: u64,
location_id: u64,
available_adjustment: i64,
) -> Result<Self, ResourceError> {
let path = "inventory_levels/adjust";
let body = serde_json::json!({
"inventory_item_id": inventory_item_id,
"location_id": location_id,
"available_adjustment": available_adjustment
});
let response = client.post(path, body, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
None,
response.request_id(),
));
}
let level: Self = response
.body
.get("inventory_level")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'inventory_level' 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 inventory level: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(level)
}
pub async fn connect(
client: &RestClient,
inventory_item_id: u64,
location_id: u64,
relocate_if_necessary: Option<bool>,
) -> Result<Self, ResourceError> {
let path = "inventory_levels/connect";
let mut body = serde_json::json!({
"inventory_item_id": inventory_item_id,
"location_id": location_id
});
if let Some(relocate) = relocate_if_necessary {
body["relocate_if_necessary"] = serde_json::json!(relocate);
}
let response = client.post(path, body, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
None,
response.request_id(),
));
}
let level: Self = response
.body
.get("inventory_level")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'inventory_level' 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 inventory level: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(level)
}
pub async fn set(
client: &RestClient,
inventory_item_id: u64,
location_id: u64,
available: i64,
disconnect_if_necessary: Option<bool>,
) -> Result<Self, ResourceError> {
let path = "inventory_levels/set";
let mut body = serde_json::json!({
"inventory_item_id": inventory_item_id,
"location_id": location_id,
"available": available
});
if let Some(disconnect) = disconnect_if_necessary {
body["disconnect_if_necessary"] = serde_json::json!(disconnect);
}
let response = client.post(path, body, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
None,
response.request_id(),
));
}
let level: Self = response
.body
.get("inventory_level")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'inventory_level' 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 inventory level: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(level)
}
pub async fn delete_at_location(
client: &RestClient,
inventory_item_id: u64,
location_id: u64,
) -> Result<(), ResourceError> {
let path = "inventory_levels";
let mut query = HashMap::new();
query.insert("inventory_item_id".to_string(), inventory_item_id.to_string());
query.insert("location_id".to_string(), location_id.to_string());
let response = client.delete(path, Some(query)).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
None,
response.request_id(),
));
}
Ok(())
}
}
impl RestResource for InventoryLevel {
type Id = String;
type FindParams = ();
type AllParams = InventoryLevelListParams;
type CountParams = ();
const NAME: &'static str = "InventoryLevel";
const PLURAL: &'static str = "inventory_levels";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::All,
&[],
"inventory_levels",
),
];
fn get_id(&self) -> Option<Self::Id> {
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct InventoryLevelListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub inventory_item_ids: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location_ids: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_min: Option<DateTime<Utc>>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_inventory_level_has_no_id_field() {
let level = InventoryLevel {
inventory_item_id: Some(808950810),
location_id: Some(655441491),
available: Some(100),
updated_at: None,
admin_graphql_api_id: None,
};
assert!(level.get_id().is_none());
}
#[test]
fn test_inventory_level_serialization() {
let level = InventoryLevel {
inventory_item_id: Some(808950810),
location_id: Some(655441491),
available: Some(100),
updated_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.unwrap()
.with_timezone(&Utc),
),
admin_graphql_api_id: Some("gid://shopify/InventoryLevel/123".to_string()),
};
let json = serde_json::to_string(&level).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["inventory_item_id"], 808950810);
assert_eq!(parsed["location_id"], 655441491);
assert_eq!(parsed["available"], 100);
assert!(parsed.get("updated_at").is_none());
assert!(parsed.get("admin_graphql_api_id").is_none());
}
#[test]
fn test_inventory_level_deserialization() {
let json = r#"{
"inventory_item_id": 808950810,
"location_id": 655441491,
"available": 42,
"updated_at": "2024-06-20T15:45:00Z",
"admin_graphql_api_id": "gid://shopify/InventoryLevel/808950810?inventory_item_id=808950810"
}"#;
let level: InventoryLevel = serde_json::from_str(json).unwrap();
assert_eq!(level.inventory_item_id, Some(808950810));
assert_eq!(level.location_id, Some(655441491));
assert_eq!(level.available, Some(42));
assert!(level.updated_at.is_some());
assert!(level.admin_graphql_api_id.is_some());
}
#[test]
fn test_inventory_level_special_operations_path_construction() {
assert_eq!(format!("inventory_levels/adjust"), "inventory_levels/adjust");
assert_eq!(
format!("inventory_levels/connect"),
"inventory_levels/connect"
);
assert_eq!(format!("inventory_levels/set"), "inventory_levels/set");
assert_eq!(format!("inventory_levels"), "inventory_levels");
}
#[test]
fn test_inventory_level_list_params_serialization() {
let params = InventoryLevelListParams {
inventory_item_ids: Some("808950810,808950811".to_string()),
location_ids: Some("655441491,655441492".to_string()),
limit: Some(50),
updated_at_min: Some(
DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
),
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["inventory_item_ids"], "808950810,808950811");
assert_eq!(json["location_ids"], "655441491,655441492");
assert_eq!(json["limit"], 50);
assert!(json["updated_at_min"].as_str().is_some());
let empty_params = InventoryLevelListParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_inventory_level_paths() {
let all_path = get_path(InventoryLevel::PATHS, ResourceOperation::All, &[]);
assert!(all_path.is_some());
assert_eq!(all_path.unwrap().template, "inventory_levels");
assert_eq!(all_path.unwrap().http_method, HttpMethod::Get);
assert!(get_path(InventoryLevel::PATHS, ResourceOperation::Find, &["id"]).is_none());
assert!(get_path(InventoryLevel::PATHS, ResourceOperation::Create, &[]).is_none());
assert!(get_path(InventoryLevel::PATHS, ResourceOperation::Update, &["id"]).is_none());
assert!(get_path(InventoryLevel::PATHS, ResourceOperation::Delete, &["id"]).is_none());
assert!(get_path(InventoryLevel::PATHS, ResourceOperation::Count, &[]).is_none());
}
#[test]
fn test_inventory_level_constants() {
assert_eq!(InventoryLevel::NAME, "InventoryLevel");
assert_eq!(InventoryLevel::PLURAL, "inventory_levels");
}
}