Skip to main content

shopify_sdk/rest/resources/v2025_10/
refund.rs

1//! Refund resource implementation.
2//!
3//! This module provides the `RefundResource` resource, which represents a refund
4//! associated with an order in Shopify.
5//!
6//! # Note on Naming
7//!
8//! This module uses `RefundResource` as the struct name to avoid conflicts with
9//! the embedded `Refund` struct in the Order module. The embedded `Refund` struct
10//! is used when refunds appear within order responses, while `RefundResource` is
11//! the full resource for direct refund operations.
12//!
13//! # Nested Path Pattern
14//!
15//! Refunds are always accessed under an order:
16//! - List: `/orders/{order_id}/refunds`
17//! - Find: `/orders/{order_id}/refunds/{id}`
18//! - Create: `/orders/{order_id}/refunds`
19//! - Calculate: `/orders/{order_id}/refunds/calculate`
20//!
21//! Use `RefundResource::all_with_parent()` to list refunds under a specific order.
22//!
23//! # Special Operations
24//!
25//! - [`RefundResource::calculate`] - Calculate refund amounts without creating a refund
26//!
27//! # Example
28//!
29//! ```rust,ignore
30//! use shopify_sdk::rest::{RestResource, ResourceResponse};
31//! use shopify_sdk::rest::resources::v2025_10::{RefundResource, RefundListParams, RefundCalculateParams};
32//!
33//! // List refunds under a specific order
34//! let refunds = RefundResource::all_with_parent(&client, "order_id", 450789469, None).await?;
35//! for refund in refunds.iter() {
36//!     println!("Refund ID: {} - Note: {:?}", refund.id.unwrap_or(0), refund.note);
37//! }
38//!
39//! // Calculate a potential refund without creating it
40//! let calc_params = RefundCalculateParams {
41//!     shipping: Some(RefundShipping { full_refund: Some(true), ..Default::default() }),
42//!     refund_line_items: Some(vec![
43//!         RefundLineItemInput { line_item_id: 669751112, quantity: 1, restock_type: None },
44//!     ]),
45//!     ..Default::default()
46//! };
47//! let calculation = RefundResource::calculate(&client, 450789469, calc_params).await?;
48//! println!("Estimated refund: {:?}", calculation);
49//!
50//! // Create an actual refund
51//! let mut refund = RefundResource {
52//!     order_id: Some(450789469),
53//!     note: Some("Customer requested refund".to_string()),
54//!     notify: Some(true),
55//!     refund_line_items: Some(serde_json::json!([
56//!         {"line_item_id": 669751112, "quantity": 1, "restock_type": "return"}
57//!     ])),
58//!     ..Default::default()
59//! };
60//! let saved = refund.save(&client).await?;
61//! ```
62
63use chrono::{DateTime, Utc};
64use serde::{Deserialize, Serialize};
65
66use crate::clients::RestClient;
67use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
68use crate::HttpMethod;
69
70/// A refund line item in a refund.
71///
72/// Represents a line item that is being refunded.
73#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
74pub struct RefundLineItem {
75    /// The unique identifier of the refund line item.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub id: Option<u64>,
78
79    /// The quantity being refunded.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub quantity: Option<i32>,
82
83    /// The line item ID being refunded.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub line_item_id: Option<u64>,
86
87    /// The location ID for restocking.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub location_id: Option<u64>,
90
91    /// The restock type.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub restock_type: Option<String>,
94
95    /// The subtotal amount.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub subtotal: Option<String>,
98
99    /// The total tax amount.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub total_tax: Option<String>,
102
103    /// The subtotal set (multi-currency).
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub subtotal_set: Option<serde_json::Value>,
106
107    /// The total tax set (multi-currency).
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub total_tax_set: Option<serde_json::Value>,
110
111    /// The line item (embedded when returned from API).
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub line_item: Option<serde_json::Value>,
114}
115
116/// An order adjustment from a refund.
117///
118/// Represents adjustments made to the order totals due to a refund.
119#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
120pub struct OrderAdjustment {
121    /// The unique identifier of the adjustment.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub id: Option<u64>,
124
125    /// The order ID.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub order_id: Option<u64>,
128
129    /// The refund ID.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub refund_id: Option<u64>,
132
133    /// The kind of adjustment.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub kind: Option<String>,
136
137    /// The reason for the adjustment.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub reason: Option<String>,
140
141    /// The adjustment amount.
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub amount: Option<String>,
144
145    /// The tax amount.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub tax_amount: Option<String>,
148
149    /// The amount set (multi-currency).
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub amount_set: Option<serde_json::Value>,
152
153    /// The tax amount set (multi-currency).
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub tax_amount_set: Option<serde_json::Value>,
156}
157
158/// Shipping refund information.
159#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
160pub struct RefundShipping {
161    /// Whether to fully refund shipping.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub full_refund: Option<bool>,
164
165    /// The amount to refund for shipping.
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub amount: Option<String>,
168}
169
170/// Input for a refund line item when calculating or creating a refund.
171#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
172pub struct RefundLineItemInput {
173    /// The line item ID to refund.
174    pub line_item_id: u64,
175
176    /// The quantity to refund.
177    pub quantity: i32,
178
179    /// The restock type (e.g., "return", "cancel", "no_restock").
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub restock_type: Option<String>,
182}
183
184/// A refund shipping line.
185#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
186pub struct RefundShippingLine {
187    /// The ID of the shipping line being refunded.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub id: Option<u64>,
190
191    /// Whether this is a full refund.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub full_refund: Option<bool>,
194
195    /// The refund amount.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub amount: Option<String>,
198
199    /// The amount set (multi-currency).
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub amount_set: Option<serde_json::Value>,
202}
203
204/// A refund resource for an order.
205///
206/// This is the full resource for direct refund operations. It is named
207/// `RefundResource` to avoid conflicts with the embedded `Refund` struct
208/// in the Order module.
209///
210/// Refunds are nested under orders and can only be created, not updated
211/// or deleted after creation.
212///
213/// # Nested Resource
214///
215/// Refunds follow the nested path pattern under orders:
216/// - All operations require `order_id` context
217/// - Use `all_with_parent()` to list refunds under an order
218/// - The `order_id` field is required for creating new refunds
219///
220/// # Fields
221///
222/// ## Read-Only Fields
223/// - `id` - The unique identifier of the refund
224/// - `created_at` - When the refund was created
225/// - `processed_at` - When the refund was processed
226/// - `admin_graphql_api_id` - The GraphQL API ID
227///
228/// ## Writable Fields
229/// - `order_id` - The ID of the order this refund belongs to
230/// - `note` - An optional note explaining the refund
231/// - `notify` - Whether to notify the customer
232/// - `shipping` - Shipping refund information
233/// - `refund_line_items` - Line items to refund
234/// - `transactions` - Transactions for the refund
235#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
236pub struct RefundResource {
237    /// The unique identifier of the refund.
238    /// Read-only field.
239    #[serde(skip_serializing)]
240    pub id: Option<u64>,
241
242    /// The ID of the order this refund belongs to.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub order_id: Option<u64>,
245
246    /// An optional note explaining the refund.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub note: Option<String>,
249
250    /// The ID of the user who processed the refund.
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub user_id: Option<u64>,
253
254    /// Whether to restock the items.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub restock: Option<bool>,
257
258    /// Whether to notify the customer.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub notify: Option<bool>,
261
262    /// When the refund was processed.
263    /// Read-only field.
264    #[serde(skip_serializing)]
265    pub processed_at: Option<DateTime<Utc>>,
266
267    /// When the refund was created.
268    /// Read-only field.
269    #[serde(skip_serializing)]
270    pub created_at: Option<DateTime<Utc>>,
271
272    /// Duties associated with the refund.
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub duties: Option<serde_json::Value>,
275
276    /// Refunded duties.
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub refund_duties: Option<serde_json::Value>,
279
280    /// Line items included in the refund.
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub refund_line_items: Option<serde_json::Value>,
283
284    /// Refund shipping lines.
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub refund_shipping_lines: Option<Vec<RefundShippingLine>>,
287
288    /// Transactions for the refund.
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub transactions: Option<serde_json::Value>,
291
292    /// Order adjustments from the refund.
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub order_adjustments: Option<Vec<OrderAdjustment>>,
295
296    /// Shipping refund information (for create).
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub shipping: Option<RefundShipping>,
299
300    /// Currency for the refund (for create).
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub currency: Option<String>,
303
304    /// The admin GraphQL API ID.
305    /// Read-only field.
306    #[serde(skip_serializing)]
307    pub admin_graphql_api_id: Option<String>,
308}
309
310/// Parameters for calculating a refund.
311#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
312pub struct RefundCalculateParams {
313    /// Shipping refund information.
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub shipping: Option<RefundShipping>,
316
317    /// Line items to refund.
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub refund_line_items: Option<Vec<RefundLineItemInput>>,
320
321    /// Currency for the calculation.
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub currency: Option<String>,
324}
325
326impl RefundResource {
327    /// Calculates a refund without creating it.
328    ///
329    /// Sends a POST request to `/admin/api/{version}/orders/{order_id}/refunds/calculate.json`
330    /// to calculate refund amounts without actually creating the refund.
331    ///
332    /// # Arguments
333    ///
334    /// * `client` - The REST client to use for the request
335    /// * `order_id` - The ID of the order to calculate refund for
336    /// * `params` - Parameters for the calculation (shipping, line_items, currency)
337    ///
338    /// # Returns
339    ///
340    /// A `RefundResource` populated with calculated amounts.
341    ///
342    /// # Errors
343    ///
344    /// Returns [`ResourceError::NotFound`] if the order doesn't exist.
345    ///
346    /// # Example
347    ///
348    /// ```rust,ignore
349    /// let calc_params = RefundCalculateParams {
350    ///     shipping: Some(RefundShipping { full_refund: Some(true), ..Default::default() }),
351    ///     refund_line_items: Some(vec![
352    ///         RefundLineItemInput { line_item_id: 669751112, quantity: 1, restock_type: None },
353    ///     ]),
354    ///     ..Default::default()
355    /// };
356    /// let calculation = RefundResource::calculate(&client, 450789469, calc_params).await?;
357    /// println!("Calculated refund: {:?}", calculation);
358    /// ```
359    pub async fn calculate(
360        client: &RestClient,
361        order_id: u64,
362        params: RefundCalculateParams,
363    ) -> Result<RefundResource, ResourceError> {
364        let path = format!("orders/{order_id}/refunds/calculate");
365
366        // Wrap params in refund key
367        let body = serde_json::json!({
368            "refund": params
369        });
370
371        let response = client.post(&path, body, None).await?;
372
373        if !response.is_ok() {
374            return Err(ResourceError::from_http_response(
375                response.code,
376                &response.body,
377                Self::NAME,
378                Some(&order_id.to_string()),
379                response.request_id(),
380            ));
381        }
382
383        // Parse the response - Shopify returns the refund wrapped in "refund" key
384        let refund: RefundResource = response
385            .body
386            .get("refund")
387            .ok_or_else(|| {
388                ResourceError::Http(crate::clients::HttpError::Response(
389                    crate::clients::HttpResponseError {
390                        code: response.code,
391                        message: "Missing 'refund' in response".to_string(),
392                        error_reference: response.request_id().map(ToString::to_string),
393                    },
394                ))
395            })
396            .and_then(|v| {
397                serde_json::from_value(v.clone()).map_err(|e| {
398                    ResourceError::Http(crate::clients::HttpError::Response(
399                        crate::clients::HttpResponseError {
400                            code: response.code,
401                            message: format!("Failed to deserialize refund: {e}"),
402                            error_reference: response.request_id().map(ToString::to_string),
403                        },
404                    ))
405                })
406            })?;
407
408        Ok(refund)
409    }
410
411    /// Counts refunds under a specific order.
412    ///
413    /// Note: The Shopify API does not provide a count endpoint for refunds.
414    /// This method is provided for API consistency but will return an error.
415    ///
416    /// Use `all_with_parent()` and count the results instead.
417    pub async fn count_with_parent<ParentId: std::fmt::Display + Send>(
418        _client: &RestClient,
419        _parent_id_name: &str,
420        _parent_id: ParentId,
421        _params: Option<RefundCountParams>,
422    ) -> Result<u64, ResourceError> {
423        Err(ResourceError::PathResolutionFailed {
424            resource: Self::NAME,
425            operation: "count",
426        })
427    }
428}
429
430impl RestResource for RefundResource {
431    type Id = u64;
432    type FindParams = RefundFindParams;
433    type AllParams = RefundListParams;
434    type CountParams = RefundCountParams;
435
436    const NAME: &'static str = "Refund";
437    const PLURAL: &'static str = "refunds";
438
439    /// Paths for the RefundResource.
440    ///
441    /// Refunds are NESTED under orders. All operations require `order_id`.
442    /// Note: Refunds cannot be updated or deleted. No count endpoint available.
443    const PATHS: &'static [ResourcePath] = &[
444        // All paths require order_id
445        ResourcePath::new(
446            HttpMethod::Get,
447            ResourceOperation::Find,
448            &["order_id", "id"],
449            "orders/{order_id}/refunds/{id}",
450        ),
451        ResourcePath::new(
452            HttpMethod::Get,
453            ResourceOperation::All,
454            &["order_id"],
455            "orders/{order_id}/refunds",
456        ),
457        ResourcePath::new(
458            HttpMethod::Post,
459            ResourceOperation::Create,
460            &["order_id"],
461            "orders/{order_id}/refunds",
462        ),
463        // No Count, Update, or Delete paths
464    ];
465
466    fn get_id(&self) -> Option<Self::Id> {
467        self.id
468    }
469}
470
471/// Parameters for finding a single refund.
472#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
473pub struct RefundFindParams {
474    /// Comma-separated list of fields to include in the response.
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub fields: Option<String>,
477
478    /// Whether to return the amount in shop currency.
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub in_shop_currency: Option<bool>,
481}
482
483/// Parameters for listing refunds.
484#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
485pub struct RefundListParams {
486    /// Maximum number of results to return (default: 50, max: 250).
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub limit: Option<u32>,
489
490    /// Comma-separated list of fields to include in the response.
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub fields: Option<String>,
493
494    /// Whether to return the amount in shop currency.
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub in_shop_currency: Option<bool>,
497}
498
499/// Parameters for counting refunds.
500///
501/// Note: The Shopify API does not provide a count endpoint for refunds.
502#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
503pub struct RefundCountParams {
504    // No count endpoint available for refunds
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use crate::rest::{get_path, ResourceOperation};
511
512    #[test]
513    fn test_refund_nested_paths_require_order_id() {
514        // All paths should require order_id (nested under orders)
515
516        // Find requires both order_id and id
517        let find_path =
518            get_path(RefundResource::PATHS, ResourceOperation::Find, &["order_id", "id"]);
519        assert!(find_path.is_some());
520        assert_eq!(
521            find_path.unwrap().template,
522            "orders/{order_id}/refunds/{id}"
523        );
524
525        // Find with only id should fail (no standalone path)
526        let find_without_order = get_path(RefundResource::PATHS, ResourceOperation::Find, &["id"]);
527        assert!(find_without_order.is_none());
528
529        // All requires order_id
530        let all_path = get_path(RefundResource::PATHS, ResourceOperation::All, &["order_id"]);
531        assert!(all_path.is_some());
532        assert_eq!(all_path.unwrap().template, "orders/{order_id}/refunds");
533
534        // All without order_id should fail
535        let all_without_order = get_path(RefundResource::PATHS, ResourceOperation::All, &[]);
536        assert!(all_without_order.is_none());
537
538        // Create requires order_id
539        let create_path = get_path(RefundResource::PATHS, ResourceOperation::Create, &["order_id"]);
540        assert!(create_path.is_some());
541        assert_eq!(create_path.unwrap().template, "orders/{order_id}/refunds");
542
543        // No Count path
544        let count_path = get_path(RefundResource::PATHS, ResourceOperation::Count, &["order_id"]);
545        assert!(count_path.is_none());
546
547        // No Update path
548        let update_path = get_path(
549            RefundResource::PATHS,
550            ResourceOperation::Update,
551            &["order_id", "id"],
552        );
553        assert!(update_path.is_none());
554
555        // No Delete path
556        let delete_path = get_path(
557            RefundResource::PATHS,
558            ResourceOperation::Delete,
559            &["order_id", "id"],
560        );
561        assert!(delete_path.is_none());
562    }
563
564    #[test]
565    fn test_refund_calculate_path_construction() {
566        // Verify the calculate path format
567        let order_id = 450789469u64;
568        let expected_path = format!("orders/{order_id}/refunds/calculate");
569        assert_eq!(expected_path, "orders/450789469/refunds/calculate");
570    }
571
572    #[test]
573    fn test_refund_struct_serialization() {
574        let refund = RefundResource {
575            id: Some(123456),
576            order_id: Some(450789469),
577            note: Some("Customer requested refund".to_string()),
578            user_id: Some(799407056),
579            restock: Some(true),
580            notify: Some(true),
581            created_at: Some(
582                DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
583                    .unwrap()
584                    .with_timezone(&Utc),
585            ),
586            processed_at: Some(
587                DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
588                    .unwrap()
589                    .with_timezone(&Utc),
590            ),
591            admin_graphql_api_id: Some("gid://shopify/Refund/123456".to_string()),
592            ..Default::default()
593        };
594
595        let json = serde_json::to_string(&refund).unwrap();
596        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
597
598        // Writable fields should be present
599        assert_eq!(parsed["order_id"], 450789469);
600        assert_eq!(parsed["note"], "Customer requested refund");
601        assert_eq!(parsed["user_id"], 799407056);
602        assert_eq!(parsed["restock"], true);
603        assert_eq!(parsed["notify"], true);
604
605        // Read-only fields should be omitted
606        assert!(parsed.get("id").is_none());
607        assert!(parsed.get("created_at").is_none());
608        assert!(parsed.get("processed_at").is_none());
609        assert!(parsed.get("admin_graphql_api_id").is_none());
610    }
611
612    #[test]
613    fn test_refund_deserialization_with_complex_nested_structures() {
614        let json = r#"{
615            "id": 123456,
616            "order_id": 450789469,
617            "note": "Customer requested refund",
618            "user_id": 799407056,
619            "restock": true,
620            "processed_at": "2024-01-15T10:30:00Z",
621            "created_at": "2024-01-15T10:30:00Z",
622            "refund_line_items": [
623                {
624                    "id": 1,
625                    "quantity": 1,
626                    "line_item_id": 669751112,
627                    "location_id": 655441491,
628                    "restock_type": "return",
629                    "subtotal": "199.99",
630                    "total_tax": "15.00",
631                    "line_item": {
632                        "id": 669751112,
633                        "title": "IPod Nano - 8GB"
634                    }
635                }
636            ],
637            "transactions": [
638                {
639                    "id": 389404469,
640                    "order_id": 450789469,
641                    "kind": "refund",
642                    "amount": "214.99",
643                    "status": "success"
644                }
645            ],
646            "order_adjustments": [
647                {
648                    "id": 1,
649                    "order_id": 450789469,
650                    "refund_id": 123456,
651                    "kind": "refund_discrepancy",
652                    "reason": "Refund discrepancy",
653                    "amount": "-0.01"
654                }
655            ],
656            "refund_shipping_lines": [
657                {
658                    "id": 1,
659                    "full_refund": true,
660                    "amount": "5.00"
661                }
662            ],
663            "admin_graphql_api_id": "gid://shopify/Refund/123456"
664        }"#;
665
666        let refund: RefundResource = serde_json::from_str(json).unwrap();
667
668        assert_eq!(refund.id, Some(123456));
669        assert_eq!(refund.order_id, Some(450789469));
670        assert_eq!(refund.note, Some("Customer requested refund".to_string()));
671        assert_eq!(refund.user_id, Some(799407056));
672        assert_eq!(refund.restock, Some(true));
673        assert!(refund.processed_at.is_some());
674        assert!(refund.created_at.is_some());
675
676        // Check nested structures
677        assert!(refund.refund_line_items.is_some());
678        assert!(refund.transactions.is_some());
679
680        // Check order adjustments
681        assert!(refund.order_adjustments.is_some());
682        let adjustments = refund.order_adjustments.unwrap();
683        assert_eq!(adjustments.len(), 1);
684        assert_eq!(adjustments[0].kind, Some("refund_discrepancy".to_string()));
685
686        // Check refund shipping lines
687        assert!(refund.refund_shipping_lines.is_some());
688        let shipping_lines = refund.refund_shipping_lines.unwrap();
689        assert_eq!(shipping_lines.len(), 1);
690        assert_eq!(shipping_lines[0].full_refund, Some(true));
691        assert_eq!(shipping_lines[0].amount, Some("5.00".to_string()));
692    }
693
694    #[test]
695    fn test_refund_calculate_params_serialization() {
696        let params = RefundCalculateParams {
697            shipping: Some(RefundShipping {
698                full_refund: Some(true),
699                amount: None,
700            }),
701            refund_line_items: Some(vec![
702                RefundLineItemInput {
703                    line_item_id: 669751112,
704                    quantity: 1,
705                    restock_type: Some("return".to_string()),
706                },
707            ]),
708            currency: Some("USD".to_string()),
709        };
710
711        let json = serde_json::to_value(&params).unwrap();
712
713        assert!(json["shipping"]["full_refund"].as_bool().unwrap());
714        assert_eq!(json["refund_line_items"][0]["line_item_id"], 669751112);
715        assert_eq!(json["refund_line_items"][0]["quantity"], 1);
716        assert_eq!(json["refund_line_items"][0]["restock_type"], "return");
717        assert_eq!(json["currency"], "USD");
718    }
719
720    #[test]
721    fn test_refund_list_params_serialization() {
722        let params = RefundListParams {
723            limit: Some(50),
724            fields: Some("id,note,created_at".to_string()),
725            in_shop_currency: Some(true),
726        };
727
728        let json = serde_json::to_value(&params).unwrap();
729
730        assert_eq!(json["limit"], 50);
731        assert_eq!(json["fields"], "id,note,created_at");
732        assert_eq!(json["in_shop_currency"], true);
733
734        // Test empty params
735        let empty_params = RefundListParams::default();
736        let empty_json = serde_json::to_value(&empty_params).unwrap();
737        assert_eq!(empty_json, serde_json::json!({}));
738    }
739
740    #[test]
741    fn test_refund_get_id_returns_correct_value() {
742        // Refund with ID
743        let refund_with_id = RefundResource {
744            id: Some(123456),
745            order_id: Some(450789469),
746            note: Some("Test refund".to_string()),
747            ..Default::default()
748        };
749        assert_eq!(refund_with_id.get_id(), Some(123456));
750
751        // Refund without ID (new refund)
752        let refund_without_id = RefundResource {
753            id: None,
754            order_id: Some(450789469),
755            note: Some("New refund".to_string()),
756            ..Default::default()
757        };
758        assert_eq!(refund_without_id.get_id(), None);
759    }
760
761    #[test]
762    fn test_refund_constants() {
763        assert_eq!(RefundResource::NAME, "Refund");
764        assert_eq!(RefundResource::PLURAL, "refunds");
765    }
766
767    #[test]
768    fn test_refund_line_item_serialization() {
769        let line_item = RefundLineItem {
770            id: Some(1),
771            quantity: Some(1),
772            line_item_id: Some(669751112),
773            location_id: Some(655441491),
774            restock_type: Some("return".to_string()),
775            subtotal: Some("199.99".to_string()),
776            total_tax: Some("15.00".to_string()),
777            ..Default::default()
778        };
779
780        let json = serde_json::to_value(&line_item).unwrap();
781
782        assert_eq!(json["id"], 1);
783        assert_eq!(json["quantity"], 1);
784        assert_eq!(json["line_item_id"], 669751112);
785        assert_eq!(json["location_id"], 655441491);
786        assert_eq!(json["restock_type"], "return");
787        assert_eq!(json["subtotal"], "199.99");
788        assert_eq!(json["total_tax"], "15.00");
789    }
790
791    #[test]
792    fn test_order_adjustment_serialization() {
793        let adjustment = OrderAdjustment {
794            id: Some(1),
795            order_id: Some(450789469),
796            refund_id: Some(123456),
797            kind: Some("refund_discrepancy".to_string()),
798            reason: Some("Refund discrepancy".to_string()),
799            amount: Some("-0.01".to_string()),
800            tax_amount: Some("0.00".to_string()),
801            ..Default::default()
802        };
803
804        let json = serde_json::to_value(&adjustment).unwrap();
805
806        assert_eq!(json["id"], 1);
807        assert_eq!(json["order_id"], 450789469);
808        assert_eq!(json["refund_id"], 123456);
809        assert_eq!(json["kind"], "refund_discrepancy");
810        assert_eq!(json["reason"], "Refund discrepancy");
811        assert_eq!(json["amount"], "-0.01");
812    }
813}