Skip to main content

shopify_sdk/rest/resources/v2025_10/
gift_card.rs

1//! GiftCard resource implementation.
2//!
3//! This module provides the [`GiftCard`] resource for managing gift cards in Shopify.
4//! Gift cards are a Shopify Plus feature that allow merchants to sell store credit.
5//!
6//! # Scope Requirements
7//!
8//! **Important**: The `read_gift_cards` and `write_gift_cards` scopes require
9//! approval from Shopify Support. Contact Shopify Partner Support to request
10//! access to these scopes for your app.
11//!
12//! # Resource-Specific Operations
13//!
14//! In addition to standard CRUD operations (no Delete), the GiftCard resource provides:
15//! - [`GiftCard::disable`] - Disable a gift card (cannot be re-enabled)
16//! - [`GiftCard::search`] - Search for gift cards by query
17//!
18//! # Field Constraints
19//!
20//! - `initial_value` is required when creating a gift card
21//! - `code` is write-only (only `last_characters` is readable after creation)
22//! - `code` is auto-generated if not provided; must be 8-20 alphanumeric chars if provided
23//! - Only `expires_on`, `note`, `template_suffix` are updatable after creation
24//! - `customer_id` can only be set if currently null
25//! - There is no Delete operation - use `disable()` instead
26//! - Gift cards cannot be re-enabled after being disabled
27//!
28//! # Example
29//!
30//! ```rust,ignore
31//! use shopify_sdk::rest::{RestResource, ResourceResponse};
32//! use shopify_sdk::rest::resources::v2025_10::{GiftCard, GiftCardListParams};
33//!
34//! // Create a gift card
35//! let mut gift_card = GiftCard {
36//!     initial_value: Some("100.00".to_string()),
37//!     note: Some("Employee reward".to_string()),
38//!     ..Default::default()
39//! };
40//! let saved = gift_card.save(&client).await?;
41//! println!("Gift card created with last chars: {:?}", saved.last_characters);
42//!
43//! // Search for gift cards
44//! let results = GiftCard::search(&client, "employee").await?;
45//!
46//! // Disable a gift card
47//! let disabled = saved.disable(&client).await?;
48//! println!("Disabled at: {:?}", disabled.disabled_at);
49//! ```
50
51use chrono::{DateTime, NaiveDate, Utc};
52use serde::{Deserialize, Serialize};
53
54use crate::clients::RestClient;
55use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
56use crate::HttpMethod;
57
58/// A gift card in Shopify.
59///
60/// Gift cards are a Shopify Plus feature that allow merchants to sell
61/// or give away store credit. Gift card codes can be redeemed at checkout.
62///
63/// # Scope Requirements
64///
65/// The `read_gift_cards` and `write_gift_cards` scopes require approval
66/// from Shopify Support. Contact support to request access for your app.
67///
68/// # Read-Only Fields
69///
70/// The following fields are read-only and will not be sent in create/update requests:
71/// - `id`, `balance`
72/// - `disabled_at`, `line_item_id`, `api_client_id`, `user_id`
73/// - `last_characters`, `order_id`
74/// - `created_at`, `updated_at`
75/// - `admin_graphql_api_id`
76///
77/// # Write-Only Fields
78///
79/// The following fields are write-only (only for creation):
80/// - `code` - The gift card code (auto-generated if not provided)
81///
82/// # Updatable Fields
83///
84/// After creation, only these fields can be updated:
85/// - `expires_on` - Expiration date
86/// - `note` - Internal note
87/// - `template_suffix` - Template suffix for rendering
88/// - `customer_id` - Only if currently null
89///
90/// # Example
91///
92/// ```rust,ignore
93/// use shopify_sdk::rest::resources::v2025_10::GiftCard;
94///
95/// let gift_card = GiftCard {
96///     initial_value: Some("50.00".to_string()),
97///     note: Some("Birthday gift".to_string()),
98///     customer_id: Some(123456),
99///     ..Default::default()
100/// };
101/// ```
102#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
103pub struct GiftCard {
104    // --- Read-only fields (not serialized) ---
105    /// The unique identifier of the gift card.
106    #[serde(skip_serializing)]
107    pub id: Option<u64>,
108
109    /// The current balance of the gift card.
110    #[serde(skip_serializing)]
111    pub balance: Option<String>,
112
113    /// When the gift card was disabled (null if still enabled).
114    /// A gift card is disabled if this field is set.
115    #[serde(skip_serializing)]
116    pub disabled_at: Option<DateTime<Utc>>,
117
118    /// The ID of the line item that created this gift card.
119    #[serde(skip_serializing)]
120    pub line_item_id: Option<u64>,
121
122    /// The ID of the API client that created this gift card.
123    #[serde(skip_serializing)]
124    pub api_client_id: Option<u64>,
125
126    /// The ID of the user who created this gift card.
127    #[serde(skip_serializing)]
128    pub user_id: Option<u64>,
129
130    /// The last four characters of the gift card code.
131    /// This is the only way to identify the code after creation.
132    #[serde(skip_serializing)]
133    pub last_characters: Option<String>,
134
135    /// The ID of the order that created this gift card (if any).
136    #[serde(skip_serializing)]
137    pub order_id: Option<u64>,
138
139    /// When the gift card was created.
140    #[serde(skip_serializing)]
141    pub created_at: Option<DateTime<Utc>>,
142
143    /// When the gift card was last updated.
144    #[serde(skip_serializing)]
145    pub updated_at: Option<DateTime<Utc>>,
146
147    /// The admin GraphQL API ID.
148    #[serde(skip_serializing)]
149    pub admin_graphql_api_id: Option<String>,
150
151    // --- Write-only fields (only deserialized, not serialized back) ---
152    // Note: We use a custom approach here - code is included in serialization
153    // for create, but won't be returned in responses
154    /// The gift card code.
155    ///
156    /// **Write-only**: This field is only used when creating a gift card.
157    /// After creation, only `last_characters` is available.
158    ///
159    /// If not provided, Shopify auto-generates a code.
160    /// If provided, must be 8-20 alphanumeric characters.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub code: Option<String>,
163
164    // --- Writable fields ---
165    /// The initial value of the gift card.
166    ///
167    /// **Required for creation.**
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub initial_value: Option<String>,
170
171    /// The currency code for the gift card (e.g., "USD").
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub currency: Option<String>,
174
175    /// The ID of the customer this gift card is associated with.
176    ///
177    /// Can only be set if currently null.
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub customer_id: Option<u64>,
180
181    /// An optional note attached to the gift card.
182    ///
183    /// Updatable after creation.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub note: Option<String>,
186
187    /// When the gift card expires.
188    ///
189    /// Updatable after creation.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub expires_on: Option<NaiveDate>,
192
193    /// The template suffix for rendering the gift card.
194    ///
195    /// Updatable after creation.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub template_suffix: Option<String>,
198}
199
200impl GiftCard {
201    /// Returns whether the gift card is currently enabled (not disabled).
202    ///
203    /// A gift card is considered enabled if `disabled_at` is `None`.
204    pub fn is_enabled(&self) -> bool {
205        self.disabled_at.is_none()
206    }
207
208    /// Returns whether the gift card is currently disabled.
209    ///
210    /// A gift card is considered disabled if `disabled_at` is set.
211    pub fn is_disabled(&self) -> bool {
212        self.disabled_at.is_some()
213    }
214}
215
216impl RestResource for GiftCard {
217    type Id = u64;
218    type FindParams = GiftCardFindParams;
219    type AllParams = GiftCardListParams;
220    type CountParams = GiftCardCountParams;
221
222    const NAME: &'static str = "GiftCard";
223    const PLURAL: &'static str = "gift_cards";
224
225    /// Paths for the GiftCard resource.
226    ///
227    /// Note: GiftCard does NOT have a Delete operation.
228    /// Use `disable()` to deactivate a gift card instead.
229    const PATHS: &'static [ResourcePath] = &[
230        ResourcePath::new(
231            HttpMethod::Get,
232            ResourceOperation::Find,
233            &["id"],
234            "gift_cards/{id}",
235        ),
236        ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "gift_cards"),
237        ResourcePath::new(
238            HttpMethod::Get,
239            ResourceOperation::Count,
240            &[],
241            "gift_cards/count",
242        ),
243        ResourcePath::new(
244            HttpMethod::Post,
245            ResourceOperation::Create,
246            &[],
247            "gift_cards",
248        ),
249        ResourcePath::new(
250            HttpMethod::Put,
251            ResourceOperation::Update,
252            &["id"],
253            "gift_cards/{id}",
254        ),
255        // No Delete path - use disable() instead
256    ];
257
258    fn get_id(&self) -> Option<Self::Id> {
259        self.id
260    }
261}
262
263impl GiftCard {
264    /// Disables the gift card.
265    ///
266    /// Sends a POST request to `/admin/api/{version}/gift_cards/{id}/disable.json`.
267    ///
268    /// **Note**: Once disabled, a gift card cannot be re-enabled.
269    ///
270    /// # Arguments
271    ///
272    /// * `client` - The REST client to use for the request
273    ///
274    /// # Returns
275    ///
276    /// The gift card with `disabled_at` populated.
277    ///
278    /// # Errors
279    ///
280    /// Returns [`ResourceError::NotFound`] if the gift card doesn't exist.
281    /// Returns [`ResourceError::PathResolutionFailed`] if the gift card has no ID.
282    ///
283    /// # Example
284    ///
285    /// ```rust,ignore
286    /// let gift_card = GiftCard::find(&client, 123, None).await?.into_inner();
287    /// let disabled = gift_card.disable(&client).await?;
288    /// assert!(disabled.is_disabled());
289    /// ```
290    pub async fn disable(&self, client: &RestClient) -> Result<Self, ResourceError> {
291        let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
292            resource: Self::NAME,
293            operation: "disable",
294        })?;
295
296        let path = format!("gift_cards/{id}/disable");
297        let body = serde_json::json!({});
298
299        let response = client.post(&path, body, None).await?;
300
301        if !response.is_ok() {
302            return Err(ResourceError::from_http_response(
303                response.code,
304                &response.body,
305                Self::NAME,
306                Some(&id.to_string()),
307                response.request_id(),
308            ));
309        }
310
311        // Parse the response - Shopify returns the gift card wrapped in "gift_card" key
312        let gift_card: Self = response
313            .body
314            .get("gift_card")
315            .ok_or_else(|| {
316                ResourceError::Http(crate::clients::HttpError::Response(
317                    crate::clients::HttpResponseError {
318                        code: response.code,
319                        message: "Missing 'gift_card' in response".to_string(),
320                        error_reference: response.request_id().map(ToString::to_string),
321                    },
322                ))
323            })
324            .and_then(|v| {
325                serde_json::from_value(v.clone()).map_err(|e| {
326                    ResourceError::Http(crate::clients::HttpError::Response(
327                        crate::clients::HttpResponseError {
328                            code: response.code,
329                            message: format!("Failed to deserialize gift_card: {e}"),
330                            error_reference: response.request_id().map(ToString::to_string),
331                        },
332                    ))
333                })
334            })?;
335
336        Ok(gift_card)
337    }
338
339    /// Searches for gift cards matching the query.
340    ///
341    /// Sends a GET request to `/admin/api/{version}/gift_cards/search.json?query={query}`.
342    ///
343    /// # Arguments
344    ///
345    /// * `client` - The REST client to use for the request
346    /// * `query` - The search query string
347    ///
348    /// # Returns
349    ///
350    /// A list of gift cards matching the search query.
351    ///
352    /// # Example
353    ///
354    /// ```rust,ignore
355    /// // Search by last characters of code
356    /// let results = GiftCard::search(&client, "abc1").await?;
357    ///
358    /// // Search by note content
359    /// let results = GiftCard::search(&client, "birthday").await?;
360    /// ```
361    pub async fn search(client: &RestClient, query: &str) -> Result<Vec<Self>, ResourceError> {
362        let path = format!("gift_cards/search");
363
364        let mut query_params = std::collections::HashMap::new();
365        query_params.insert("query".to_string(), query.to_string());
366
367        let response = client.get(&path, Some(query_params)).await?;
368
369        if !response.is_ok() {
370            return Err(ResourceError::from_http_response(
371                response.code,
372                &response.body,
373                Self::NAME,
374                None,
375                response.request_id(),
376            ));
377        }
378
379        // Parse the response - Shopify returns gift cards wrapped in "gift_cards" key
380        let gift_cards: Vec<Self> = response
381            .body
382            .get("gift_cards")
383            .ok_or_else(|| {
384                ResourceError::Http(crate::clients::HttpError::Response(
385                    crate::clients::HttpResponseError {
386                        code: response.code,
387                        message: "Missing 'gift_cards' in response".to_string(),
388                        error_reference: response.request_id().map(ToString::to_string),
389                    },
390                ))
391            })
392            .and_then(|v| {
393                serde_json::from_value(v.clone()).map_err(|e| {
394                    ResourceError::Http(crate::clients::HttpError::Response(
395                        crate::clients::HttpResponseError {
396                            code: response.code,
397                            message: format!("Failed to deserialize gift_cards: {e}"),
398                            error_reference: response.request_id().map(ToString::to_string),
399                        },
400                    ))
401                })
402            })?;
403
404        Ok(gift_cards)
405    }
406}
407
408/// Parameters for finding a single gift card.
409#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
410pub struct GiftCardFindParams {
411    /// Comma-separated list of fields to include in the response.
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub fields: Option<String>,
414}
415
416/// Parameters for listing gift cards.
417///
418/// All fields are optional. Unset fields will not be included in the request.
419#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
420pub struct GiftCardListParams {
421    /// Maximum number of results to return (default: 50, max: 250).
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub limit: Option<u32>,
424
425    /// Return only gift cards after the specified ID.
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub since_id: Option<u64>,
428
429    /// Filter by status: "enabled" or "disabled".
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub status: Option<String>,
432
433    /// Comma-separated list of fields to include in the response.
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub fields: Option<String>,
436
437    /// Page info for cursor-based pagination.
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub page_info: Option<String>,
440}
441
442/// Parameters for counting gift cards.
443#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
444pub struct GiftCardCountParams {
445    /// Filter by status: "enabled" or "disabled".
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub status: Option<String>,
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use crate::rest::{get_path, ResourceOperation};
454
455    #[test]
456    fn test_gift_card_struct_serialization() {
457        let gift_card = GiftCard {
458            id: Some(123456789),
459            balance: Some("75.00".to_string()),
460            disabled_at: None,
461            line_item_id: Some(111),
462            api_client_id: Some(222),
463            user_id: Some(333),
464            last_characters: Some("abc1".to_string()),
465            order_id: Some(444),
466            created_at: Some(
467                DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
468                    .unwrap()
469                    .with_timezone(&Utc),
470            ),
471            updated_at: Some(
472                DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
473                    .unwrap()
474                    .with_timezone(&Utc),
475            ),
476            admin_graphql_api_id: Some("gid://shopify/GiftCard/123456789".to_string()),
477            code: Some("GIFT1234ABCD5678".to_string()),
478            initial_value: Some("100.00".to_string()),
479            currency: Some("USD".to_string()),
480            customer_id: Some(789012),
481            note: Some("Employee reward".to_string()),
482            expires_on: Some(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()),
483            template_suffix: Some("premium".to_string()),
484        };
485
486        let json = serde_json::to_string(&gift_card).unwrap();
487        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
488
489        // Writable fields should be present
490        assert_eq!(parsed["code"], "GIFT1234ABCD5678");
491        assert_eq!(parsed["initial_value"], "100.00");
492        assert_eq!(parsed["currency"], "USD");
493        assert_eq!(parsed["customer_id"], 789012);
494        assert_eq!(parsed["note"], "Employee reward");
495        assert_eq!(parsed["expires_on"], "2025-12-31");
496        assert_eq!(parsed["template_suffix"], "premium");
497
498        // Read-only fields should NOT be serialized
499        assert!(parsed.get("id").is_none());
500        assert!(parsed.get("balance").is_none());
501        assert!(parsed.get("disabled_at").is_none());
502        assert!(parsed.get("line_item_id").is_none());
503        assert!(parsed.get("api_client_id").is_none());
504        assert!(parsed.get("user_id").is_none());
505        assert!(parsed.get("last_characters").is_none());
506        assert!(parsed.get("order_id").is_none());
507        assert!(parsed.get("created_at").is_none());
508        assert!(parsed.get("updated_at").is_none());
509        assert!(parsed.get("admin_graphql_api_id").is_none());
510    }
511
512    #[test]
513    fn test_gift_card_deserialization_from_api_response() {
514        let json_str = r#"{
515            "id": 1035197676,
516            "balance": "100.00",
517            "created_at": "2024-01-15T10:30:00Z",
518            "updated_at": "2024-01-15T10:30:00Z",
519            "currency": "USD",
520            "initial_value": "100.00",
521            "disabled_at": null,
522            "line_item_id": 466157049,
523            "api_client_id": 755357713,
524            "user_id": null,
525            "customer_id": 207119551,
526            "note": "Birthday gift for John",
527            "expires_on": "2025-12-31",
528            "template_suffix": null,
529            "last_characters": "0e0e",
530            "order_id": 450789469,
531            "admin_graphql_api_id": "gid://shopify/GiftCard/1035197676"
532        }"#;
533
534        let gift_card: GiftCard = serde_json::from_str(json_str).unwrap();
535
536        assert_eq!(gift_card.id, Some(1035197676));
537        assert_eq!(gift_card.balance.as_deref(), Some("100.00"));
538        assert_eq!(gift_card.currency.as_deref(), Some("USD"));
539        assert_eq!(gift_card.initial_value.as_deref(), Some("100.00"));
540        assert_eq!(gift_card.disabled_at, None);
541        assert_eq!(gift_card.line_item_id, Some(466157049));
542        assert_eq!(gift_card.api_client_id, Some(755357713));
543        assert_eq!(gift_card.user_id, None);
544        assert_eq!(gift_card.customer_id, Some(207119551));
545        assert_eq!(gift_card.note.as_deref(), Some("Birthday gift for John"));
546        assert_eq!(
547            gift_card.expires_on,
548            Some(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap())
549        );
550        assert_eq!(gift_card.template_suffix, None);
551        assert_eq!(gift_card.last_characters.as_deref(), Some("0e0e"));
552        assert_eq!(gift_card.order_id, Some(450789469));
553        assert!(gift_card.created_at.is_some());
554        assert!(gift_card.updated_at.is_some());
555
556        // Code should not be in the response (write-only)
557        assert_eq!(gift_card.code, None);
558    }
559
560    #[test]
561    fn test_gift_card_path_constants() {
562        // Test Find path
563        let find_path = get_path(GiftCard::PATHS, ResourceOperation::Find, &["id"]);
564        assert!(find_path.is_some());
565        assert_eq!(find_path.unwrap().template, "gift_cards/{id}");
566        assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
567
568        // Test All path
569        let all_path = get_path(GiftCard::PATHS, ResourceOperation::All, &[]);
570        assert!(all_path.is_some());
571        assert_eq!(all_path.unwrap().template, "gift_cards");
572
573        // Test Count path
574        let count_path = get_path(GiftCard::PATHS, ResourceOperation::Count, &[]);
575        assert!(count_path.is_some());
576        assert_eq!(count_path.unwrap().template, "gift_cards/count");
577
578        // Test Create path
579        let create_path = get_path(GiftCard::PATHS, ResourceOperation::Create, &[]);
580        assert!(create_path.is_some());
581        assert_eq!(create_path.unwrap().template, "gift_cards");
582        assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
583
584        // Test Update path
585        let update_path = get_path(GiftCard::PATHS, ResourceOperation::Update, &["id"]);
586        assert!(update_path.is_some());
587        assert_eq!(update_path.unwrap().template, "gift_cards/{id}");
588        assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
589
590        // Test that there is NO Delete path
591        let delete_path = get_path(GiftCard::PATHS, ResourceOperation::Delete, &["id"]);
592        assert!(delete_path.is_none());
593
594        // Verify constants
595        assert_eq!(GiftCard::NAME, "GiftCard");
596        assert_eq!(GiftCard::PLURAL, "gift_cards");
597    }
598
599    #[test]
600    fn test_disable_method_signature() {
601        // Verify the disable method signature compiles correctly
602        fn _assert_disable_signature<F, Fut>(f: F)
603        where
604            F: Fn(&GiftCard, &RestClient) -> Fut,
605            Fut: std::future::Future<Output = Result<GiftCard, ResourceError>>,
606        {
607            let _ = f;
608        }
609
610        // Verify PathResolutionFailed error is returned when gift card has no ID
611        let gift_card_without_id = GiftCard::default();
612        assert!(gift_card_without_id.get_id().is_none());
613    }
614
615    #[test]
616    fn test_search_method_signature() {
617        // Verify the search method signature compiles correctly
618        fn _assert_search_signature<F, Fut>(f: F)
619        where
620            F: Fn(&RestClient, &str) -> Fut,
621            Fut: std::future::Future<Output = Result<Vec<GiftCard>, ResourceError>>,
622        {
623            let _ = f;
624        }
625    }
626
627    #[test]
628    fn test_gift_card_is_enabled_disabled() {
629        let enabled_gift_card = GiftCard {
630            id: Some(123),
631            balance: Some("100.00".to_string()),
632            disabled_at: None,
633            ..Default::default()
634        };
635
636        assert!(enabled_gift_card.is_enabled());
637        assert!(!enabled_gift_card.is_disabled());
638
639        let disabled_gift_card = GiftCard {
640            id: Some(456),
641            balance: Some("0.00".to_string()),
642            disabled_at: Some(
643                DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
644                    .unwrap()
645                    .with_timezone(&Utc),
646            ),
647            ..Default::default()
648        };
649
650        assert!(!disabled_gift_card.is_enabled());
651        assert!(disabled_gift_card.is_disabled());
652    }
653
654    #[test]
655    fn test_gift_card_get_id_returns_correct_value() {
656        let gift_card_with_id = GiftCard {
657            id: Some(1035197676),
658            balance: Some("100.00".to_string()),
659            ..Default::default()
660        };
661        assert_eq!(gift_card_with_id.get_id(), Some(1035197676));
662
663        let gift_card_without_id = GiftCard {
664            id: None,
665            initial_value: Some("50.00".to_string()),
666            ..Default::default()
667        };
668        assert_eq!(gift_card_without_id.get_id(), None);
669    }
670
671    #[test]
672    fn test_gift_card_list_params_serialization() {
673        let params = GiftCardListParams {
674            limit: Some(50),
675            since_id: Some(12345),
676            status: Some("enabled".to_string()),
677            fields: Some("id,balance,last_characters".to_string()),
678            page_info: None,
679        };
680
681        let json = serde_json::to_value(&params).unwrap();
682
683        assert_eq!(json["limit"], 50);
684        assert_eq!(json["since_id"], 12345);
685        assert_eq!(json["status"], "enabled");
686        assert_eq!(json["fields"], "id,balance,last_characters");
687        assert!(json.get("page_info").is_none());
688
689        // Test empty params
690        let empty_params = GiftCardListParams::default();
691        let empty_json = serde_json::to_value(&empty_params).unwrap();
692        assert_eq!(empty_json, serde_json::json!({}));
693    }
694
695    #[test]
696    fn test_gift_card_count_params_serialization() {
697        let params = GiftCardCountParams {
698            status: Some("disabled".to_string()),
699        };
700
701        let json = serde_json::to_value(&params).unwrap();
702        assert_eq!(json["status"], "disabled");
703
704        // Test empty params
705        let empty_params = GiftCardCountParams::default();
706        let empty_json = serde_json::to_value(&empty_params).unwrap();
707        assert_eq!(empty_json, serde_json::json!({}));
708    }
709
710    #[test]
711    fn test_gift_card_updatable_fields() {
712        // Test that only updatable fields can be serialized for update
713        let update_gift_card = GiftCard {
714            expires_on: Some(NaiveDate::from_ymd_opt(2026, 6, 30).unwrap()),
715            note: Some("Updated note".to_string()),
716            template_suffix: Some("custom".to_string()),
717            customer_id: Some(999999),
718            ..Default::default()
719        };
720
721        let json = serde_json::to_value(&update_gift_card).unwrap();
722
723        // Updatable fields should be present
724        assert_eq!(json["expires_on"], "2026-06-30");
725        assert_eq!(json["note"], "Updated note");
726        assert_eq!(json["template_suffix"], "custom");
727        assert_eq!(json["customer_id"], 999999);
728    }
729
730    #[test]
731    fn test_gift_card_code_is_write_only() {
732        // When creating a gift card, code can be provided
733        let create_gift_card = GiftCard {
734            initial_value: Some("100.00".to_string()),
735            code: Some("MYGIFTCODE1234".to_string()),
736            ..Default::default()
737        };
738
739        let json = serde_json::to_value(&create_gift_card).unwrap();
740        assert_eq!(json["code"], "MYGIFTCODE1234");
741        assert_eq!(json["initial_value"], "100.00");
742
743        // When deserializing from API (no code in response, only last_characters)
744        let api_response = r#"{
745            "id": 123,
746            "balance": "100.00",
747            "initial_value": "100.00",
748            "last_characters": "1234"
749        }"#;
750
751        let gift_card: GiftCard = serde_json::from_str(api_response).unwrap();
752        assert_eq!(gift_card.code, None);
753        assert_eq!(gift_card.last_characters.as_deref(), Some("1234"));
754    }
755}