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