1use chrono::{DateTime, Utc};
42use serde::{Deserialize, Serialize};
43
44use crate::clients::RestClient;
45use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
46use crate::HttpMethod;
47
48use super::common::Address;
49
50#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
54#[serde(rename_all = "snake_case")]
55pub enum FulfillmentStatus {
56 #[default]
58 Pending,
59 Open,
61 Success,
63 Cancelled,
65 Error,
67 Failure,
69}
70
71#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
75#[serde(rename_all = "snake_case")]
76pub enum ShipmentStatus {
77 LabelPrinted,
79 LabelPurchased,
81 AttemptedDelivery,
83 ReadyForPickup,
85 Confirmed,
87 InTransit,
89 OutForDelivery,
91 Delivered,
93 Failure,
95}
96
97#[allow(clippy::derive_partial_eq_without_eq)]
101#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
102pub struct FulfillmentLineItem {
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub id: Option<u64>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub variant_id: Option<u64>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub title: Option<String>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub quantity: Option<i64>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub sku: Option<String>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub variant_title: Option<String>,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub vendor: Option<String>,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub fulfillment_service: Option<String>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub product_id: Option<u64>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub requires_shipping: Option<bool>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub taxable: Option<bool>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub gift_card: Option<bool>,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub name: Option<String>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub variant_inventory_management: Option<String>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub properties: Option<Vec<serde_json::Value>>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub product_exists: Option<bool>,
166
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub price: Option<String>,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub total_discount: Option<String>,
174
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub fulfillable_quantity: Option<i64>,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub fulfillment_status: Option<String>,
182}
183
184#[allow(clippy::derive_partial_eq_without_eq)]
201#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
202pub struct TrackingInfo {
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub tracking_number: Option<String>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub tracking_url: Option<String>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub tracking_company: Option<String>,
214}
215
216#[allow(clippy::derive_partial_eq_without_eq)]
248#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
249pub struct Fulfillment {
250 #[serde(skip_serializing)]
253 pub id: Option<u64>,
254
255 #[serde(skip_serializing)]
257 pub order_id: Option<u64>,
258
259 #[serde(skip_serializing)]
261 pub name: Option<String>,
262
263 #[serde(skip_serializing)]
265 pub created_at: Option<DateTime<Utc>>,
266
267 #[serde(skip_serializing)]
269 pub updated_at: Option<DateTime<Utc>>,
270
271 #[serde(skip_serializing)]
273 pub admin_graphql_api_id: Option<String>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
278 pub status: Option<FulfillmentStatus>,
279
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub service: Option<String>,
283
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub location_id: Option<u64>,
287
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub shipment_status: Option<ShipmentStatus>,
291
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub tracking_company: Option<String>,
295
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub tracking_number: Option<String>,
299
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub tracking_numbers: Option<Vec<String>>,
303
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub tracking_url: Option<String>,
307
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub tracking_urls: Option<Vec<String>>,
311
312 #[serde(skip_serializing_if = "Option::is_none")]
314 pub origin_address: Option<Address>,
315
316 #[serde(skip_serializing_if = "Option::is_none")]
318 pub line_items: Option<Vec<FulfillmentLineItem>>,
319
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub notify_customer: Option<bool>,
323
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub variant_inventory_management: Option<String>,
327}
328
329impl RestResource for Fulfillment {
330 type Id = u64;
331 type FindParams = FulfillmentFindParams;
332 type AllParams = FulfillmentListParams;
333 type CountParams = FulfillmentCountParams;
334
335 const NAME: &'static str = "Fulfillment";
336 const PLURAL: &'static str = "fulfillments";
337
338 const PATHS: &'static [ResourcePath] = &[
343 ResourcePath::new(
345 HttpMethod::Get,
346 ResourceOperation::Find,
347 &["order_id", "id"],
348 "orders/{order_id}/fulfillments/{id}",
349 ),
350 ResourcePath::new(
351 HttpMethod::Get,
352 ResourceOperation::All,
353 &["order_id"],
354 "orders/{order_id}/fulfillments",
355 ),
356 ResourcePath::new(
357 HttpMethod::Get,
358 ResourceOperation::Count,
359 &["order_id"],
360 "orders/{order_id}/fulfillments/count",
361 ),
362 ResourcePath::new(
363 HttpMethod::Post,
364 ResourceOperation::Create,
365 &["order_id"],
366 "orders/{order_id}/fulfillments",
367 ),
368 ResourcePath::new(
369 HttpMethod::Put,
370 ResourceOperation::Update,
371 &["order_id", "id"],
372 "orders/{order_id}/fulfillments/{id}",
373 ),
374 ];
375
376 fn get_id(&self) -> Option<Self::Id> {
377 self.id
378 }
379}
380
381impl Fulfillment {
382 pub async fn cancel(&self, client: &RestClient) -> Result<Self, ResourceError> {
403 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
404 resource: Self::NAME,
405 operation: "cancel",
406 })?;
407
408 let order_id = self.order_id.ok_or(ResourceError::PathResolutionFailed {
409 resource: Self::NAME,
410 operation: "cancel",
411 })?;
412
413 let path = format!("orders/{order_id}/fulfillments/{id}/cancel");
414 let body = serde_json::json!({});
415
416 let response = client.post(&path, body, None).await?;
417
418 if !response.is_ok() {
419 return Err(ResourceError::from_http_response(
420 response.code,
421 &response.body,
422 Self::NAME,
423 Some(&id.to_string()),
424 response.request_id(),
425 ));
426 }
427
428 let fulfillment: Self = response
430 .body
431 .get("fulfillment")
432 .ok_or_else(|| {
433 ResourceError::Http(crate::clients::HttpError::Response(
434 crate::clients::HttpResponseError {
435 code: response.code,
436 message: "Missing 'fulfillment' in response".to_string(),
437 error_reference: response.request_id().map(ToString::to_string),
438 },
439 ))
440 })
441 .and_then(|v| {
442 serde_json::from_value(v.clone()).map_err(|e| {
443 ResourceError::Http(crate::clients::HttpError::Response(
444 crate::clients::HttpResponseError {
445 code: response.code,
446 message: format!("Failed to deserialize fulfillment: {e}"),
447 error_reference: response.request_id().map(ToString::to_string),
448 },
449 ))
450 })
451 })?;
452
453 Ok(fulfillment)
454 }
455
456 pub async fn update_tracking(
488 &self,
489 client: &RestClient,
490 tracking_info: TrackingInfo,
491 ) -> Result<Self, ResourceError> {
492 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
493 resource: Self::NAME,
494 operation: "update_tracking",
495 })?;
496
497 let path = format!("fulfillments/{id}/update_tracking");
499
500 let body = serde_json::json!({
501 "fulfillment": {
502 "tracking_info": tracking_info
503 }
504 });
505
506 let response = client.post(&path, body, None).await?;
507
508 if !response.is_ok() {
509 return Err(ResourceError::from_http_response(
510 response.code,
511 &response.body,
512 Self::NAME,
513 Some(&id.to_string()),
514 response.request_id(),
515 ));
516 }
517
518 let fulfillment: Self = response
520 .body
521 .get("fulfillment")
522 .ok_or_else(|| {
523 ResourceError::Http(crate::clients::HttpError::Response(
524 crate::clients::HttpResponseError {
525 code: response.code,
526 message: "Missing 'fulfillment' in response".to_string(),
527 error_reference: response.request_id().map(ToString::to_string),
528 },
529 ))
530 })
531 .and_then(|v| {
532 serde_json::from_value(v.clone()).map_err(|e| {
533 ResourceError::Http(crate::clients::HttpError::Response(
534 crate::clients::HttpResponseError {
535 code: response.code,
536 message: format!("Failed to deserialize fulfillment: {e}"),
537 error_reference: response.request_id().map(ToString::to_string),
538 },
539 ))
540 })
541 })?;
542
543 Ok(fulfillment)
544 }
545}
546
547#[allow(clippy::derive_partial_eq_without_eq)]
549#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
550pub struct FulfillmentFindParams {
551 #[serde(skip_serializing_if = "Option::is_none")]
553 pub fields: Option<String>,
554}
555
556#[allow(clippy::derive_partial_eq_without_eq)]
560#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
561pub struct FulfillmentListParams {
562 #[serde(skip_serializing_if = "Option::is_none")]
564 pub limit: Option<u32>,
565
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub since_id: Option<u64>,
569
570 #[serde(skip_serializing_if = "Option::is_none")]
572 pub created_at_min: Option<DateTime<Utc>>,
573
574 #[serde(skip_serializing_if = "Option::is_none")]
576 pub created_at_max: Option<DateTime<Utc>>,
577
578 #[serde(skip_serializing_if = "Option::is_none")]
580 pub updated_at_min: Option<DateTime<Utc>>,
581
582 #[serde(skip_serializing_if = "Option::is_none")]
584 pub updated_at_max: Option<DateTime<Utc>>,
585
586 #[serde(skip_serializing_if = "Option::is_none")]
588 pub fields: Option<String>,
589
590 #[serde(skip_serializing_if = "Option::is_none")]
592 pub page_info: Option<String>,
593}
594
595#[allow(clippy::derive_partial_eq_without_eq)]
597#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
598pub struct FulfillmentCountParams {
599 #[serde(skip_serializing_if = "Option::is_none")]
601 pub created_at_min: Option<DateTime<Utc>>,
602
603 #[serde(skip_serializing_if = "Option::is_none")]
605 pub created_at_max: Option<DateTime<Utc>>,
606
607 #[serde(skip_serializing_if = "Option::is_none")]
609 pub updated_at_min: Option<DateTime<Utc>>,
610
611 #[serde(skip_serializing_if = "Option::is_none")]
613 pub updated_at_max: Option<DateTime<Utc>>,
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619 use crate::rest::{get_path, ResourceOperation};
620
621 #[test]
622 fn test_fulfillment_struct_serialization() {
623 let fulfillment = Fulfillment {
624 id: Some(123456789),
625 order_id: Some(987654321),
626 name: Some("#1001.1".to_string()),
627 status: Some(FulfillmentStatus::Success),
628 service: Some("manual".to_string()),
629 location_id: Some(111222333),
630 shipment_status: Some(ShipmentStatus::Delivered),
631 tracking_company: Some("UPS".to_string()),
632 tracking_number: Some("1Z999AA10123456784".to_string()),
633 tracking_numbers: Some(vec!["1Z999AA10123456784".to_string()]),
634 tracking_url: Some("https://ups.com/tracking/1Z999AA10123456784".to_string()),
635 tracking_urls: Some(vec![
636 "https://ups.com/tracking/1Z999AA10123456784".to_string()
637 ]),
638 notify_customer: Some(true),
639 ..Default::default()
640 };
641
642 let json = serde_json::to_string(&fulfillment).unwrap();
643 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
644
645 assert_eq!(parsed["status"], "success");
647 assert_eq!(parsed["service"], "manual");
648 assert_eq!(parsed["location_id"], 111222333);
649 assert_eq!(parsed["shipment_status"], "delivered");
650 assert_eq!(parsed["tracking_company"], "UPS");
651 assert_eq!(parsed["tracking_number"], "1Z999AA10123456784");
652 assert_eq!(parsed["notify_customer"], true);
653
654 assert!(parsed.get("id").is_none());
656 assert!(parsed.get("order_id").is_none());
657 assert!(parsed.get("name").is_none());
658 assert!(parsed.get("created_at").is_none());
659 assert!(parsed.get("updated_at").is_none());
660 assert!(parsed.get("admin_graphql_api_id").is_none());
661 }
662
663 #[test]
664 fn test_fulfillment_deserialization_from_api_response() {
665 let json_str = r##"{
667 "id": 255858046,
668 "order_id": 450789469,
669 "name": "#1001.1",
670 "status": "success",
671 "service": "manual",
672 "location_id": 487838322,
673 "created_at": "2024-01-15T10:30:00Z",
674 "updated_at": "2024-06-20T15:45:00Z",
675 "shipment_status": "in_transit",
676 "tracking_company": "USPS",
677 "tracking_number": "9400111899223456789012",
678 "tracking_numbers": ["9400111899223456789012"],
679 "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=9400111899223456789012",
680 "tracking_urls": ["https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=9400111899223456789012"],
681 "line_items": [
682 {
683 "id": 669751112,
684 "variant_id": 457924702,
685 "product_id": 632910392,
686 "title": "IPod Nano - 8GB",
687 "quantity": 1,
688 "sku": "IPOD2008BLACK",
689 "requires_shipping": true
690 }
691 ],
692 "admin_graphql_api_id": "gid://shopify/Fulfillment/255858046"
693 }"##;
694
695 let fulfillment: Fulfillment = serde_json::from_str(json_str).unwrap();
696
697 assert_eq!(fulfillment.id, Some(255858046));
698 assert_eq!(fulfillment.order_id, Some(450789469));
699 assert_eq!(fulfillment.name.as_deref(), Some("#1001.1"));
700 assert_eq!(fulfillment.status, Some(FulfillmentStatus::Success));
701 assert_eq!(fulfillment.service.as_deref(), Some("manual"));
702 assert_eq!(fulfillment.location_id, Some(487838322));
703 assert_eq!(fulfillment.shipment_status, Some(ShipmentStatus::InTransit));
704 assert_eq!(fulfillment.tracking_company.as_deref(), Some("USPS"));
705 assert_eq!(
706 fulfillment.tracking_number.as_deref(),
707 Some("9400111899223456789012")
708 );
709 assert!(fulfillment.created_at.is_some());
710 assert!(fulfillment.updated_at.is_some());
711
712 let line_items = fulfillment.line_items.unwrap();
714 assert_eq!(line_items.len(), 1);
715 assert_eq!(line_items[0].id, Some(669751112));
716 assert_eq!(line_items[0].title.as_deref(), Some("IPod Nano - 8GB"));
717 assert_eq!(line_items[0].quantity, Some(1));
718 }
719
720 #[test]
721 fn test_fulfillment_status_enum_serialization() {
722 let pending_str = serde_json::to_string(&FulfillmentStatus::Pending).unwrap();
724 assert_eq!(pending_str, "\"pending\"");
725
726 let open_str = serde_json::to_string(&FulfillmentStatus::Open).unwrap();
727 assert_eq!(open_str, "\"open\"");
728
729 let success_str = serde_json::to_string(&FulfillmentStatus::Success).unwrap();
730 assert_eq!(success_str, "\"success\"");
731
732 let cancelled_str = serde_json::to_string(&FulfillmentStatus::Cancelled).unwrap();
733 assert_eq!(cancelled_str, "\"cancelled\"");
734
735 let error_str = serde_json::to_string(&FulfillmentStatus::Error).unwrap();
736 assert_eq!(error_str, "\"error\"");
737
738 let failure_str = serde_json::to_string(&FulfillmentStatus::Failure).unwrap();
739 assert_eq!(failure_str, "\"failure\"");
740
741 let success: FulfillmentStatus = serde_json::from_str("\"success\"").unwrap();
743 let cancelled: FulfillmentStatus = serde_json::from_str("\"cancelled\"").unwrap();
744
745 assert_eq!(success, FulfillmentStatus::Success);
746 assert_eq!(cancelled, FulfillmentStatus::Cancelled);
747
748 assert_eq!(FulfillmentStatus::default(), FulfillmentStatus::Pending);
750 }
751
752 #[test]
753 fn test_shipment_status_enum_serialization() {
754 let label_printed = serde_json::to_string(&ShipmentStatus::LabelPrinted).unwrap();
756 assert_eq!(label_printed, "\"label_printed\"");
757
758 let label_purchased = serde_json::to_string(&ShipmentStatus::LabelPurchased).unwrap();
759 assert_eq!(label_purchased, "\"label_purchased\"");
760
761 let attempted = serde_json::to_string(&ShipmentStatus::AttemptedDelivery).unwrap();
762 assert_eq!(attempted, "\"attempted_delivery\"");
763
764 let ready = serde_json::to_string(&ShipmentStatus::ReadyForPickup).unwrap();
765 assert_eq!(ready, "\"ready_for_pickup\"");
766
767 let confirmed = serde_json::to_string(&ShipmentStatus::Confirmed).unwrap();
768 assert_eq!(confirmed, "\"confirmed\"");
769
770 let in_transit = serde_json::to_string(&ShipmentStatus::InTransit).unwrap();
771 assert_eq!(in_transit, "\"in_transit\"");
772
773 let out_for_delivery = serde_json::to_string(&ShipmentStatus::OutForDelivery).unwrap();
774 assert_eq!(out_for_delivery, "\"out_for_delivery\"");
775
776 let delivered = serde_json::to_string(&ShipmentStatus::Delivered).unwrap();
777 assert_eq!(delivered, "\"delivered\"");
778
779 let failure = serde_json::to_string(&ShipmentStatus::Failure).unwrap();
780 assert_eq!(failure, "\"failure\"");
781
782 let in_transit_val: ShipmentStatus = serde_json::from_str("\"in_transit\"").unwrap();
784 let delivered_val: ShipmentStatus = serde_json::from_str("\"delivered\"").unwrap();
785 let out_for_delivery_val: ShipmentStatus =
786 serde_json::from_str("\"out_for_delivery\"").unwrap();
787
788 assert_eq!(in_transit_val, ShipmentStatus::InTransit);
789 assert_eq!(delivered_val, ShipmentStatus::Delivered);
790 assert_eq!(out_for_delivery_val, ShipmentStatus::OutForDelivery);
791 }
792
793 #[test]
794 fn test_nested_path_under_orders() {
795 let find_path = get_path(
797 Fulfillment::PATHS,
798 ResourceOperation::Find,
799 &["order_id", "id"],
800 );
801 assert!(find_path.is_some());
802 assert_eq!(
803 find_path.unwrap().template,
804 "orders/{order_id}/fulfillments/{id}"
805 );
806 assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
807
808 let all_path = get_path(Fulfillment::PATHS, ResourceOperation::All, &["order_id"]);
810 assert!(all_path.is_some());
811 assert_eq!(all_path.unwrap().template, "orders/{order_id}/fulfillments");
812
813 let count_path = get_path(Fulfillment::PATHS, ResourceOperation::Count, &["order_id"]);
815 assert!(count_path.is_some());
816 assert_eq!(
817 count_path.unwrap().template,
818 "orders/{order_id}/fulfillments/count"
819 );
820
821 let create_path = get_path(Fulfillment::PATHS, ResourceOperation::Create, &["order_id"]);
823 assert!(create_path.is_some());
824 assert_eq!(
825 create_path.unwrap().template,
826 "orders/{order_id}/fulfillments"
827 );
828 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
829
830 let update_path = get_path(
832 Fulfillment::PATHS,
833 ResourceOperation::Update,
834 &["order_id", "id"],
835 );
836 assert!(update_path.is_some());
837 assert_eq!(
838 update_path.unwrap().template,
839 "orders/{order_id}/fulfillments/{id}"
840 );
841 assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
842
843 let no_order_all = get_path(Fulfillment::PATHS, ResourceOperation::All, &[]);
845 assert!(no_order_all.is_none());
846
847 assert_eq!(Fulfillment::NAME, "Fulfillment");
849 assert_eq!(Fulfillment::PLURAL, "fulfillments");
850 }
851
852 #[test]
853 fn test_resource_specific_operations_signatures() {
854 fn _assert_cancel_signature<F, Fut>(f: F)
860 where
861 F: Fn(&Fulfillment, &RestClient) -> Fut,
862 Fut: std::future::Future<Output = Result<Fulfillment, ResourceError>>,
863 {
864 let _ = f;
865 }
866
867 fn _assert_update_tracking_signature<F, Fut>(f: F)
869 where
870 F: Fn(&Fulfillment, &RestClient, TrackingInfo) -> Fut,
871 Fut: std::future::Future<Output = Result<Fulfillment, ResourceError>>,
872 {
873 let _ = f;
874 }
875
876 let fulfillment_without_id = Fulfillment::default();
878 assert!(fulfillment_without_id.get_id().is_none());
879
880 let tracking = TrackingInfo {
882 tracking_number: Some("1Z999AA10123456784".to_string()),
883 tracking_url: Some("https://ups.com/tracking".to_string()),
884 tracking_company: Some("UPS".to_string()),
885 };
886 assert_eq!(
887 tracking.tracking_number,
888 Some("1Z999AA10123456784".to_string())
889 );
890
891 let fulfillment_with_id = Fulfillment {
893 id: Some(255858046),
894 order_id: Some(450789469),
895 ..Default::default()
896 };
897 assert_eq!(fulfillment_with_id.get_id(), Some(255858046));
898 }
899
900 #[test]
901 fn test_fulfillment_list_params_serialization() {
902 let created_at_min = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
903 .unwrap()
904 .with_timezone(&Utc);
905
906 let params = FulfillmentListParams {
907 limit: Some(50),
908 since_id: Some(12345),
909 created_at_min: Some(created_at_min),
910 created_at_max: None,
911 updated_at_min: None,
912 updated_at_max: None,
913 fields: Some("id,status,tracking_number".to_string()),
914 page_info: None,
915 };
916
917 let json = serde_json::to_value(¶ms).unwrap();
918
919 assert_eq!(json["limit"], 50);
920 assert_eq!(json["since_id"], 12345);
921 assert_eq!(json["fields"], "id,status,tracking_number");
922 assert!(json["created_at_min"].as_str().is_some());
923
924 assert!(json.get("created_at_max").is_none());
926 assert!(json.get("updated_at_min").is_none());
927 assert!(json.get("page_info").is_none());
928
929 let empty_params = FulfillmentListParams::default();
931 let empty_json = serde_json::to_value(&empty_params).unwrap();
932 assert_eq!(empty_json, serde_json::json!({}));
933 }
934
935 #[test]
936 fn test_tracking_info_serialization() {
937 let tracking = TrackingInfo {
938 tracking_number: Some("1Z999AA10123456784".to_string()),
939 tracking_url: Some("https://ups.com/tracking/1Z999AA10123456784".to_string()),
940 tracking_company: Some("UPS".to_string()),
941 };
942
943 let json = serde_json::to_string(&tracking).unwrap();
944 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
945
946 assert_eq!(parsed["tracking_number"], "1Z999AA10123456784");
947 assert_eq!(
948 parsed["tracking_url"],
949 "https://ups.com/tracking/1Z999AA10123456784"
950 );
951 assert_eq!(parsed["tracking_company"], "UPS");
952
953 let partial_tracking = TrackingInfo {
955 tracking_number: Some("12345".to_string()),
956 ..Default::default()
957 };
958
959 let partial_json = serde_json::to_value(&partial_tracking).unwrap();
960 assert_eq!(partial_json["tracking_number"], "12345");
961 assert!(partial_json.get("tracking_url").is_none());
962 assert!(partial_json.get("tracking_company").is_none());
963 }
964
965 #[test]
966 fn test_fulfillment_line_item_serialization() {
967 let line_item = FulfillmentLineItem {
968 id: Some(669751112),
969 variant_id: Some(457924702),
970 product_id: Some(632910392),
971 title: Some("IPod Nano - 8GB".to_string()),
972 quantity: Some(2),
973 sku: Some("IPOD2008BLACK".to_string()),
974 variant_title: Some("Black".to_string()),
975 vendor: Some("Apple".to_string()),
976 fulfillment_service: Some("manual".to_string()),
977 requires_shipping: Some(true),
978 taxable: Some(true),
979 gift_card: Some(false),
980 name: Some("IPod Nano - 8GB - Black".to_string()),
981 product_exists: Some(true),
982 price: Some("199.00".to_string()),
983 total_discount: Some("0.00".to_string()),
984 fulfillable_quantity: Some(0),
985 fulfillment_status: Some("fulfilled".to_string()),
986 ..Default::default()
987 };
988
989 let json = serde_json::to_string(&line_item).unwrap();
990 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
991
992 assert_eq!(parsed["id"], 669751112);
993 assert_eq!(parsed["variant_id"], 457924702);
994 assert_eq!(parsed["title"], "IPod Nano - 8GB");
995 assert_eq!(parsed["quantity"], 2);
996 assert_eq!(parsed["sku"], "IPOD2008BLACK");
997 assert_eq!(parsed["price"], "199.00");
998 assert_eq!(parsed["requires_shipping"], true);
999 }
1000
1001 #[test]
1002 fn test_fulfillment_with_origin_address() {
1003 let fulfillment = Fulfillment {
1004 id: Some(123),
1005 order_id: Some(456),
1006 status: Some(FulfillmentStatus::Success),
1007 origin_address: Some(Address {
1008 first_name: Some("Warehouse".to_string()),
1009 address1: Some("123 Fulfillment Center".to_string()),
1010 city: Some("Los Angeles".to_string()),
1011 province: Some("California".to_string()),
1012 province_code: Some("CA".to_string()),
1013 country: Some("United States".to_string()),
1014 country_code: Some("US".to_string()),
1015 zip: Some("90001".to_string()),
1016 ..Default::default()
1017 }),
1018 ..Default::default()
1019 };
1020
1021 let json = serde_json::to_string(&fulfillment).unwrap();
1022 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1023
1024 assert!(parsed.get("origin_address").is_some());
1026 assert_eq!(parsed["origin_address"]["city"], "Los Angeles");
1027 assert_eq!(parsed["origin_address"]["province_code"], "CA");
1028 }
1029}