use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::clients::RestClient;
use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
use crate::HttpMethod;
use super::common::Address;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum FulfillmentStatus {
#[default]
Pending,
Open,
Success,
Cancelled,
Error,
Failure,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ShipmentStatus {
LabelPrinted,
LabelPurchased,
AttemptedDelivery,
ReadyForPickup,
Confirmed,
InTransit,
OutForDelivery,
Delivered,
Failure,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct FulfillmentLineItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variant_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quantity: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sku: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variant_title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vendor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fulfillment_service: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub product_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_shipping: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub taxable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gift_card: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variant_inventory_management: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub product_exists: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_discount: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fulfillable_quantity: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fulfillment_status: Option<String>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct TrackingInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub tracking_number: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tracking_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tracking_company: Option<String>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Fulfillment {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing)]
pub order_id: Option<u64>,
#[serde(skip_serializing)]
pub name: Option<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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<FulfillmentStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shipment_status: Option<ShipmentStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tracking_company: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tracking_number: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tracking_numbers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tracking_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tracking_urls: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin_address: Option<Address>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line_items: Option<Vec<FulfillmentLineItem>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notify_customer: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variant_inventory_management: Option<String>,
}
impl RestResource for Fulfillment {
type Id = u64;
type FindParams = FulfillmentFindParams;
type AllParams = FulfillmentListParams;
type CountParams = FulfillmentCountParams;
const NAME: &'static str = "Fulfillment";
const PLURAL: &'static str = "fulfillments";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["order_id", "id"],
"orders/{order_id}/fulfillments/{id}",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::All,
&["order_id"],
"orders/{order_id}/fulfillments",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Count,
&["order_id"],
"orders/{order_id}/fulfillments/count",
),
ResourcePath::new(
HttpMethod::Post,
ResourceOperation::Create,
&["order_id"],
"orders/{order_id}/fulfillments",
),
ResourcePath::new(
HttpMethod::Put,
ResourceOperation::Update,
&["order_id", "id"],
"orders/{order_id}/fulfillments/{id}",
),
];
fn get_id(&self) -> Option<Self::Id> {
self.id
}
}
impl Fulfillment {
pub async fn cancel(&self, client: &RestClient) -> Result<Self, ResourceError> {
let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
resource: Self::NAME,
operation: "cancel",
})?;
let order_id = self.order_id.ok_or(ResourceError::PathResolutionFailed {
resource: Self::NAME,
operation: "cancel",
})?;
let path = format!("orders/{order_id}/fulfillments/{id}/cancel");
let body = serde_json::json!({});
let response = client.post(&path, body, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
Some(&id.to_string()),
response.request_id(),
));
}
let fulfillment: Self = response
.body
.get("fulfillment")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'fulfillment' 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 fulfillment: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(fulfillment)
}
pub async fn update_tracking(
&self,
client: &RestClient,
tracking_info: TrackingInfo,
) -> Result<Self, ResourceError> {
let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
resource: Self::NAME,
operation: "update_tracking",
})?;
let path = format!("fulfillments/{id}/update_tracking");
let body = serde_json::json!({
"fulfillment": {
"tracking_info": tracking_info
}
});
let response = client.post(&path, body, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
Some(&id.to_string()),
response.request_id(),
));
}
let fulfillment: Self = response
.body
.get("fulfillment")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'fulfillment' 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 fulfillment: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(fulfillment)
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct FulfillmentFindParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct FulfillmentListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub since_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_info: Option<String>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct FulfillmentCountParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_max: Option<DateTime<Utc>>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_fulfillment_struct_serialization() {
let fulfillment = Fulfillment {
id: Some(123456789),
order_id: Some(987654321),
name: Some("#1001.1".to_string()),
status: Some(FulfillmentStatus::Success),
service: Some("manual".to_string()),
location_id: Some(111222333),
shipment_status: Some(ShipmentStatus::Delivered),
tracking_company: Some("UPS".to_string()),
tracking_number: Some("1Z999AA10123456784".to_string()),
tracking_numbers: Some(vec!["1Z999AA10123456784".to_string()]),
tracking_url: Some("https://ups.com/tracking/1Z999AA10123456784".to_string()),
tracking_urls: Some(vec![
"https://ups.com/tracking/1Z999AA10123456784".to_string()
]),
notify_customer: Some(true),
..Default::default()
};
let json = serde_json::to_string(&fulfillment).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["status"], "success");
assert_eq!(parsed["service"], "manual");
assert_eq!(parsed["location_id"], 111222333);
assert_eq!(parsed["shipment_status"], "delivered");
assert_eq!(parsed["tracking_company"], "UPS");
assert_eq!(parsed["tracking_number"], "1Z999AA10123456784");
assert_eq!(parsed["notify_customer"], true);
assert!(parsed.get("id").is_none());
assert!(parsed.get("order_id").is_none());
assert!(parsed.get("name").is_none());
assert!(parsed.get("created_at").is_none());
assert!(parsed.get("updated_at").is_none());
assert!(parsed.get("admin_graphql_api_id").is_none());
}
#[test]
fn test_fulfillment_deserialization_from_api_response() {
let json_str = r##"{
"id": 255858046,
"order_id": 450789469,
"name": "#1001.1",
"status": "success",
"service": "manual",
"location_id": 487838322,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-06-20T15:45:00Z",
"shipment_status": "in_transit",
"tracking_company": "USPS",
"tracking_number": "9400111899223456789012",
"tracking_numbers": ["9400111899223456789012"],
"tracking_url": "https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=9400111899223456789012",
"tracking_urls": ["https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=9400111899223456789012"],
"line_items": [
{
"id": 669751112,
"variant_id": 457924702,
"product_id": 632910392,
"title": "IPod Nano - 8GB",
"quantity": 1,
"sku": "IPOD2008BLACK",
"requires_shipping": true
}
],
"admin_graphql_api_id": "gid://shopify/Fulfillment/255858046"
}"##;
let fulfillment: Fulfillment = serde_json::from_str(json_str).unwrap();
assert_eq!(fulfillment.id, Some(255858046));
assert_eq!(fulfillment.order_id, Some(450789469));
assert_eq!(fulfillment.name.as_deref(), Some("#1001.1"));
assert_eq!(fulfillment.status, Some(FulfillmentStatus::Success));
assert_eq!(fulfillment.service.as_deref(), Some("manual"));
assert_eq!(fulfillment.location_id, Some(487838322));
assert_eq!(fulfillment.shipment_status, Some(ShipmentStatus::InTransit));
assert_eq!(fulfillment.tracking_company.as_deref(), Some("USPS"));
assert_eq!(
fulfillment.tracking_number.as_deref(),
Some("9400111899223456789012")
);
assert!(fulfillment.created_at.is_some());
assert!(fulfillment.updated_at.is_some());
let line_items = fulfillment.line_items.unwrap();
assert_eq!(line_items.len(), 1);
assert_eq!(line_items[0].id, Some(669751112));
assert_eq!(line_items[0].title.as_deref(), Some("IPod Nano - 8GB"));
assert_eq!(line_items[0].quantity, Some(1));
}
#[test]
fn test_fulfillment_status_enum_serialization() {
let pending_str = serde_json::to_string(&FulfillmentStatus::Pending).unwrap();
assert_eq!(pending_str, "\"pending\"");
let open_str = serde_json::to_string(&FulfillmentStatus::Open).unwrap();
assert_eq!(open_str, "\"open\"");
let success_str = serde_json::to_string(&FulfillmentStatus::Success).unwrap();
assert_eq!(success_str, "\"success\"");
let cancelled_str = serde_json::to_string(&FulfillmentStatus::Cancelled).unwrap();
assert_eq!(cancelled_str, "\"cancelled\"");
let error_str = serde_json::to_string(&FulfillmentStatus::Error).unwrap();
assert_eq!(error_str, "\"error\"");
let failure_str = serde_json::to_string(&FulfillmentStatus::Failure).unwrap();
assert_eq!(failure_str, "\"failure\"");
let success: FulfillmentStatus = serde_json::from_str("\"success\"").unwrap();
let cancelled: FulfillmentStatus = serde_json::from_str("\"cancelled\"").unwrap();
assert_eq!(success, FulfillmentStatus::Success);
assert_eq!(cancelled, FulfillmentStatus::Cancelled);
assert_eq!(FulfillmentStatus::default(), FulfillmentStatus::Pending);
}
#[test]
fn test_shipment_status_enum_serialization() {
let label_printed = serde_json::to_string(&ShipmentStatus::LabelPrinted).unwrap();
assert_eq!(label_printed, "\"label_printed\"");
let label_purchased = serde_json::to_string(&ShipmentStatus::LabelPurchased).unwrap();
assert_eq!(label_purchased, "\"label_purchased\"");
let attempted = serde_json::to_string(&ShipmentStatus::AttemptedDelivery).unwrap();
assert_eq!(attempted, "\"attempted_delivery\"");
let ready = serde_json::to_string(&ShipmentStatus::ReadyForPickup).unwrap();
assert_eq!(ready, "\"ready_for_pickup\"");
let confirmed = serde_json::to_string(&ShipmentStatus::Confirmed).unwrap();
assert_eq!(confirmed, "\"confirmed\"");
let in_transit = serde_json::to_string(&ShipmentStatus::InTransit).unwrap();
assert_eq!(in_transit, "\"in_transit\"");
let out_for_delivery = serde_json::to_string(&ShipmentStatus::OutForDelivery).unwrap();
assert_eq!(out_for_delivery, "\"out_for_delivery\"");
let delivered = serde_json::to_string(&ShipmentStatus::Delivered).unwrap();
assert_eq!(delivered, "\"delivered\"");
let failure = serde_json::to_string(&ShipmentStatus::Failure).unwrap();
assert_eq!(failure, "\"failure\"");
let in_transit_val: ShipmentStatus = serde_json::from_str("\"in_transit\"").unwrap();
let delivered_val: ShipmentStatus = serde_json::from_str("\"delivered\"").unwrap();
let out_for_delivery_val: ShipmentStatus =
serde_json::from_str("\"out_for_delivery\"").unwrap();
assert_eq!(in_transit_val, ShipmentStatus::InTransit);
assert_eq!(delivered_val, ShipmentStatus::Delivered);
assert_eq!(out_for_delivery_val, ShipmentStatus::OutForDelivery);
}
#[test]
fn test_nested_path_under_orders() {
let find_path = get_path(
Fulfillment::PATHS,
ResourceOperation::Find,
&["order_id", "id"],
);
assert!(find_path.is_some());
assert_eq!(
find_path.unwrap().template,
"orders/{order_id}/fulfillments/{id}"
);
assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
let all_path = get_path(Fulfillment::PATHS, ResourceOperation::All, &["order_id"]);
assert!(all_path.is_some());
assert_eq!(all_path.unwrap().template, "orders/{order_id}/fulfillments");
let count_path = get_path(Fulfillment::PATHS, ResourceOperation::Count, &["order_id"]);
assert!(count_path.is_some());
assert_eq!(
count_path.unwrap().template,
"orders/{order_id}/fulfillments/count"
);
let create_path = get_path(Fulfillment::PATHS, ResourceOperation::Create, &["order_id"]);
assert!(create_path.is_some());
assert_eq!(
create_path.unwrap().template,
"orders/{order_id}/fulfillments"
);
assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
let update_path = get_path(
Fulfillment::PATHS,
ResourceOperation::Update,
&["order_id", "id"],
);
assert!(update_path.is_some());
assert_eq!(
update_path.unwrap().template,
"orders/{order_id}/fulfillments/{id}"
);
assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
let no_order_all = get_path(Fulfillment::PATHS, ResourceOperation::All, &[]);
assert!(no_order_all.is_none());
assert_eq!(Fulfillment::NAME, "Fulfillment");
assert_eq!(Fulfillment::PLURAL, "fulfillments");
}
#[test]
fn test_resource_specific_operations_signatures() {
fn _assert_cancel_signature<F, Fut>(f: F)
where
F: Fn(&Fulfillment, &RestClient) -> Fut,
Fut: std::future::Future<Output = Result<Fulfillment, ResourceError>>,
{
let _ = f;
}
fn _assert_update_tracking_signature<F, Fut>(f: F)
where
F: Fn(&Fulfillment, &RestClient, TrackingInfo) -> Fut,
Fut: std::future::Future<Output = Result<Fulfillment, ResourceError>>,
{
let _ = f;
}
let fulfillment_without_id = Fulfillment::default();
assert!(fulfillment_without_id.get_id().is_none());
let tracking = TrackingInfo {
tracking_number: Some("1Z999AA10123456784".to_string()),
tracking_url: Some("https://ups.com/tracking".to_string()),
tracking_company: Some("UPS".to_string()),
};
assert_eq!(
tracking.tracking_number,
Some("1Z999AA10123456784".to_string())
);
let fulfillment_with_id = Fulfillment {
id: Some(255858046),
order_id: Some(450789469),
..Default::default()
};
assert_eq!(fulfillment_with_id.get_id(), Some(255858046));
}
#[test]
fn test_fulfillment_list_params_serialization() {
let created_at_min = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc);
let params = FulfillmentListParams {
limit: Some(50),
since_id: Some(12345),
created_at_min: Some(created_at_min),
created_at_max: None,
updated_at_min: None,
updated_at_max: None,
fields: Some("id,status,tracking_number".to_string()),
page_info: None,
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["limit"], 50);
assert_eq!(json["since_id"], 12345);
assert_eq!(json["fields"], "id,status,tracking_number");
assert!(json["created_at_min"].as_str().is_some());
assert!(json.get("created_at_max").is_none());
assert!(json.get("updated_at_min").is_none());
assert!(json.get("page_info").is_none());
let empty_params = FulfillmentListParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_tracking_info_serialization() {
let tracking = TrackingInfo {
tracking_number: Some("1Z999AA10123456784".to_string()),
tracking_url: Some("https://ups.com/tracking/1Z999AA10123456784".to_string()),
tracking_company: Some("UPS".to_string()),
};
let json = serde_json::to_string(&tracking).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["tracking_number"], "1Z999AA10123456784");
assert_eq!(
parsed["tracking_url"],
"https://ups.com/tracking/1Z999AA10123456784"
);
assert_eq!(parsed["tracking_company"], "UPS");
let partial_tracking = TrackingInfo {
tracking_number: Some("12345".to_string()),
..Default::default()
};
let partial_json = serde_json::to_value(&partial_tracking).unwrap();
assert_eq!(partial_json["tracking_number"], "12345");
assert!(partial_json.get("tracking_url").is_none());
assert!(partial_json.get("tracking_company").is_none());
}
#[test]
fn test_fulfillment_line_item_serialization() {
let line_item = FulfillmentLineItem {
id: Some(669751112),
variant_id: Some(457924702),
product_id: Some(632910392),
title: Some("IPod Nano - 8GB".to_string()),
quantity: Some(2),
sku: Some("IPOD2008BLACK".to_string()),
variant_title: Some("Black".to_string()),
vendor: Some("Apple".to_string()),
fulfillment_service: Some("manual".to_string()),
requires_shipping: Some(true),
taxable: Some(true),
gift_card: Some(false),
name: Some("IPod Nano - 8GB - Black".to_string()),
product_exists: Some(true),
price: Some("199.00".to_string()),
total_discount: Some("0.00".to_string()),
fulfillable_quantity: Some(0),
fulfillment_status: Some("fulfilled".to_string()),
..Default::default()
};
let json = serde_json::to_string(&line_item).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["id"], 669751112);
assert_eq!(parsed["variant_id"], 457924702);
assert_eq!(parsed["title"], "IPod Nano - 8GB");
assert_eq!(parsed["quantity"], 2);
assert_eq!(parsed["sku"], "IPOD2008BLACK");
assert_eq!(parsed["price"], "199.00");
assert_eq!(parsed["requires_shipping"], true);
}
#[test]
fn test_fulfillment_with_origin_address() {
let fulfillment = Fulfillment {
id: Some(123),
order_id: Some(456),
status: Some(FulfillmentStatus::Success),
origin_address: Some(Address {
first_name: Some("Warehouse".to_string()),
address1: Some("123 Fulfillment Center".to_string()),
city: Some("Los Angeles".to_string()),
province: Some("California".to_string()),
province_code: Some("CA".to_string()),
country: Some("United States".to_string()),
country_code: Some("US".to_string()),
zip: Some("90001".to_string()),
..Default::default()
}),
..Default::default()
};
let json = serde_json::to_string(&fulfillment).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("origin_address").is_some());
assert_eq!(parsed["origin_address"]["city"], "Los Angeles");
assert_eq!(parsed["origin_address"]["province_code"], "CA");
}
}