Skip to main content

shopify_sdk/rest/resources/v2025_10/
draft_order.rs

1//! DraftOrder resource implementation.
2//!
3//! This module provides the [`DraftOrder`] resource for managing draft orders in Shopify.
4//! Draft orders are used for B2B/wholesale order creation workflows where merchants
5//! can create orders on behalf of customers before payment is completed.
6//!
7//! # Resource-Specific Operations
8//!
9//! In addition to standard CRUD operations, the DraftOrder resource provides:
10//! - [`DraftOrder::complete`] - Convert a draft order to an actual order
11//! - [`DraftOrder::send_invoice`] - Send an invoice email to the customer
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use shopify_sdk::rest::{RestResource, ResourceResponse};
17//! use shopify_sdk::rest::resources::v2025_10::{
18//!     DraftOrder, DraftOrderListParams, DraftOrderStatus,
19//!     DraftOrderLineItem, AppliedDiscount, DraftOrderInvoice,
20//!     DraftOrderCompleteParams
21//! };
22//!
23//! // Create a draft order
24//! let mut draft = DraftOrder {
25//!     line_items: Some(vec![DraftOrderLineItem {
26//!         variant_id: Some(123456),
27//!         quantity: Some(2),
28//!         ..Default::default()
29//!     }]),
30//!     customer_id: Some(789012),
31//!     ..Default::default()
32//! };
33//! let saved = draft.save(&client).await?;
34//!
35//! // Send an invoice to the customer
36//! let invoice = DraftOrderInvoice {
37//!     to: Some("customer@example.com".to_string()),
38//!     subject: Some("Your order is ready".to_string()),
39//!     custom_message: Some("Thanks for your order!".to_string()),
40//!     ..Default::default()
41//! };
42//! let updated = saved.send_invoice(&client, invoice).await?;
43//!
44//! // Complete the draft order to create an actual order
45//! let params = DraftOrderCompleteParams {
46//!     payment_pending: Some(true),
47//! };
48//! let completed = updated.complete(&client, Some(params)).await?;
49//! println!("Created order ID: {:?}", completed.order_id);
50//! ```
51
52use 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/// The status of a draft order.
63///
64/// Indicates the current state of the draft order.
65#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
66#[serde(rename_all = "snake_case")]
67pub enum DraftOrderStatus {
68    /// The draft order is open and can be edited.
69    #[default]
70    Open,
71    /// An invoice has been sent to the customer.
72    InvoiceSent,
73    /// The draft order has been completed and converted to an order.
74    Completed,
75}
76
77/// A discount applied to a draft order or line item.
78///
79/// Used for applying custom discounts to draft orders.
80#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
81pub struct AppliedDiscount {
82    /// The title of the discount.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub title: Option<String>,
85
86    /// A description of the discount.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub description: Option<String>,
89
90    /// The discount value (numeric).
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub value: Option<String>,
93
94    /// The type of value: "percentage" or "fixed_amount".
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub value_type: Option<String>,
97
98    /// The calculated discount amount.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub amount: Option<String>,
101}
102
103/// A line item in a draft order.
104///
105/// Represents a product or custom item to be included in the draft order.
106#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
107pub struct DraftOrderLineItem {
108    /// The unique identifier of the line item.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub id: Option<u64>,
111
112    /// The ID of the product variant.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub variant_id: Option<u64>,
115
116    /// The ID of the product.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub product_id: Option<u64>,
119
120    /// The title of the product.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub title: Option<String>,
123
124    /// The title of the variant.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub variant_title: Option<String>,
127
128    /// The SKU of the variant.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub sku: Option<String>,
131
132    /// The vendor of the product.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub vendor: Option<String>,
135
136    /// The quantity of items.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub quantity: Option<i64>,
139
140    /// Whether the item requires shipping.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub requires_shipping: Option<bool>,
143
144    /// Whether the item is taxable.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub taxable: Option<bool>,
147
148    /// Whether the item is a gift card.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub gift_card: Option<bool>,
151
152    /// The fulfillment service for the item.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub fulfillment_service: Option<String>,
155
156    /// The weight in grams.
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub grams: Option<i64>,
159
160    /// Tax lines applied to this line item.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub tax_lines: Option<Vec<TaxLine>>,
163
164    /// The name of the line item.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub name: Option<String>,
167
168    /// Custom properties on the line item.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub properties: Option<Vec<serde_json::Value>>,
171
172    /// Whether the custom line item (not a product variant).
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub custom: Option<bool>,
175
176    /// The price per item as a string.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub price: Option<String>,
179
180    /// A discount applied to this specific line item.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub applied_discount: Option<AppliedDiscount>,
183
184    /// The admin GraphQL API ID.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub admin_graphql_api_id: Option<String>,
187}
188
189/// Invoice details for sending to a customer.
190///
191/// Used with the [`DraftOrder::send_invoice`] operation.
192#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
193pub struct DraftOrderInvoice {
194    /// The email address to send the invoice to.
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub to: Option<String>,
197
198    /// The email address the invoice is sent from.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub from: Option<String>,
201
202    /// The subject line of the email.
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub subject: Option<String>,
205
206    /// A custom message included in the invoice email.
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub custom_message: Option<String>,
209
210    /// Email addresses to BCC on the invoice.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub bcc: Option<Vec<String>>,
213}
214
215/// Parameters for completing a draft order.
216///
217/// Used with the [`DraftOrder::complete`] operation.
218#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
219pub struct DraftOrderCompleteParams {
220    /// Whether to mark the payment as pending.
221    /// If true, the order is created with payment pending status.
222    /// If false or omitted, the order is marked as paid.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub payment_pending: Option<bool>,
225}
226
227/// A draft order in Shopify.
228///
229/// Draft orders are used for B2B/wholesale workflows where merchants create
230/// orders on behalf of customers. They support custom pricing, discounts,
231/// and can be completed to become actual orders.
232///
233/// # Status Lifecycle
234///
235/// - `Open` - Initial state, can be edited
236/// - `InvoiceSent` - Invoice sent to customer via `send_invoice()`
237/// - `Completed` - Converted to actual order via `complete()`
238///
239/// # Read-Only Fields
240///
241/// The following fields are read-only and will not be sent in create/update requests:
242/// - `id`, `order_id`, `name`
243/// - `invoice_sent_at`, `invoice_url`
244/// - `completed_at`, `created_at`, `updated_at`
245/// - `admin_graphql_api_id`
246///
247/// # Example
248///
249/// ```rust,ignore
250/// use shopify_sdk::rest::resources::v2025_10::{DraftOrder, DraftOrderLineItem};
251///
252/// let draft = DraftOrder {
253///     line_items: Some(vec![DraftOrderLineItem {
254///         variant_id: Some(123456),
255///         quantity: Some(2),
256///         ..Default::default()
257///     }]),
258///     note: Some("Wholesale order".to_string()),
259///     ..Default::default()
260/// };
261/// ```
262#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
263pub struct DraftOrder {
264    // --- Read-only fields (not serialized) ---
265    /// The unique identifier of the draft order.
266    #[serde(skip_serializing)]
267    pub id: Option<u64>,
268
269    /// The ID of the order created when this draft was completed.
270    #[serde(skip_serializing)]
271    pub order_id: Option<u64>,
272
273    /// The name of the draft order (e.g., "#D1").
274    #[serde(skip_serializing)]
275    pub name: Option<String>,
276
277    /// When an invoice was last sent for this draft order.
278    #[serde(skip_serializing)]
279    pub invoice_sent_at: Option<DateTime<Utc>>,
280
281    /// The URL to the invoice for this draft order.
282    #[serde(skip_serializing)]
283    pub invoice_url: Option<String>,
284
285    /// When the draft order was completed.
286    #[serde(skip_serializing)]
287    pub completed_at: Option<DateTime<Utc>>,
288
289    /// When the draft order was created.
290    #[serde(skip_serializing)]
291    pub created_at: Option<DateTime<Utc>>,
292
293    /// When the draft order was last updated.
294    #[serde(skip_serializing)]
295    pub updated_at: Option<DateTime<Utc>>,
296
297    /// The admin GraphQL API ID.
298    #[serde(skip_serializing)]
299    pub admin_graphql_api_id: Option<String>,
300
301    // --- Core fields ---
302    /// The status of the draft order.
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub status: Option<DraftOrderStatus>,
305
306    /// The customer's email address.
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub email: Option<String>,
309
310    /// The currency code for the draft order (e.g., "USD").
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub currency: Option<String>,
313
314    /// Whether the customer is tax exempt.
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub tax_exempt: Option<bool>,
317
318    /// Tax exemptions applied to the customer.
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub tax_exemptions: Option<Vec<String>>,
321
322    /// Whether taxes are included in the prices.
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub taxes_included: Option<bool>,
325
326    /// The total tax amount.
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub total_tax: Option<String>,
329
330    /// The subtotal price before taxes and shipping.
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub subtotal_price: Option<String>,
333
334    /// The total price including taxes and shipping.
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub total_price: Option<String>,
337
338    /// An optional note attached to the draft order.
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub note: Option<String>,
341
342    /// Custom note attributes (key-value pairs).
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub note_attributes: Option<Vec<NoteAttribute>>,
345
346    /// Comma-separated tags.
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub tags: Option<String>,
349
350    /// The ID of the customer associated with this draft order.
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub customer_id: Option<u64>,
353
354    /// Whether to use the customer's default address.
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub use_customer_default_address: Option<bool>,
357
358    // --- Nested structures ---
359    /// Line items in the draft order.
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub line_items: Option<Vec<DraftOrderLineItem>>,
362
363    /// The shipping address.
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub shipping_address: Option<Address>,
366
367    /// The billing address.
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub billing_address: Option<Address>,
370
371    /// The customer associated with this draft order.
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub customer: Option<Customer>,
374
375    /// The shipping line for the draft order.
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub shipping_line: Option<ShippingLine>,
378
379    /// Tax lines for the draft order.
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub tax_lines: Option<Vec<TaxLine>>,
382
383    /// A discount applied to the entire draft order.
384    #[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    /// Paths for the DraftOrder resource.
398    ///
399    /// DraftOrder uses standalone paths (not nested under other resources).
400    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    /// Completes the draft order and converts it to an actual order.
441    ///
442    /// Sends a PUT request to `/admin/api/{version}/draft_orders/{id}/complete.json`.
443    ///
444    /// # Arguments
445    ///
446    /// * `client` - The REST client to use for the request
447    /// * `params` - Optional parameters including `payment_pending`
448    ///
449    /// # Returns
450    ///
451    /// The completed draft order with the `order_id` field populated.
452    ///
453    /// # Errors
454    ///
455    /// Returns [`ResourceError::NotFound`] if the draft order doesn't exist.
456    /// Returns [`ResourceError::PathResolutionFailed`] if the draft order has no ID.
457    ///
458    /// # Example
459    ///
460    /// ```rust,ignore
461    /// let params = DraftOrderCompleteParams {
462    ///     payment_pending: Some(true),
463    /// };
464    /// let completed = draft_order.complete(&client, Some(params)).await?;
465    /// println!("Created order ID: {:?}", completed.order_id);
466    /// ```
467    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        // Build query string for payment_pending parameter
478        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        // Parse the response - Shopify returns the draft order wrapped in "draft_order" key
500        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    /// Sends an invoice for the draft order to the customer.
528    ///
529    /// Sends a POST request to `/admin/api/{version}/draft_orders/{id}/send_invoice.json`.
530    ///
531    /// # Arguments
532    ///
533    /// * `client` - The REST client to use for the request
534    /// * `invoice` - Invoice details including recipient, subject, and message
535    ///
536    /// # Returns
537    ///
538    /// The updated draft order with `invoice_sent_at` and `invoice_url` populated.
539    ///
540    /// # Errors
541    ///
542    /// Returns [`ResourceError::NotFound`] if the draft order doesn't exist.
543    /// Returns [`ResourceError::PathResolutionFailed`] if the draft order has no ID.
544    ///
545    /// # Example
546    ///
547    /// ```rust,ignore
548    /// let invoice = DraftOrderInvoice {
549    ///     to: Some("customer@example.com".to_string()),
550    ///     subject: Some("Your order is ready".to_string()),
551    ///     custom_message: Some("Thank you for your order!".to_string()),
552    ///     ..Default::default()
553    /// };
554    /// let updated = draft_order.send_invoice(&client, invoice).await?;
555    /// println!("Invoice URL: {:?}", updated.invoice_url);
556    /// ```
557    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        // Parse the response - Shopify returns the draft order wrapped in "draft_order" key
586        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/// Parameters for finding a single draft order.
615#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
616pub struct DraftOrderFindParams {
617    /// Comma-separated list of fields to include in the response.
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub fields: Option<String>,
620}
621
622/// Parameters for listing draft orders.
623///
624/// All fields are optional. Unset fields will not be included in the request.
625#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
626pub struct DraftOrderListParams {
627    /// Maximum number of results to return (default: 50, max: 250).
628    #[serde(skip_serializing_if = "Option::is_none")]
629    pub limit: Option<u32>,
630
631    /// Return only draft orders after the specified ID.
632    #[serde(skip_serializing_if = "Option::is_none")]
633    pub since_id: Option<u64>,
634
635    /// Show draft orders created at or after this date.
636    #[serde(skip_serializing_if = "Option::is_none")]
637    pub created_at_min: Option<DateTime<Utc>>,
638
639    /// Show draft orders created at or before this date.
640    #[serde(skip_serializing_if = "Option::is_none")]
641    pub created_at_max: Option<DateTime<Utc>>,
642
643    /// Show draft orders last updated at or after this date.
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub updated_at_min: Option<DateTime<Utc>>,
646
647    /// Show draft orders last updated at or before this date.
648    #[serde(skip_serializing_if = "Option::is_none")]
649    pub updated_at_max: Option<DateTime<Utc>>,
650
651    /// Comma-separated list of draft order IDs to retrieve.
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub ids: Option<Vec<u64>>,
654
655    /// Filter by draft order status (open, invoice_sent, completed).
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub status: Option<DraftOrderStatus>,
658
659    /// Comma-separated list of fields to include in the response.
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub fields: Option<String>,
662
663    /// Page info for cursor-based pagination.
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub page_info: Option<String>,
666}
667
668/// Parameters for counting draft orders.
669#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
670pub struct DraftOrderCountParams {
671    /// Return only draft orders after the specified ID.
672    #[serde(skip_serializing_if = "Option::is_none")]
673    pub since_id: Option<u64>,
674
675    /// Filter by draft order status.
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub status: Option<DraftOrderStatus>,
678
679    /// Show draft orders created at or after this date.
680    #[serde(skip_serializing_if = "Option::is_none")]
681    pub created_at_min: Option<DateTime<Utc>>,
682
683    /// Show draft orders created at or before this date.
684    #[serde(skip_serializing_if = "Option::is_none")]
685    pub created_at_max: Option<DateTime<Utc>>,
686
687    /// Show draft orders last updated at or after this date.
688    #[serde(skip_serializing_if = "Option::is_none")]
689    pub updated_at_min: Option<DateTime<Utc>>,
690
691    /// Show draft orders last updated at or before this date.
692    #[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        // Writable fields should be present
729        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        // Read-only fields should NOT be serialized
741        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        // Check line items
823        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        // Check shipping address
831        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        // Check billing address
836        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        // Test serialization to snake_case
843        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        // Test deserialization
853        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        // Test default
862        assert_eq!(DraftOrderStatus::default(), DraftOrderStatus::Open);
863    }
864
865    #[test]
866    fn test_draft_order_path_constants() {
867        // Test Find path
868        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        // Test All path
874        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        // Test Count path
879        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        // Test Create path
884        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        // Test Update path
890        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        // Test Delete path
896        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        // Verify constants
902        assert_eq!(DraftOrder::NAME, "DraftOrder");
903        assert_eq!(DraftOrder::PLURAL, "draft_orders");
904    }
905
906    #[test]
907    fn test_complete_method_signature() {
908        // Verify the complete method signature compiles correctly
909        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        // Verify DraftOrderCompleteParams
918        let params = DraftOrderCompleteParams {
919            payment_pending: Some(true),
920        };
921        assert_eq!(params.payment_pending, Some(true));
922
923        // Verify PathResolutionFailed error is returned when draft order has no ID
924        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        // Verify the send_invoice method signature compiles correctly
931        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        // Verify DraftOrderInvoice struct
940        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        // Test empty invoice
1004        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}