use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::clients::RestClient;
use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
use crate::HttpMethod;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct RefundLineItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quantity: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line_item_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub restock_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subtotal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_tax: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subtotal_set: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_tax_set: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line_item: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct OrderAdjustment {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refund_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tax_amount: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount_set: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tax_amount_set: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct RefundShipping {
#[serde(skip_serializing_if = "Option::is_none")]
pub full_refund: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct RefundLineItemInput {
pub line_item_id: u64,
pub quantity: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub restock_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct RefundShippingLine {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub full_refund: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount_set: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct RefundResource {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub restock: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notify: Option<bool>,
#[serde(skip_serializing)]
pub processed_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub created_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duties: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refund_duties: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refund_line_items: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refund_shipping_lines: Option<Vec<RefundShippingLine>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transactions: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_adjustments: Option<Vec<OrderAdjustment>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shipping: Option<RefundShipping>,
#[serde(skip_serializing_if = "Option::is_none")]
pub currency: Option<String>,
#[serde(skip_serializing)]
pub admin_graphql_api_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct RefundCalculateParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub shipping: Option<RefundShipping>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refund_line_items: Option<Vec<RefundLineItemInput>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub currency: Option<String>,
}
impl RefundResource {
pub async fn calculate(
client: &RestClient,
order_id: u64,
params: RefundCalculateParams,
) -> Result<RefundResource, ResourceError> {
let path = format!("orders/{order_id}/refunds/calculate");
let body = serde_json::json!({
"refund": params
});
let response = client.post(&path, body, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
Some(&order_id.to_string()),
response.request_id(),
));
}
let refund: RefundResource = response
.body
.get("refund")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'refund' in response".to_string(),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
.and_then(|v| {
serde_json::from_value(v.clone()).map_err(|e| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: format!("Failed to deserialize refund: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(refund)
}
pub async fn count_with_parent<ParentId: std::fmt::Display + Send>(
_client: &RestClient,
_parent_id_name: &str,
_parent_id: ParentId,
_params: Option<RefundCountParams>,
) -> Result<u64, ResourceError> {
Err(ResourceError::PathResolutionFailed {
resource: Self::NAME,
operation: "count",
})
}
}
impl RestResource for RefundResource {
type Id = u64;
type FindParams = RefundFindParams;
type AllParams = RefundListParams;
type CountParams = RefundCountParams;
const NAME: &'static str = "Refund";
const PLURAL: &'static str = "refunds";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["order_id", "id"],
"orders/{order_id}/refunds/{id}",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::All,
&["order_id"],
"orders/{order_id}/refunds",
),
ResourcePath::new(
HttpMethod::Post,
ResourceOperation::Create,
&["order_id"],
"orders/{order_id}/refunds",
),
];
fn get_id(&self) -> Option<Self::Id> {
self.id
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct RefundFindParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub in_shop_currency: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct RefundListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub in_shop_currency: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct RefundCountParams {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_refund_nested_paths_require_order_id() {
let find_path =
get_path(RefundResource::PATHS, ResourceOperation::Find, &["order_id", "id"]);
assert!(find_path.is_some());
assert_eq!(
find_path.unwrap().template,
"orders/{order_id}/refunds/{id}"
);
let find_without_order = get_path(RefundResource::PATHS, ResourceOperation::Find, &["id"]);
assert!(find_without_order.is_none());
let all_path = get_path(RefundResource::PATHS, ResourceOperation::All, &["order_id"]);
assert!(all_path.is_some());
assert_eq!(all_path.unwrap().template, "orders/{order_id}/refunds");
let all_without_order = get_path(RefundResource::PATHS, ResourceOperation::All, &[]);
assert!(all_without_order.is_none());
let create_path = get_path(RefundResource::PATHS, ResourceOperation::Create, &["order_id"]);
assert!(create_path.is_some());
assert_eq!(create_path.unwrap().template, "orders/{order_id}/refunds");
let count_path = get_path(RefundResource::PATHS, ResourceOperation::Count, &["order_id"]);
assert!(count_path.is_none());
let update_path = get_path(
RefundResource::PATHS,
ResourceOperation::Update,
&["order_id", "id"],
);
assert!(update_path.is_none());
let delete_path = get_path(
RefundResource::PATHS,
ResourceOperation::Delete,
&["order_id", "id"],
);
assert!(delete_path.is_none());
}
#[test]
fn test_refund_calculate_path_construction() {
let order_id = 450789469u64;
let expected_path = format!("orders/{order_id}/refunds/calculate");
assert_eq!(expected_path, "orders/450789469/refunds/calculate");
}
#[test]
fn test_refund_struct_serialization() {
let refund = RefundResource {
id: Some(123456),
order_id: Some(450789469),
note: Some("Customer requested refund".to_string()),
user_id: Some(799407056),
restock: Some(true),
notify: Some(true),
created_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.unwrap()
.with_timezone(&Utc),
),
processed_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.unwrap()
.with_timezone(&Utc),
),
admin_graphql_api_id: Some("gid://shopify/Refund/123456".to_string()),
..Default::default()
};
let json = serde_json::to_string(&refund).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["order_id"], 450789469);
assert_eq!(parsed["note"], "Customer requested refund");
assert_eq!(parsed["user_id"], 799407056);
assert_eq!(parsed["restock"], true);
assert_eq!(parsed["notify"], true);
assert!(parsed.get("id").is_none());
assert!(parsed.get("created_at").is_none());
assert!(parsed.get("processed_at").is_none());
assert!(parsed.get("admin_graphql_api_id").is_none());
}
#[test]
fn test_refund_deserialization_with_complex_nested_structures() {
let json = r#"{
"id": 123456,
"order_id": 450789469,
"note": "Customer requested refund",
"user_id": 799407056,
"restock": true,
"processed_at": "2024-01-15T10:30:00Z",
"created_at": "2024-01-15T10:30:00Z",
"refund_line_items": [
{
"id": 1,
"quantity": 1,
"line_item_id": 669751112,
"location_id": 655441491,
"restock_type": "return",
"subtotal": "199.99",
"total_tax": "15.00",
"line_item": {
"id": 669751112,
"title": "IPod Nano - 8GB"
}
}
],
"transactions": [
{
"id": 389404469,
"order_id": 450789469,
"kind": "refund",
"amount": "214.99",
"status": "success"
}
],
"order_adjustments": [
{
"id": 1,
"order_id": 450789469,
"refund_id": 123456,
"kind": "refund_discrepancy",
"reason": "Refund discrepancy",
"amount": "-0.01"
}
],
"refund_shipping_lines": [
{
"id": 1,
"full_refund": true,
"amount": "5.00"
}
],
"admin_graphql_api_id": "gid://shopify/Refund/123456"
}"#;
let refund: RefundResource = serde_json::from_str(json).unwrap();
assert_eq!(refund.id, Some(123456));
assert_eq!(refund.order_id, Some(450789469));
assert_eq!(refund.note, Some("Customer requested refund".to_string()));
assert_eq!(refund.user_id, Some(799407056));
assert_eq!(refund.restock, Some(true));
assert!(refund.processed_at.is_some());
assert!(refund.created_at.is_some());
assert!(refund.refund_line_items.is_some());
assert!(refund.transactions.is_some());
assert!(refund.order_adjustments.is_some());
let adjustments = refund.order_adjustments.unwrap();
assert_eq!(adjustments.len(), 1);
assert_eq!(adjustments[0].kind, Some("refund_discrepancy".to_string()));
assert!(refund.refund_shipping_lines.is_some());
let shipping_lines = refund.refund_shipping_lines.unwrap();
assert_eq!(shipping_lines.len(), 1);
assert_eq!(shipping_lines[0].full_refund, Some(true));
assert_eq!(shipping_lines[0].amount, Some("5.00".to_string()));
}
#[test]
fn test_refund_calculate_params_serialization() {
let params = RefundCalculateParams {
shipping: Some(RefundShipping {
full_refund: Some(true),
amount: None,
}),
refund_line_items: Some(vec![
RefundLineItemInput {
line_item_id: 669751112,
quantity: 1,
restock_type: Some("return".to_string()),
},
]),
currency: Some("USD".to_string()),
};
let json = serde_json::to_value(¶ms).unwrap();
assert!(json["shipping"]["full_refund"].as_bool().unwrap());
assert_eq!(json["refund_line_items"][0]["line_item_id"], 669751112);
assert_eq!(json["refund_line_items"][0]["quantity"], 1);
assert_eq!(json["refund_line_items"][0]["restock_type"], "return");
assert_eq!(json["currency"], "USD");
}
#[test]
fn test_refund_list_params_serialization() {
let params = RefundListParams {
limit: Some(50),
fields: Some("id,note,created_at".to_string()),
in_shop_currency: Some(true),
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["limit"], 50);
assert_eq!(json["fields"], "id,note,created_at");
assert_eq!(json["in_shop_currency"], true);
let empty_params = RefundListParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_refund_get_id_returns_correct_value() {
let refund_with_id = RefundResource {
id: Some(123456),
order_id: Some(450789469),
note: Some("Test refund".to_string()),
..Default::default()
};
assert_eq!(refund_with_id.get_id(), Some(123456));
let refund_without_id = RefundResource {
id: None,
order_id: Some(450789469),
note: Some("New refund".to_string()),
..Default::default()
};
assert_eq!(refund_without_id.get_id(), None);
}
#[test]
fn test_refund_constants() {
assert_eq!(RefundResource::NAME, "Refund");
assert_eq!(RefundResource::PLURAL, "refunds");
}
#[test]
fn test_refund_line_item_serialization() {
let line_item = RefundLineItem {
id: Some(1),
quantity: Some(1),
line_item_id: Some(669751112),
location_id: Some(655441491),
restock_type: Some("return".to_string()),
subtotal: Some("199.99".to_string()),
total_tax: Some("15.00".to_string()),
..Default::default()
};
let json = serde_json::to_value(&line_item).unwrap();
assert_eq!(json["id"], 1);
assert_eq!(json["quantity"], 1);
assert_eq!(json["line_item_id"], 669751112);
assert_eq!(json["location_id"], 655441491);
assert_eq!(json["restock_type"], "return");
assert_eq!(json["subtotal"], "199.99");
assert_eq!(json["total_tax"], "15.00");
}
#[test]
fn test_order_adjustment_serialization() {
let adjustment = OrderAdjustment {
id: Some(1),
order_id: Some(450789469),
refund_id: Some(123456),
kind: Some("refund_discrepancy".to_string()),
reason: Some("Refund discrepancy".to_string()),
amount: Some("-0.01".to_string()),
tax_amount: Some("0.00".to_string()),
..Default::default()
};
let json = serde_json::to_value(&adjustment).unwrap();
assert_eq!(json["id"], 1);
assert_eq!(json["order_id"], 450789469);
assert_eq!(json["refund_id"], 123456);
assert_eq!(json["kind"], "refund_discrepancy");
assert_eq!(json["reason"], "Refund discrepancy");
assert_eq!(json["amount"], "-0.01");
}
}