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!(
812 draft_order.email.as_deref(),
813 Some("bob.norman@example.com")
814 );
815 assert_eq!(draft_order.currency.as_deref(), Some("USD"));
816 assert_eq!(draft_order.total_tax.as_deref(), Some("11.94"));
817 assert_eq!(draft_order.subtotal_price.as_deref(), Some("398.00"));
818 assert_eq!(draft_order.total_price.as_deref(), Some("409.94"));
819 assert!(draft_order.created_at.is_some());
820 assert!(draft_order.updated_at.is_some());
821
822 let line_items = draft_order.line_items.unwrap();
824 assert_eq!(line_items.len(), 1);
825 assert_eq!(line_items[0].id, Some(994118540));
826 assert_eq!(line_items[0].title.as_deref(), Some("IPod Nano - 8GB"));
827 assert_eq!(line_items[0].quantity, Some(1));
828 assert_eq!(line_items[0].price.as_deref(), Some("199.00"));
829
830 let shipping = draft_order.shipping_address.unwrap();
832 assert_eq!(shipping.first_name.as_deref(), Some("Bob"));
833 assert_eq!(shipping.city.as_deref(), Some("Louisville"));
834
835 let billing = draft_order.billing_address.unwrap();
837 assert_eq!(billing.first_name.as_deref(), Some("Bob"));
838 }
839
840 #[test]
841 fn test_draft_order_status_enum_serialization() {
842 let open_str = serde_json::to_string(&DraftOrderStatus::Open).unwrap();
844 assert_eq!(open_str, "\"open\"");
845
846 let invoice_sent_str = serde_json::to_string(&DraftOrderStatus::InvoiceSent).unwrap();
847 assert_eq!(invoice_sent_str, "\"invoice_sent\"");
848
849 let completed_str = serde_json::to_string(&DraftOrderStatus::Completed).unwrap();
850 assert_eq!(completed_str, "\"completed\"");
851
852 let open: DraftOrderStatus = serde_json::from_str("\"open\"").unwrap();
854 let invoice_sent: DraftOrderStatus = serde_json::from_str("\"invoice_sent\"").unwrap();
855 let completed: DraftOrderStatus = serde_json::from_str("\"completed\"").unwrap();
856
857 assert_eq!(open, DraftOrderStatus::Open);
858 assert_eq!(invoice_sent, DraftOrderStatus::InvoiceSent);
859 assert_eq!(completed, DraftOrderStatus::Completed);
860
861 assert_eq!(DraftOrderStatus::default(), DraftOrderStatus::Open);
863 }
864
865 #[test]
866 fn test_draft_order_path_constants() {
867 let find_path = get_path(DraftOrder::PATHS, ResourceOperation::Find, &["id"]);
869 assert!(find_path.is_some());
870 assert_eq!(find_path.unwrap().template, "draft_orders/{id}");
871 assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
872
873 let all_path = get_path(DraftOrder::PATHS, ResourceOperation::All, &[]);
875 assert!(all_path.is_some());
876 assert_eq!(all_path.unwrap().template, "draft_orders");
877
878 let count_path = get_path(DraftOrder::PATHS, ResourceOperation::Count, &[]);
880 assert!(count_path.is_some());
881 assert_eq!(count_path.unwrap().template, "draft_orders/count");
882
883 let create_path = get_path(DraftOrder::PATHS, ResourceOperation::Create, &[]);
885 assert!(create_path.is_some());
886 assert_eq!(create_path.unwrap().template, "draft_orders");
887 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
888
889 let update_path = get_path(DraftOrder::PATHS, ResourceOperation::Update, &["id"]);
891 assert!(update_path.is_some());
892 assert_eq!(update_path.unwrap().template, "draft_orders/{id}");
893 assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
894
895 let delete_path = get_path(DraftOrder::PATHS, ResourceOperation::Delete, &["id"]);
897 assert!(delete_path.is_some());
898 assert_eq!(delete_path.unwrap().template, "draft_orders/{id}");
899 assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
900
901 assert_eq!(DraftOrder::NAME, "DraftOrder");
903 assert_eq!(DraftOrder::PLURAL, "draft_orders");
904 }
905
906 #[test]
907 fn test_complete_method_signature() {
908 fn _assert_complete_signature<F, Fut>(f: F)
910 where
911 F: Fn(&DraftOrder, &RestClient, Option<DraftOrderCompleteParams>) -> Fut,
912 Fut: std::future::Future<Output = Result<DraftOrder, ResourceError>>,
913 {
914 let _ = f;
915 }
916
917 let params = DraftOrderCompleteParams {
919 payment_pending: Some(true),
920 };
921 assert_eq!(params.payment_pending, Some(true));
922
923 let draft_without_id = DraftOrder::default();
925 assert!(draft_without_id.get_id().is_none());
926 }
927
928 #[test]
929 fn test_send_invoice_method_signature() {
930 fn _assert_send_invoice_signature<F, Fut>(f: F)
932 where
933 F: Fn(&DraftOrder, &RestClient, DraftOrderInvoice) -> Fut,
934 Fut: std::future::Future<Output = Result<DraftOrder, ResourceError>>,
935 {
936 let _ = f;
937 }
938
939 let invoice = DraftOrderInvoice {
941 to: Some("customer@example.com".to_string()),
942 from: Some("store@example.com".to_string()),
943 subject: Some("Your order".to_string()),
944 custom_message: Some("Thanks!".to_string()),
945 bcc: Some(vec!["admin@example.com".to_string()]),
946 };
947
948 let json = serde_json::to_value(&invoice).unwrap();
949 assert_eq!(json["to"], "customer@example.com");
950 assert_eq!(json["from"], "store@example.com");
951 assert_eq!(json["subject"], "Your order");
952 assert_eq!(json["custom_message"], "Thanks!");
953 assert_eq!(json["bcc"][0], "admin@example.com");
954 }
955
956 #[test]
957 fn test_draft_order_line_item_with_applied_discount() {
958 let line_item = DraftOrderLineItem {
959 id: Some(123),
960 variant_id: Some(456),
961 product_id: Some(789),
962 title: Some("Test Product".to_string()),
963 quantity: Some(2),
964 price: Some("50.00".to_string()),
965 taxable: Some(true),
966 applied_discount: Some(AppliedDiscount {
967 title: Some("10% Off".to_string()),
968 description: Some("Wholesale discount".to_string()),
969 value: Some("10".to_string()),
970 value_type: Some("percentage".to_string()),
971 amount: Some("10.00".to_string()),
972 }),
973 ..Default::default()
974 };
975
976 let json = serde_json::to_value(&line_item).unwrap();
977 assert_eq!(json["id"], 123);
978 assert_eq!(json["title"], "Test Product");
979 assert_eq!(json["quantity"], 2);
980 assert!(json.get("applied_discount").is_some());
981 assert_eq!(json["applied_discount"]["title"], "10% Off");
982 assert_eq!(json["applied_discount"]["value"], "10");
983 assert_eq!(json["applied_discount"]["value_type"], "percentage");
984 }
985
986 #[test]
987 fn test_draft_order_invoice_serialization() {
988 let invoice = DraftOrderInvoice {
989 to: Some("customer@example.com".to_string()),
990 subject: Some("Invoice for your order".to_string()),
991 custom_message: Some("Thank you for shopping with us!".to_string()),
992 ..Default::default()
993 };
994
995 let json = serde_json::to_value(&invoice).unwrap();
996
997 assert_eq!(json["to"], "customer@example.com");
998 assert_eq!(json["subject"], "Invoice for your order");
999 assert_eq!(json["custom_message"], "Thank you for shopping with us!");
1000 assert!(json.get("from").is_none());
1001 assert!(json.get("bcc").is_none());
1002
1003 let empty_invoice = DraftOrderInvoice::default();
1005 let empty_json = serde_json::to_value(&empty_invoice).unwrap();
1006 assert_eq!(empty_json, serde_json::json!({}));
1007 }
1008
1009 #[test]
1010 fn test_draft_order_get_id_returns_correct_value() {
1011 let draft_with_id = DraftOrder {
1012 id: Some(994118539),
1013 name: Some("#D2".to_string()),
1014 ..Default::default()
1015 };
1016 assert_eq!(draft_with_id.get_id(), Some(994118539));
1017
1018 let draft_without_id = DraftOrder {
1019 id: None,
1020 email: Some("customer@example.com".to_string()),
1021 ..Default::default()
1022 };
1023 assert_eq!(draft_without_id.get_id(), None);
1024 }
1025}