1use chrono::{DateTime, Utc};
53use serde::{Deserialize, Serialize};
54
55use crate::clients::RestClient;
56use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
57use crate::HttpMethod;
58
59use super::common::{Address, NoteAttribute, ShippingLine, TaxLine};
60use super::customer::Customer;
61
62#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
66#[serde(rename_all = "snake_case")]
67pub enum DraftOrderStatus {
68 #[default]
70 Open,
71 InvoiceSent,
73 Completed,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
81pub struct AppliedDiscount {
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub title: Option<String>,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub description: Option<String>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub value: Option<String>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub value_type: Option<String>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub amount: Option<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
107pub struct DraftOrderLineItem {
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub id: Option<u64>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub variant_id: Option<u64>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub product_id: Option<u64>,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub title: Option<String>,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub variant_title: Option<String>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub sku: Option<String>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub vendor: Option<String>,
135
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub quantity: Option<i64>,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub requires_shipping: Option<bool>,
143
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub taxable: Option<bool>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub gift_card: Option<bool>,
151
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub fulfillment_service: Option<String>,
155
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub grams: Option<i64>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub tax_lines: Option<Vec<TaxLine>>,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub name: Option<String>,
167
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub properties: Option<Vec<serde_json::Value>>,
171
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub custom: Option<bool>,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub price: Option<String>,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub applied_discount: Option<AppliedDiscount>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub admin_graphql_api_id: Option<String>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
193pub struct DraftOrderInvoice {
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub to: Option<String>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub from: Option<String>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub subject: Option<String>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub custom_message: Option<String>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub bcc: Option<Vec<String>>,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
219pub struct DraftOrderCompleteParams {
220 #[serde(skip_serializing_if = "Option::is_none")]
224 pub payment_pending: Option<bool>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
263pub struct DraftOrder {
264 #[serde(skip_serializing)]
267 pub id: Option<u64>,
268
269 #[serde(skip_serializing)]
271 pub order_id: Option<u64>,
272
273 #[serde(skip_serializing)]
275 pub name: Option<String>,
276
277 #[serde(skip_serializing)]
279 pub invoice_sent_at: Option<DateTime<Utc>>,
280
281 #[serde(skip_serializing)]
283 pub invoice_url: Option<String>,
284
285 #[serde(skip_serializing)]
287 pub completed_at: Option<DateTime<Utc>>,
288
289 #[serde(skip_serializing)]
291 pub created_at: Option<DateTime<Utc>>,
292
293 #[serde(skip_serializing)]
295 pub updated_at: Option<DateTime<Utc>>,
296
297 #[serde(skip_serializing)]
299 pub admin_graphql_api_id: Option<String>,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
304 pub status: Option<DraftOrderStatus>,
305
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub email: Option<String>,
309
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub currency: Option<String>,
313
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub tax_exempt: Option<bool>,
317
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub tax_exemptions: Option<Vec<String>>,
321
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub taxes_included: Option<bool>,
325
326 #[serde(skip_serializing_if = "Option::is_none")]
328 pub total_tax: Option<String>,
329
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub subtotal_price: Option<String>,
333
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub total_price: Option<String>,
337
338 #[serde(skip_serializing_if = "Option::is_none")]
340 pub note: Option<String>,
341
342 #[serde(skip_serializing_if = "Option::is_none")]
344 pub note_attributes: Option<Vec<NoteAttribute>>,
345
346 #[serde(skip_serializing_if = "Option::is_none")]
348 pub tags: Option<String>,
349
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub customer_id: Option<u64>,
353
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub use_customer_default_address: Option<bool>,
357
358 #[serde(skip_serializing_if = "Option::is_none")]
361 pub line_items: Option<Vec<DraftOrderLineItem>>,
362
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub shipping_address: Option<Address>,
366
367 #[serde(skip_serializing_if = "Option::is_none")]
369 pub billing_address: Option<Address>,
370
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub customer: Option<Customer>,
374
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub shipping_line: Option<ShippingLine>,
378
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub tax_lines: Option<Vec<TaxLine>>,
382
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub applied_discount: Option<AppliedDiscount>,
386}
387
388impl RestResource for DraftOrder {
389 type Id = u64;
390 type FindParams = DraftOrderFindParams;
391 type AllParams = DraftOrderListParams;
392 type CountParams = DraftOrderCountParams;
393
394 const NAME: &'static str = "DraftOrder";
395 const PLURAL: &'static str = "draft_orders";
396
397 const PATHS: &'static [ResourcePath] = &[
401 ResourcePath::new(
402 HttpMethod::Get,
403 ResourceOperation::Find,
404 &["id"],
405 "draft_orders/{id}",
406 ),
407 ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "draft_orders"),
408 ResourcePath::new(
409 HttpMethod::Get,
410 ResourceOperation::Count,
411 &[],
412 "draft_orders/count",
413 ),
414 ResourcePath::new(
415 HttpMethod::Post,
416 ResourceOperation::Create,
417 &[],
418 "draft_orders",
419 ),
420 ResourcePath::new(
421 HttpMethod::Put,
422 ResourceOperation::Update,
423 &["id"],
424 "draft_orders/{id}",
425 ),
426 ResourcePath::new(
427 HttpMethod::Delete,
428 ResourceOperation::Delete,
429 &["id"],
430 "draft_orders/{id}",
431 ),
432 ];
433
434 fn get_id(&self) -> Option<Self::Id> {
435 self.id
436 }
437}
438
439impl DraftOrder {
440 pub async fn complete(
468 &self,
469 client: &RestClient,
470 params: Option<DraftOrderCompleteParams>,
471 ) -> Result<Self, ResourceError> {
472 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
473 resource: Self::NAME,
474 operation: "complete",
475 })?;
476
477 let query_string = params
479 .as_ref()
480 .and_then(|p| p.payment_pending)
481 .map(|pp| format!("?payment_pending={}", pp))
482 .unwrap_or_default();
483
484 let path = format!("draft_orders/{id}/complete{query_string}");
485 let body = serde_json::json!({});
486
487 let response = client.put(&path, body, None).await?;
488
489 if !response.is_ok() {
490 return Err(ResourceError::from_http_response(
491 response.code,
492 &response.body,
493 Self::NAME,
494 Some(&id.to_string()),
495 response.request_id(),
496 ));
497 }
498
499 let draft_order: Self = response
501 .body
502 .get("draft_order")
503 .ok_or_else(|| {
504 ResourceError::Http(crate::clients::HttpError::Response(
505 crate::clients::HttpResponseError {
506 code: response.code,
507 message: "Missing 'draft_order' in response".to_string(),
508 error_reference: response.request_id().map(ToString::to_string),
509 },
510 ))
511 })
512 .and_then(|v| {
513 serde_json::from_value(v.clone()).map_err(|e| {
514 ResourceError::Http(crate::clients::HttpError::Response(
515 crate::clients::HttpResponseError {
516 code: response.code,
517 message: format!("Failed to deserialize draft_order: {e}"),
518 error_reference: response.request_id().map(ToString::to_string),
519 },
520 ))
521 })
522 })?;
523
524 Ok(draft_order)
525 }
526
527 pub async fn send_invoice(
558 &self,
559 client: &RestClient,
560 invoice: DraftOrderInvoice,
561 ) -> Result<Self, ResourceError> {
562 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
563 resource: Self::NAME,
564 operation: "send_invoice",
565 })?;
566
567 let path = format!("draft_orders/{id}/send_invoice");
568
569 let body = serde_json::json!({
570 "draft_order_invoice": invoice
571 });
572
573 let response = client.post(&path, body, None).await?;
574
575 if !response.is_ok() {
576 return Err(ResourceError::from_http_response(
577 response.code,
578 &response.body,
579 Self::NAME,
580 Some(&id.to_string()),
581 response.request_id(),
582 ));
583 }
584
585 let draft_order: Self = response
587 .body
588 .get("draft_order")
589 .ok_or_else(|| {
590 ResourceError::Http(crate::clients::HttpError::Response(
591 crate::clients::HttpResponseError {
592 code: response.code,
593 message: "Missing 'draft_order' in response".to_string(),
594 error_reference: response.request_id().map(ToString::to_string),
595 },
596 ))
597 })
598 .and_then(|v| {
599 serde_json::from_value(v.clone()).map_err(|e| {
600 ResourceError::Http(crate::clients::HttpError::Response(
601 crate::clients::HttpResponseError {
602 code: response.code,
603 message: format!("Failed to deserialize draft_order: {e}"),
604 error_reference: response.request_id().map(ToString::to_string),
605 },
606 ))
607 })
608 })?;
609
610 Ok(draft_order)
611 }
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
616pub struct DraftOrderFindParams {
617 #[serde(skip_serializing_if = "Option::is_none")]
619 pub fields: Option<String>,
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
626pub struct DraftOrderListParams {
627 #[serde(skip_serializing_if = "Option::is_none")]
629 pub limit: Option<u32>,
630
631 #[serde(skip_serializing_if = "Option::is_none")]
633 pub since_id: Option<u64>,
634
635 #[serde(skip_serializing_if = "Option::is_none")]
637 pub created_at_min: Option<DateTime<Utc>>,
638
639 #[serde(skip_serializing_if = "Option::is_none")]
641 pub created_at_max: Option<DateTime<Utc>>,
642
643 #[serde(skip_serializing_if = "Option::is_none")]
645 pub updated_at_min: Option<DateTime<Utc>>,
646
647 #[serde(skip_serializing_if = "Option::is_none")]
649 pub updated_at_max: Option<DateTime<Utc>>,
650
651 #[serde(skip_serializing_if = "Option::is_none")]
653 pub ids: Option<Vec<u64>>,
654
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub status: Option<DraftOrderStatus>,
658
659 #[serde(skip_serializing_if = "Option::is_none")]
661 pub fields: Option<String>,
662
663 #[serde(skip_serializing_if = "Option::is_none")]
665 pub page_info: Option<String>,
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
670pub struct DraftOrderCountParams {
671 #[serde(skip_serializing_if = "Option::is_none")]
673 pub since_id: Option<u64>,
674
675 #[serde(skip_serializing_if = "Option::is_none")]
677 pub status: Option<DraftOrderStatus>,
678
679 #[serde(skip_serializing_if = "Option::is_none")]
681 pub created_at_min: Option<DateTime<Utc>>,
682
683 #[serde(skip_serializing_if = "Option::is_none")]
685 pub created_at_max: Option<DateTime<Utc>>,
686
687 #[serde(skip_serializing_if = "Option::is_none")]
689 pub updated_at_min: Option<DateTime<Utc>>,
690
691 #[serde(skip_serializing_if = "Option::is_none")]
693 pub updated_at_max: Option<DateTime<Utc>>,
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699 use crate::rest::{get_path, ResourceOperation};
700
701 #[test]
702 fn test_draft_order_struct_serialization() {
703 let draft_order = DraftOrder {
704 id: Some(123456789),
705 order_id: Some(987654321),
706 name: Some("#D1".to_string()),
707 status: Some(DraftOrderStatus::Open),
708 email: Some("customer@example.com".to_string()),
709 currency: Some("USD".to_string()),
710 tax_exempt: Some(false),
711 taxes_included: Some(true),
712 total_tax: Some("10.00".to_string()),
713 subtotal_price: Some("100.00".to_string()),
714 total_price: Some("110.00".to_string()),
715 note: Some("Wholesale order".to_string()),
716 tags: Some("vip, wholesale".to_string()),
717 created_at: Some(
718 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
719 .unwrap()
720 .with_timezone(&Utc),
721 ),
722 ..Default::default()
723 };
724
725 let json = serde_json::to_string(&draft_order).unwrap();
726 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
727
728 assert_eq!(parsed["status"], "open");
730 assert_eq!(parsed["email"], "customer@example.com");
731 assert_eq!(parsed["currency"], "USD");
732 assert_eq!(parsed["tax_exempt"], false);
733 assert_eq!(parsed["taxes_included"], true);
734 assert_eq!(parsed["total_tax"], "10.00");
735 assert_eq!(parsed["subtotal_price"], "100.00");
736 assert_eq!(parsed["total_price"], "110.00");
737 assert_eq!(parsed["note"], "Wholesale order");
738 assert_eq!(parsed["tags"], "vip, wholesale");
739
740 assert!(parsed.get("id").is_none());
742 assert!(parsed.get("order_id").is_none());
743 assert!(parsed.get("name").is_none());
744 assert!(parsed.get("created_at").is_none());
745 assert!(parsed.get("updated_at").is_none());
746 assert!(parsed.get("admin_graphql_api_id").is_none());
747 }
748
749 #[test]
750 fn test_draft_order_deserialization_from_api_response() {
751 let json_str = r##"{
752 "id": 994118539,
753 "order_id": null,
754 "name": "#D2",
755 "status": "open",
756 "email": "bob.norman@example.com",
757 "currency": "USD",
758 "invoice_sent_at": null,
759 "invoice_url": "https://jsmith.myshopify.com/548380009/invoices/994118539/dcc0adb7c08e3be1",
760 "created_at": "2024-01-15T10:30:00Z",
761 "updated_at": "2024-01-15T10:30:00Z",
762 "tax_exempt": false,
763 "taxes_included": false,
764 "total_tax": "11.94",
765 "subtotal_price": "398.00",
766 "total_price": "409.94",
767 "line_items": [
768 {
769 "id": 994118540,
770 "variant_id": 39072856,
771 "product_id": 632910392,
772 "title": "IPod Nano - 8GB",
773 "variant_title": "green",
774 "sku": "IPOD2008GREEN",
775 "vendor": "Apple",
776 "quantity": 1,
777 "requires_shipping": true,
778 "taxable": true,
779 "gift_card": false,
780 "price": "199.00"
781 }
782 ],
783 "shipping_address": {
784 "first_name": "Bob",
785 "last_name": "Norman",
786 "address1": "Chestnut Street 92",
787 "city": "Louisville",
788 "province": "Kentucky",
789 "country": "United States",
790 "zip": "40202"
791 },
792 "billing_address": {
793 "first_name": "Bob",
794 "last_name": "Norman",
795 "address1": "Chestnut Street 92",
796 "city": "Louisville",
797 "province": "Kentucky",
798 "country": "United States",
799 "zip": "40202"
800 },
801 "note": "Test draft order",
802 "admin_graphql_api_id": "gid://shopify/DraftOrder/994118539"
803 }"##;
804
805 let draft_order: DraftOrder = serde_json::from_str(json_str).unwrap();
806
807 assert_eq!(draft_order.id, Some(994118539));
808 assert_eq!(draft_order.order_id, None);
809 assert_eq!(draft_order.name.as_deref(), Some("#D2"));
810 assert_eq!(draft_order.status, Some(DraftOrderStatus::Open));
811 assert_eq!(draft_order.email.as_deref(), Some("bob.norman@example.com"));
812 assert_eq!(draft_order.currency.as_deref(), Some("USD"));
813 assert_eq!(draft_order.total_tax.as_deref(), Some("11.94"));
814 assert_eq!(draft_order.subtotal_price.as_deref(), Some("398.00"));
815 assert_eq!(draft_order.total_price.as_deref(), Some("409.94"));
816 assert!(draft_order.created_at.is_some());
817 assert!(draft_order.updated_at.is_some());
818
819 let line_items = draft_order.line_items.unwrap();
821 assert_eq!(line_items.len(), 1);
822 assert_eq!(line_items[0].id, Some(994118540));
823 assert_eq!(line_items[0].title.as_deref(), Some("IPod Nano - 8GB"));
824 assert_eq!(line_items[0].quantity, Some(1));
825 assert_eq!(line_items[0].price.as_deref(), Some("199.00"));
826
827 let shipping = draft_order.shipping_address.unwrap();
829 assert_eq!(shipping.first_name.as_deref(), Some("Bob"));
830 assert_eq!(shipping.city.as_deref(), Some("Louisville"));
831
832 let billing = draft_order.billing_address.unwrap();
834 assert_eq!(billing.first_name.as_deref(), Some("Bob"));
835 }
836
837 #[test]
838 fn test_draft_order_status_enum_serialization() {
839 let open_str = serde_json::to_string(&DraftOrderStatus::Open).unwrap();
841 assert_eq!(open_str, "\"open\"");
842
843 let invoice_sent_str = serde_json::to_string(&DraftOrderStatus::InvoiceSent).unwrap();
844 assert_eq!(invoice_sent_str, "\"invoice_sent\"");
845
846 let completed_str = serde_json::to_string(&DraftOrderStatus::Completed).unwrap();
847 assert_eq!(completed_str, "\"completed\"");
848
849 let open: DraftOrderStatus = serde_json::from_str("\"open\"").unwrap();
851 let invoice_sent: DraftOrderStatus = serde_json::from_str("\"invoice_sent\"").unwrap();
852 let completed: DraftOrderStatus = serde_json::from_str("\"completed\"").unwrap();
853
854 assert_eq!(open, DraftOrderStatus::Open);
855 assert_eq!(invoice_sent, DraftOrderStatus::InvoiceSent);
856 assert_eq!(completed, DraftOrderStatus::Completed);
857
858 assert_eq!(DraftOrderStatus::default(), DraftOrderStatus::Open);
860 }
861
862 #[test]
863 fn test_draft_order_path_constants() {
864 let find_path = get_path(DraftOrder::PATHS, ResourceOperation::Find, &["id"]);
866 assert!(find_path.is_some());
867 assert_eq!(find_path.unwrap().template, "draft_orders/{id}");
868 assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
869
870 let all_path = get_path(DraftOrder::PATHS, ResourceOperation::All, &[]);
872 assert!(all_path.is_some());
873 assert_eq!(all_path.unwrap().template, "draft_orders");
874
875 let count_path = get_path(DraftOrder::PATHS, ResourceOperation::Count, &[]);
877 assert!(count_path.is_some());
878 assert_eq!(count_path.unwrap().template, "draft_orders/count");
879
880 let create_path = get_path(DraftOrder::PATHS, ResourceOperation::Create, &[]);
882 assert!(create_path.is_some());
883 assert_eq!(create_path.unwrap().template, "draft_orders");
884 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
885
886 let update_path = get_path(DraftOrder::PATHS, ResourceOperation::Update, &["id"]);
888 assert!(update_path.is_some());
889 assert_eq!(update_path.unwrap().template, "draft_orders/{id}");
890 assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
891
892 let delete_path = get_path(DraftOrder::PATHS, ResourceOperation::Delete, &["id"]);
894 assert!(delete_path.is_some());
895 assert_eq!(delete_path.unwrap().template, "draft_orders/{id}");
896 assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
897
898 assert_eq!(DraftOrder::NAME, "DraftOrder");
900 assert_eq!(DraftOrder::PLURAL, "draft_orders");
901 }
902
903 #[test]
904 fn test_complete_method_signature() {
905 fn _assert_complete_signature<F, Fut>(f: F)
907 where
908 F: Fn(&DraftOrder, &RestClient, Option<DraftOrderCompleteParams>) -> Fut,
909 Fut: std::future::Future<Output = Result<DraftOrder, ResourceError>>,
910 {
911 let _ = f;
912 }
913
914 let params = DraftOrderCompleteParams {
916 payment_pending: Some(true),
917 };
918 assert_eq!(params.payment_pending, Some(true));
919
920 let draft_without_id = DraftOrder::default();
922 assert!(draft_without_id.get_id().is_none());
923 }
924
925 #[test]
926 fn test_send_invoice_method_signature() {
927 fn _assert_send_invoice_signature<F, Fut>(f: F)
929 where
930 F: Fn(&DraftOrder, &RestClient, DraftOrderInvoice) -> Fut,
931 Fut: std::future::Future<Output = Result<DraftOrder, ResourceError>>,
932 {
933 let _ = f;
934 }
935
936 let invoice = DraftOrderInvoice {
938 to: Some("customer@example.com".to_string()),
939 from: Some("store@example.com".to_string()),
940 subject: Some("Your order".to_string()),
941 custom_message: Some("Thanks!".to_string()),
942 bcc: Some(vec!["admin@example.com".to_string()]),
943 };
944
945 let json = serde_json::to_value(&invoice).unwrap();
946 assert_eq!(json["to"], "customer@example.com");
947 assert_eq!(json["from"], "store@example.com");
948 assert_eq!(json["subject"], "Your order");
949 assert_eq!(json["custom_message"], "Thanks!");
950 assert_eq!(json["bcc"][0], "admin@example.com");
951 }
952
953 #[test]
954 fn test_draft_order_line_item_with_applied_discount() {
955 let line_item = DraftOrderLineItem {
956 id: Some(123),
957 variant_id: Some(456),
958 product_id: Some(789),
959 title: Some("Test Product".to_string()),
960 quantity: Some(2),
961 price: Some("50.00".to_string()),
962 taxable: Some(true),
963 applied_discount: Some(AppliedDiscount {
964 title: Some("10% Off".to_string()),
965 description: Some("Wholesale discount".to_string()),
966 value: Some("10".to_string()),
967 value_type: Some("percentage".to_string()),
968 amount: Some("10.00".to_string()),
969 }),
970 ..Default::default()
971 };
972
973 let json = serde_json::to_value(&line_item).unwrap();
974 assert_eq!(json["id"], 123);
975 assert_eq!(json["title"], "Test Product");
976 assert_eq!(json["quantity"], 2);
977 assert!(json.get("applied_discount").is_some());
978 assert_eq!(json["applied_discount"]["title"], "10% Off");
979 assert_eq!(json["applied_discount"]["value"], "10");
980 assert_eq!(json["applied_discount"]["value_type"], "percentage");
981 }
982
983 #[test]
984 fn test_draft_order_invoice_serialization() {
985 let invoice = DraftOrderInvoice {
986 to: Some("customer@example.com".to_string()),
987 subject: Some("Invoice for your order".to_string()),
988 custom_message: Some("Thank you for shopping with us!".to_string()),
989 ..Default::default()
990 };
991
992 let json = serde_json::to_value(&invoice).unwrap();
993
994 assert_eq!(json["to"], "customer@example.com");
995 assert_eq!(json["subject"], "Invoice for your order");
996 assert_eq!(json["custom_message"], "Thank you for shopping with us!");
997 assert!(json.get("from").is_none());
998 assert!(json.get("bcc").is_none());
999
1000 let empty_invoice = DraftOrderInvoice::default();
1002 let empty_json = serde_json::to_value(&empty_invoice).unwrap();
1003 assert_eq!(empty_json, serde_json::json!({}));
1004 }
1005
1006 #[test]
1007 fn test_draft_order_get_id_returns_correct_value() {
1008 let draft_with_id = DraftOrder {
1009 id: Some(994118539),
1010 name: Some("#D2".to_string()),
1011 ..Default::default()
1012 };
1013 assert_eq!(draft_with_id.get_id(), Some(994118539));
1014
1015 let draft_without_id = DraftOrder {
1016 id: None,
1017 email: Some("customer@example.com".to_string()),
1018 ..Default::default()
1019 };
1020 assert_eq!(draft_without_id.get_id(), None);
1021 }
1022}