Skip to main content

shopify_sdk/rest/resources/v2025_10/
discount_code.rs

1//! DiscountCode resource implementation.
2//!
3//! This module provides the [`DiscountCode`] resource for managing discount codes
4//! associated with price rules. Discount codes are the customer-facing codes that
5//! customers enter at checkout to apply price rule discounts.
6//!
7//! # Nested Resource
8//!
9//! DiscountCodes are nested under PriceRules:
10//! - `GET /price_rules/{price_rule_id}/discount_codes.json`
11//! - `POST /price_rules/{price_rule_id}/discount_codes.json`
12//! - `GET /price_rules/{price_rule_id}/discount_codes/{id}.json`
13//! - `PUT /price_rules/{price_rule_id}/discount_codes/{id}.json`
14//! - `DELETE /price_rules/{price_rule_id}/discount_codes/{id}.json`
15//!
16//! # Special Operations
17//!
18//! - **Lookup by code**: `DiscountCode::lookup(&client, "CODE")` finds a discount
19//!   code by its code string using a standalone path.
20//! - **Batch create**: `DiscountCode::batch(&client, price_rule_id, codes)` creates
21//!   multiple discount codes at once under a price rule.
22//!
23//! # Example
24//!
25//! ```rust,ignore
26//! use shopify_sdk::rest::{RestResource, ResourceResponse};
27//! use shopify_sdk::rest::resources::v2025_10::{DiscountCode, DiscountCodeListParams};
28//!
29//! // Create a discount code under a price rule
30//! let code = DiscountCode {
31//!     price_rule_id: Some(507328175),
32//!     code: Some("SUMMER20".to_string()),
33//!     ..Default::default()
34//! };
35//! let saved = code.save(&client).await?;
36//!
37//! // Lookup a discount code by its code string
38//! let found = DiscountCode::lookup(&client, "SUMMER20").await?;
39//!
40//! // List all discount codes for a price rule
41//! let codes = DiscountCode::all_with_parent(
42//!     &client,
43//!     "price_rule_id",
44//!     507328175,
45//!     None
46//! ).await?;
47//! ```
48
49use std::collections::HashMap;
50
51use chrono::{DateTime, Utc};
52use serde::{Deserialize, Serialize};
53
54use crate::clients::RestClient;
55use crate::rest::{
56    build_path, get_path, ResourceError, ResourceOperation, ResourcePath, ResourceResponse,
57    RestResource,
58};
59use crate::HttpMethod;
60
61/// A discount code associated with a price rule.
62///
63/// Discount codes are the customer-facing codes that shoppers enter at checkout.
64/// Each discount code belongs to a price rule that defines the discount logic.
65///
66/// # Nested Resource
67///
68/// This is a nested resource under `PriceRule`. Most operations require the
69/// parent `price_rule_id`.
70///
71/// Use `DiscountCode::all_with_parent()` to list codes under a specific price rule.
72///
73/// # Fields
74///
75/// ## Read-Only Fields
76/// - `id` - The unique identifier
77/// - `usage_count` - Number of times the code has been used
78/// - `errors` - Any errors associated with the code
79/// - `created_at` - When the code was created
80/// - `updated_at` - When the code was last updated
81///
82/// ## Writable Fields
83/// - `price_rule_id` - The parent price rule ID (required)
84/// - `code` - The discount code string customers enter
85#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
86pub struct DiscountCode {
87    /// The unique identifier of the discount code.
88    /// Read-only field.
89    #[serde(skip_serializing)]
90    pub id: Option<u64>,
91
92    /// The ID of the parent price rule.
93    /// Required for creating new discount codes.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub price_rule_id: Option<u64>,
96
97    /// The discount code that customers enter at checkout.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub code: Option<String>,
100
101    /// The number of times this discount code has been used.
102    /// Read-only field.
103    #[serde(skip_serializing)]
104    pub usage_count: Option<i32>,
105
106    /// Any errors associated with this discount code.
107    /// Read-only field populated after batch creation.
108    #[serde(skip_serializing)]
109    pub errors: Option<Vec<DiscountCodeError>>,
110
111    /// When the discount code was created.
112    /// Read-only field.
113    #[serde(skip_serializing)]
114    pub created_at: Option<DateTime<Utc>>,
115
116    /// When the discount code was last updated.
117    /// Read-only field.
118    #[serde(skip_serializing)]
119    pub updated_at: Option<DateTime<Utc>>,
120}
121
122/// An error associated with a discount code.
123#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
124pub struct DiscountCodeError {
125    /// The error code.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub code: Option<String>,
128
129    /// The error message.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub message: Option<String>,
132}
133
134/// The result of a batch discount code creation.
135#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
136pub struct DiscountCodeBatchResult {
137    /// The ID of the batch job.
138    pub id: Option<u64>,
139
140    /// The price rule ID the batch is associated with.
141    pub price_rule_id: Option<u64>,
142
143    /// When the batch job started.
144    pub started_at: Option<DateTime<Utc>>,
145
146    /// When the batch job completed.
147    pub completed_at: Option<DateTime<Utc>>,
148
149    /// When the batch job was created.
150    pub created_at: Option<DateTime<Utc>>,
151
152    /// When the batch job was last updated.
153    pub updated_at: Option<DateTime<Utc>>,
154
155    /// The status of the batch job: "queued", "running", "completed".
156    pub status: Option<String>,
157
158    /// The number of codes processed.
159    pub codes_count: Option<i32>,
160
161    /// The number of codes imported successfully.
162    pub imported_count: Option<i32>,
163
164    /// The number of codes that failed to import.
165    pub failed_count: Option<i32>,
166
167    /// The log entries for the batch job.
168    pub logs: Option<Vec<String>>,
169}
170
171impl DiscountCode {
172    /// Counts discount codes under a specific price rule.
173    ///
174    /// # Arguments
175    ///
176    /// * `client` - The REST client to use for the request
177    /// * `price_rule_id` - The parent price rule ID
178    /// * `params` - Optional parameters for filtering
179    ///
180    /// # Returns
181    ///
182    /// The count of matching discount codes as a `u64`.
183    ///
184    /// # Errors
185    ///
186    /// Returns [`ResourceError::PathResolutionFailed`] if no count path exists.
187    ///
188    /// # Example
189    ///
190    /// ```rust,ignore
191    /// let count = DiscountCode::count_with_parent(&client, 507328175, None).await?;
192    /// println!("Total discount codes: {}", count);
193    /// ```
194    pub async fn count_with_parent(
195        client: &RestClient,
196        price_rule_id: u64,
197        params: Option<DiscountCodeCountParams>,
198    ) -> Result<u64, ResourceError> {
199        let mut ids: HashMap<&str, String> = HashMap::new();
200        ids.insert("price_rule_id", price_rule_id.to_string());
201
202        let available_ids: Vec<&str> = ids.keys().copied().collect();
203        let path = get_path(Self::PATHS, ResourceOperation::Count, &available_ids).ok_or(
204            ResourceError::PathResolutionFailed {
205                resource: Self::NAME,
206                operation: "count",
207            },
208        )?;
209
210        let url = build_path(path.template, &ids);
211
212        // Build query params
213        let query = params
214            .map(|p| {
215                let value = serde_json::to_value(&p).map_err(|e| {
216                    ResourceError::Http(crate::clients::HttpError::Response(
217                        crate::clients::HttpResponseError {
218                            code: 400,
219                            message: format!("Failed to serialize params: {e}"),
220                            error_reference: None,
221                        },
222                    ))
223                })?;
224
225                let mut query = HashMap::new();
226                if let serde_json::Value::Object(map) = value {
227                    for (key, val) in map {
228                        match val {
229                            serde_json::Value::String(s) => {
230                                query.insert(key, s);
231                            }
232                            serde_json::Value::Number(n) => {
233                                query.insert(key, n.to_string());
234                            }
235                            serde_json::Value::Bool(b) => {
236                                query.insert(key, b.to_string());
237                            }
238                            _ => {}
239                        }
240                    }
241                }
242                Ok::<_, ResourceError>(query)
243            })
244            .transpose()?
245            .filter(|q| !q.is_empty());
246
247        let response = client.get(&url, query).await?;
248
249        if !response.is_ok() {
250            return Err(ResourceError::from_http_response(
251                response.code,
252                &response.body,
253                Self::NAME,
254                None,
255                response.request_id(),
256            ));
257        }
258
259        let count = response
260            .body
261            .get("count")
262            .and_then(serde_json::Value::as_u64)
263            .ok_or_else(|| {
264                ResourceError::Http(crate::clients::HttpError::Response(
265                    crate::clients::HttpResponseError {
266                        code: response.code,
267                        message: "Missing 'count' in response".to_string(),
268                        error_reference: response.request_id().map(ToString::to_string),
269                    },
270                ))
271            })?;
272
273        Ok(count)
274    }
275
276    /// Finds a single discount code by ID under a price rule.
277    ///
278    /// # Arguments
279    ///
280    /// * `client` - The REST client to use for the request
281    /// * `price_rule_id` - The parent price rule ID
282    /// * `id` - The discount code ID to find
283    /// * `params` - Optional parameters for the request
284    ///
285    /// # Errors
286    ///
287    /// Returns [`ResourceError::NotFound`] if the discount code doesn't exist.
288    ///
289    /// # Example
290    ///
291    /// ```rust,ignore
292    /// let code = DiscountCode::find_with_parent(&client, 507328175, 123, None).await?;
293    /// ```
294    pub async fn find_with_parent(
295        client: &RestClient,
296        price_rule_id: u64,
297        id: u64,
298        _params: Option<DiscountCodeFindParams>,
299    ) -> Result<ResourceResponse<Self>, ResourceError> {
300        let mut ids: HashMap<&str, String> = HashMap::new();
301        ids.insert("price_rule_id", price_rule_id.to_string());
302        ids.insert("id", id.to_string());
303
304        let available_ids: Vec<&str> = ids.keys().copied().collect();
305        let path = get_path(Self::PATHS, ResourceOperation::Find, &available_ids).ok_or(
306            ResourceError::PathResolutionFailed {
307                resource: Self::NAME,
308                operation: "find",
309            },
310        )?;
311
312        let url = build_path(path.template, &ids);
313        let response = client.get(&url, None).await?;
314
315        if !response.is_ok() {
316            return Err(ResourceError::from_http_response(
317                response.code,
318                &response.body,
319                Self::NAME,
320                Some(&id.to_string()),
321                response.request_id(),
322            ));
323        }
324
325        let key = Self::resource_key();
326        ResourceResponse::from_http_response(response, &key)
327    }
328
329    /// Looks up a discount code by its code string.
330    ///
331    /// This uses a standalone path that doesn't require knowing the price rule ID.
332    /// Useful when you only know the discount code string.
333    ///
334    /// # Arguments
335    ///
336    /// * `client` - The REST client to use for the request
337    /// * `code` - The discount code string to look up
338    ///
339    /// # Returns
340    ///
341    /// The discount code if found.
342    ///
343    /// # Errors
344    ///
345    /// Returns [`ResourceError::NotFound`] if no discount code with that code exists.
346    ///
347    /// # Example
348    ///
349    /// ```rust,ignore
350    /// let code = DiscountCode::lookup(&client, "SUMMER20").await?;
351    /// println!("Found discount code: {:?}", code.price_rule_id);
352    /// ```
353    pub async fn lookup(
354        client: &RestClient,
355        code: &str,
356    ) -> Result<ResourceResponse<Self>, ResourceError> {
357        let url = "discount_codes/lookup";
358        let mut query = HashMap::new();
359        query.insert("code".to_string(), code.to_string());
360
361        let response = client.get(url, Some(query)).await?;
362
363        if !response.is_ok() {
364            return Err(ResourceError::from_http_response(
365                response.code,
366                &response.body,
367                Self::NAME,
368                Some(code),
369                response.request_id(),
370            ));
371        }
372
373        let key = Self::resource_key();
374        ResourceResponse::from_http_response(response, &key)
375    }
376
377    /// Creates multiple discount codes in a batch.
378    ///
379    /// This starts an asynchronous job to create multiple discount codes
380    /// under a price rule. Use this for bulk creation of codes.
381    ///
382    /// # Arguments
383    ///
384    /// * `client` - The REST client to use for the request
385    /// * `price_rule_id` - The price rule ID to create codes under
386    /// * `codes` - A list of code strings to create
387    ///
388    /// # Returns
389    ///
390    /// A batch result containing the job status.
391    ///
392    /// # Example
393    ///
394    /// ```rust,ignore
395    /// let result = DiscountCode::batch(
396    ///     &client,
397    ///     507328175,
398    ///     vec!["CODE1".to_string(), "CODE2".to_string(), "CODE3".to_string()]
399    /// ).await?;
400    /// println!("Batch job status: {:?}", result.status);
401    /// ```
402    pub async fn batch(
403        client: &RestClient,
404        price_rule_id: u64,
405        codes: Vec<String>,
406    ) -> Result<DiscountCodeBatchResult, ResourceError> {
407        let url = format!("price_rules/{price_rule_id}/batch");
408
409        // Build the request body
410        let discount_codes: Vec<serde_json::Value> = codes
411            .into_iter()
412            .map(|code| serde_json::json!({ "code": code }))
413            .collect();
414
415        let body = serde_json::json!({
416            "discount_codes": discount_codes
417        });
418
419        let response = client.post(&url, body, None).await?;
420
421        if !response.is_ok() {
422            return Err(ResourceError::from_http_response(
423                response.code,
424                &response.body,
425                Self::NAME,
426                None,
427                response.request_id(),
428            ));
429        }
430
431        // Parse the batch result from the response
432        let result = response
433            .body
434            .get("discount_code_creation")
435            .ok_or_else(|| {
436                ResourceError::Http(crate::clients::HttpError::Response(
437                    crate::clients::HttpResponseError {
438                        code: response.code,
439                        message: "Missing 'discount_code_creation' in response".to_string(),
440                        error_reference: response.request_id().map(ToString::to_string),
441                    },
442                ))
443            })?;
444
445        let batch_result: DiscountCodeBatchResult =
446            serde_json::from_value(result.clone()).map_err(|e| {
447                ResourceError::Http(crate::clients::HttpError::Response(
448                    crate::clients::HttpResponseError {
449                        code: response.code,
450                        message: format!("Failed to parse batch result: {e}"),
451                        error_reference: response.request_id().map(ToString::to_string),
452                    },
453                ))
454            })?;
455
456        Ok(batch_result)
457    }
458
459    /// Gets the status of a batch discount code creation job.
460    ///
461    /// # Arguments
462    ///
463    /// * `client` - The REST client to use for the request
464    /// * `price_rule_id` - The price rule ID
465    /// * `batch_id` - The batch job ID from `batch()`
466    ///
467    /// # Returns
468    ///
469    /// The current batch job status.
470    ///
471    /// # Example
472    ///
473    /// ```rust,ignore
474    /// let status = DiscountCode::batch_status(&client, 507328175, 123).await?;
475    /// println!("Status: {:?}, Imported: {:?}", status.status, status.imported_count);
476    /// ```
477    pub async fn batch_status(
478        client: &RestClient,
479        price_rule_id: u64,
480        batch_id: u64,
481    ) -> Result<DiscountCodeBatchResult, ResourceError> {
482        let url = format!("price_rules/{price_rule_id}/batch/{batch_id}");
483
484        let response = client.get(&url, None).await?;
485
486        if !response.is_ok() {
487            return Err(ResourceError::from_http_response(
488                response.code,
489                &response.body,
490                Self::NAME,
491                Some(&batch_id.to_string()),
492                response.request_id(),
493            ));
494        }
495
496        let result = response
497            .body
498            .get("discount_code_creation")
499            .ok_or_else(|| {
500                ResourceError::Http(crate::clients::HttpError::Response(
501                    crate::clients::HttpResponseError {
502                        code: response.code,
503                        message: "Missing 'discount_code_creation' in response".to_string(),
504                        error_reference: response.request_id().map(ToString::to_string),
505                    },
506                ))
507            })?;
508
509        let batch_result: DiscountCodeBatchResult =
510            serde_json::from_value(result.clone()).map_err(|e| {
511                ResourceError::Http(crate::clients::HttpError::Response(
512                    crate::clients::HttpResponseError {
513                        code: response.code,
514                        message: format!("Failed to parse batch result: {e}"),
515                        error_reference: response.request_id().map(ToString::to_string),
516                    },
517                ))
518            })?;
519
520        Ok(batch_result)
521    }
522
523    /// Gets the discount codes from a completed batch job.
524    ///
525    /// # Arguments
526    ///
527    /// * `client` - The REST client to use for the request
528    /// * `price_rule_id` - The price rule ID
529    /// * `batch_id` - The batch job ID from `batch()`
530    ///
531    /// # Returns
532    ///
533    /// A list of discount codes created by the batch job.
534    ///
535    /// # Example
536    ///
537    /// ```rust,ignore
538    /// let codes = DiscountCode::batch_codes(&client, 507328175, 123).await?;
539    /// for code in codes {
540    ///     println!("Created code: {:?}", code.code);
541    /// }
542    /// ```
543    pub async fn batch_codes(
544        client: &RestClient,
545        price_rule_id: u64,
546        batch_id: u64,
547    ) -> Result<Vec<Self>, ResourceError> {
548        let url = format!("price_rules/{price_rule_id}/batch/{batch_id}/discount_codes");
549
550        let response = client.get(&url, None).await?;
551
552        if !response.is_ok() {
553            return Err(ResourceError::from_http_response(
554                response.code,
555                &response.body,
556                Self::NAME,
557                Some(&batch_id.to_string()),
558                response.request_id(),
559            ));
560        }
561
562        let codes_value = response
563            .body
564            .get(Self::PLURAL)
565            .ok_or_else(|| {
566                ResourceError::Http(crate::clients::HttpError::Response(
567                    crate::clients::HttpResponseError {
568                        code: response.code,
569                        message: format!("Missing '{}' in response", Self::PLURAL),
570                        error_reference: response.request_id().map(ToString::to_string),
571                    },
572                ))
573            })?;
574
575        let codes: Vec<Self> = serde_json::from_value(codes_value.clone()).map_err(|e| {
576            ResourceError::Http(crate::clients::HttpError::Response(
577                crate::clients::HttpResponseError {
578                    code: response.code,
579                    message: format!("Failed to parse discount codes: {e}"),
580                    error_reference: response.request_id().map(ToString::to_string),
581                },
582            ))
583        })?;
584
585        Ok(codes)
586    }
587}
588
589impl RestResource for DiscountCode {
590    type Id = u64;
591    type FindParams = DiscountCodeFindParams;
592    type AllParams = DiscountCodeListParams;
593    type CountParams = DiscountCodeCountParams;
594
595    const NAME: &'static str = "DiscountCode";
596    const PLURAL: &'static str = "discount_codes";
597
598    /// Paths for the DiscountCode resource.
599    ///
600    /// All paths except lookup require `price_rule_id` as DiscountCodes
601    /// are nested under PriceRules.
602    const PATHS: &'static [ResourcePath] = &[
603        ResourcePath::new(
604            HttpMethod::Get,
605            ResourceOperation::Find,
606            &["price_rule_id", "id"],
607            "price_rules/{price_rule_id}/discount_codes/{id}",
608        ),
609        ResourcePath::new(
610            HttpMethod::Get,
611            ResourceOperation::All,
612            &["price_rule_id"],
613            "price_rules/{price_rule_id}/discount_codes",
614        ),
615        ResourcePath::new(
616            HttpMethod::Get,
617            ResourceOperation::Count,
618            &["price_rule_id"],
619            "price_rules/{price_rule_id}/discount_codes/count",
620        ),
621        ResourcePath::new(
622            HttpMethod::Post,
623            ResourceOperation::Create,
624            &["price_rule_id"],
625            "price_rules/{price_rule_id}/discount_codes",
626        ),
627        ResourcePath::new(
628            HttpMethod::Put,
629            ResourceOperation::Update,
630            &["price_rule_id", "id"],
631            "price_rules/{price_rule_id}/discount_codes/{id}",
632        ),
633        ResourcePath::new(
634            HttpMethod::Delete,
635            ResourceOperation::Delete,
636            &["price_rule_id", "id"],
637            "price_rules/{price_rule_id}/discount_codes/{id}",
638        ),
639    ];
640
641    fn get_id(&self) -> Option<Self::Id> {
642        self.id
643    }
644}
645
646/// Parameters for finding a single discount code.
647#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
648pub struct DiscountCodeFindParams {
649    /// Comma-separated list of fields to include in the response.
650    #[serde(skip_serializing_if = "Option::is_none")]
651    pub fields: Option<String>,
652}
653
654/// Parameters for listing discount codes.
655#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
656pub struct DiscountCodeListParams {
657    /// Maximum number of results to return (default: 50, max: 250).
658    #[serde(skip_serializing_if = "Option::is_none")]
659    pub limit: Option<u32>,
660
661    /// Return codes after this ID.
662    #[serde(skip_serializing_if = "Option::is_none")]
663    pub since_id: Option<u64>,
664
665    /// Cursor for pagination.
666    #[serde(skip_serializing_if = "Option::is_none")]
667    pub page_info: Option<String>,
668
669    /// Comma-separated list of fields to include in the response.
670    #[serde(skip_serializing_if = "Option::is_none")]
671    pub fields: Option<String>,
672}
673
674/// Parameters for counting discount codes.
675#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
676pub struct DiscountCodeCountParams {
677    /// Filter by times used.
678    #[serde(skip_serializing_if = "Option::is_none")]
679    pub times_used: Option<i32>,
680
681    /// Filter by minimum times used.
682    #[serde(skip_serializing_if = "Option::is_none")]
683    pub times_used_min: Option<i32>,
684
685    /// Filter by maximum times used.
686    #[serde(skip_serializing_if = "Option::is_none")]
687    pub times_used_max: Option<i32>,
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693    use crate::rest::{get_path, ResourceOperation};
694
695    #[test]
696    fn test_discount_code_serialization() {
697        let code = DiscountCode {
698            id: Some(12345),
699            price_rule_id: Some(507328175),
700            code: Some("SUMMER20".to_string()),
701            usage_count: Some(42),
702            errors: None,
703            created_at: Some(
704                DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
705                    .unwrap()
706                    .with_timezone(&Utc),
707            ),
708            updated_at: Some(
709                DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
710                    .unwrap()
711                    .with_timezone(&Utc),
712            ),
713        };
714
715        let json = serde_json::to_string(&code).unwrap();
716        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
717
718        // Writable fields should be present
719        assert_eq!(parsed["price_rule_id"], 507328175);
720        assert_eq!(parsed["code"], "SUMMER20");
721
722        // Read-only fields should be omitted
723        assert!(parsed.get("id").is_none());
724        assert!(parsed.get("usage_count").is_none());
725        assert!(parsed.get("errors").is_none());
726        assert!(parsed.get("created_at").is_none());
727        assert!(parsed.get("updated_at").is_none());
728    }
729
730    #[test]
731    fn test_discount_code_deserialization() {
732        let json = r#"{
733            "id": 1054381139,
734            "price_rule_id": 507328175,
735            "code": "SUMMERSALE20OFF",
736            "usage_count": 25,
737            "errors": [],
738            "created_at": "2024-01-15T10:30:00Z",
739            "updated_at": "2024-06-20T15:45:00Z"
740        }"#;
741
742        let code: DiscountCode = serde_json::from_str(json).unwrap();
743
744        assert_eq!(code.id, Some(1054381139));
745        assert_eq!(code.price_rule_id, Some(507328175));
746        assert_eq!(code.code, Some("SUMMERSALE20OFF".to_string()));
747        assert_eq!(code.usage_count, Some(25));
748        assert!(code.errors.is_some());
749        assert!(code.errors.unwrap().is_empty());
750        assert!(code.created_at.is_some());
751        assert!(code.updated_at.is_some());
752    }
753
754    #[test]
755    fn test_discount_code_nested_paths() {
756        // All paths should require price_rule_id
757
758        // Find requires both price_rule_id and id
759        let find_path = get_path(
760            DiscountCode::PATHS,
761            ResourceOperation::Find,
762            &["price_rule_id", "id"],
763        );
764        assert!(find_path.is_some());
765        assert_eq!(
766            find_path.unwrap().template,
767            "price_rules/{price_rule_id}/discount_codes/{id}"
768        );
769
770        // Find with only id should fail (no standalone path)
771        let find_without_parent = get_path(DiscountCode::PATHS, ResourceOperation::Find, &["id"]);
772        assert!(find_without_parent.is_none());
773
774        // All requires price_rule_id
775        let all_path = get_path(
776            DiscountCode::PATHS,
777            ResourceOperation::All,
778            &["price_rule_id"],
779        );
780        assert!(all_path.is_some());
781        assert_eq!(
782            all_path.unwrap().template,
783            "price_rules/{price_rule_id}/discount_codes"
784        );
785
786        // All without parent should fail
787        let all_without_parent = get_path(DiscountCode::PATHS, ResourceOperation::All, &[]);
788        assert!(all_without_parent.is_none());
789
790        // Count requires price_rule_id
791        let count_path = get_path(
792            DiscountCode::PATHS,
793            ResourceOperation::Count,
794            &["price_rule_id"],
795        );
796        assert!(count_path.is_some());
797        assert_eq!(
798            count_path.unwrap().template,
799            "price_rules/{price_rule_id}/discount_codes/count"
800        );
801
802        // Create requires price_rule_id
803        let create_path = get_path(
804            DiscountCode::PATHS,
805            ResourceOperation::Create,
806            &["price_rule_id"],
807        );
808        assert!(create_path.is_some());
809        assert_eq!(
810            create_path.unwrap().template,
811            "price_rules/{price_rule_id}/discount_codes"
812        );
813        assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
814
815        // Update requires both price_rule_id and id
816        let update_path = get_path(
817            DiscountCode::PATHS,
818            ResourceOperation::Update,
819            &["price_rule_id", "id"],
820        );
821        assert!(update_path.is_some());
822        assert_eq!(
823            update_path.unwrap().template,
824            "price_rules/{price_rule_id}/discount_codes/{id}"
825        );
826        assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
827
828        // Delete requires both price_rule_id and id
829        let delete_path = get_path(
830            DiscountCode::PATHS,
831            ResourceOperation::Delete,
832            &["price_rule_id", "id"],
833        );
834        assert!(delete_path.is_some());
835        assert_eq!(
836            delete_path.unwrap().template,
837            "price_rules/{price_rule_id}/discount_codes/{id}"
838        );
839        assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
840    }
841
842    #[test]
843    fn test_discount_code_lookup_is_standalone_path() {
844        // The lookup method uses a standalone path "discount_codes/lookup"
845        // which doesn't require price_rule_id
846        // This is tested by the method signature - it only takes client and code
847        // No path in PATHS array for this - it's a special method
848    }
849
850    #[test]
851    fn test_discount_code_batch_result_deserialization() {
852        let json = r#"{
853            "id": 173232803,
854            "price_rule_id": 507328175,
855            "started_at": "2024-06-15T10:00:00Z",
856            "completed_at": "2024-06-15T10:05:00Z",
857            "created_at": "2024-06-15T09:55:00Z",
858            "updated_at": "2024-06-15T10:05:00Z",
859            "status": "completed",
860            "codes_count": 3,
861            "imported_count": 3,
862            "failed_count": 0,
863            "logs": []
864        }"#;
865
866        let result: DiscountCodeBatchResult = serde_json::from_str(json).unwrap();
867
868        assert_eq!(result.id, Some(173232803));
869        assert_eq!(result.price_rule_id, Some(507328175));
870        assert_eq!(result.status, Some("completed".to_string()));
871        assert_eq!(result.codes_count, Some(3));
872        assert_eq!(result.imported_count, Some(3));
873        assert_eq!(result.failed_count, Some(0));
874        assert!(result.started_at.is_some());
875        assert!(result.completed_at.is_some());
876    }
877
878    #[test]
879    fn test_discount_code_count_params() {
880        let params = DiscountCodeCountParams {
881            times_used: Some(5),
882            times_used_min: Some(1),
883            times_used_max: Some(100),
884        };
885
886        let json = serde_json::to_value(&params).unwrap();
887
888        assert_eq!(json["times_used"], 5);
889        assert_eq!(json["times_used_min"], 1);
890        assert_eq!(json["times_used_max"], 100);
891
892        // Empty params should serialize to empty object
893        let empty_params = DiscountCodeCountParams::default();
894        let empty_json = serde_json::to_value(&empty_params).unwrap();
895        assert_eq!(empty_json, serde_json::json!({}));
896    }
897
898    #[test]
899    fn test_discount_code_constants() {
900        assert_eq!(DiscountCode::NAME, "DiscountCode");
901        assert_eq!(DiscountCode::PLURAL, "discount_codes");
902    }
903
904    #[test]
905    fn test_discount_code_get_id() {
906        let code_with_id = DiscountCode {
907            id: Some(12345),
908            code: Some("TEST".to_string()),
909            ..Default::default()
910        };
911        assert_eq!(code_with_id.get_id(), Some(12345));
912
913        let code_without_id = DiscountCode::default();
914        assert_eq!(code_without_id.get_id(), None);
915    }
916}