use chrono::{DateTime, NaiveDate, 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 GiftCard {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing)]
pub balance: Option<String>,
#[serde(skip_serializing)]
pub disabled_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub line_item_id: Option<u64>,
#[serde(skip_serializing)]
pub api_client_id: Option<u64>,
#[serde(skip_serializing)]
pub user_id: Option<u64>,
#[serde(skip_serializing)]
pub last_characters: Option<String>,
#[serde(skip_serializing)]
pub order_id: Option<u64>,
#[serde(skip_serializing)]
pub created_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub updated_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub admin_graphql_api_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub currency: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub customer_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_on: Option<NaiveDate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template_suffix: Option<String>,
}
impl GiftCard {
pub fn is_enabled(&self) -> bool {
self.disabled_at.is_none()
}
pub fn is_disabled(&self) -> bool {
self.disabled_at.is_some()
}
}
impl RestResource for GiftCard {
type Id = u64;
type FindParams = GiftCardFindParams;
type AllParams = GiftCardListParams;
type CountParams = GiftCardCountParams;
const NAME: &'static str = "GiftCard";
const PLURAL: &'static str = "gift_cards";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"gift_cards/{id}",
),
ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "gift_cards"),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Count,
&[],
"gift_cards/count",
),
ResourcePath::new(
HttpMethod::Post,
ResourceOperation::Create,
&[],
"gift_cards",
),
ResourcePath::new(
HttpMethod::Put,
ResourceOperation::Update,
&["id"],
"gift_cards/{id}",
),
];
fn get_id(&self) -> Option<Self::Id> {
self.id
}
}
impl GiftCard {
pub async fn disable(&self, client: &RestClient) -> Result<Self, ResourceError> {
let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
resource: Self::NAME,
operation: "disable",
})?;
let path = format!("gift_cards/{id}/disable");
let body = serde_json::json!({});
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(&id.to_string()),
response.request_id(),
));
}
let gift_card: Self = response
.body
.get("gift_card")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'gift_card' 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 gift_card: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(gift_card)
}
pub async fn search(client: &RestClient, query: &str) -> Result<Vec<Self>, ResourceError> {
let path = format!("gift_cards/search");
let mut query_params = std::collections::HashMap::new();
query_params.insert("query".to_string(), query.to_string());
let response = client.get(&path, Some(query_params)).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
None,
response.request_id(),
));
}
let gift_cards: Vec<Self> = response
.body
.get("gift_cards")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'gift_cards' 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 gift_cards: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})
})?;
Ok(gift_cards)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct GiftCardFindParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct GiftCardListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub since_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_info: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct GiftCardCountParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_gift_card_struct_serialization() {
let gift_card = GiftCard {
id: Some(123456789),
balance: Some("75.00".to_string()),
disabled_at: None,
line_item_id: Some(111),
api_client_id: Some(222),
user_id: Some(333),
last_characters: Some("abc1".to_string()),
order_id: Some(444),
created_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.unwrap()
.with_timezone(&Utc),
),
updated_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.unwrap()
.with_timezone(&Utc),
),
admin_graphql_api_id: Some("gid://shopify/GiftCard/123456789".to_string()),
code: Some("GIFT1234ABCD5678".to_string()),
initial_value: Some("100.00".to_string()),
currency: Some("USD".to_string()),
customer_id: Some(789012),
note: Some("Employee reward".to_string()),
expires_on: Some(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()),
template_suffix: Some("premium".to_string()),
};
let json = serde_json::to_string(&gift_card).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["code"], "GIFT1234ABCD5678");
assert_eq!(parsed["initial_value"], "100.00");
assert_eq!(parsed["currency"], "USD");
assert_eq!(parsed["customer_id"], 789012);
assert_eq!(parsed["note"], "Employee reward");
assert_eq!(parsed["expires_on"], "2025-12-31");
assert_eq!(parsed["template_suffix"], "premium");
assert!(parsed.get("id").is_none());
assert!(parsed.get("balance").is_none());
assert!(parsed.get("disabled_at").is_none());
assert!(parsed.get("line_item_id").is_none());
assert!(parsed.get("api_client_id").is_none());
assert!(parsed.get("user_id").is_none());
assert!(parsed.get("last_characters").is_none());
assert!(parsed.get("order_id").is_none());
assert!(parsed.get("created_at").is_none());
assert!(parsed.get("updated_at").is_none());
assert!(parsed.get("admin_graphql_api_id").is_none());
}
#[test]
fn test_gift_card_deserialization_from_api_response() {
let json_str = r#"{
"id": 1035197676,
"balance": "100.00",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"currency": "USD",
"initial_value": "100.00",
"disabled_at": null,
"line_item_id": 466157049,
"api_client_id": 755357713,
"user_id": null,
"customer_id": 207119551,
"note": "Birthday gift for John",
"expires_on": "2025-12-31",
"template_suffix": null,
"last_characters": "0e0e",
"order_id": 450789469,
"admin_graphql_api_id": "gid://shopify/GiftCard/1035197676"
}"#;
let gift_card: GiftCard = serde_json::from_str(json_str).unwrap();
assert_eq!(gift_card.id, Some(1035197676));
assert_eq!(gift_card.balance.as_deref(), Some("100.00"));
assert_eq!(gift_card.currency.as_deref(), Some("USD"));
assert_eq!(gift_card.initial_value.as_deref(), Some("100.00"));
assert_eq!(gift_card.disabled_at, None);
assert_eq!(gift_card.line_item_id, Some(466157049));
assert_eq!(gift_card.api_client_id, Some(755357713));
assert_eq!(gift_card.user_id, None);
assert_eq!(gift_card.customer_id, Some(207119551));
assert_eq!(gift_card.note.as_deref(), Some("Birthday gift for John"));
assert_eq!(
gift_card.expires_on,
Some(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap())
);
assert_eq!(gift_card.template_suffix, None);
assert_eq!(gift_card.last_characters.as_deref(), Some("0e0e"));
assert_eq!(gift_card.order_id, Some(450789469));
assert!(gift_card.created_at.is_some());
assert!(gift_card.updated_at.is_some());
assert_eq!(gift_card.code, None);
}
#[test]
fn test_gift_card_path_constants() {
let find_path = get_path(GiftCard::PATHS, ResourceOperation::Find, &["id"]);
assert!(find_path.is_some());
assert_eq!(find_path.unwrap().template, "gift_cards/{id}");
assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
let all_path = get_path(GiftCard::PATHS, ResourceOperation::All, &[]);
assert!(all_path.is_some());
assert_eq!(all_path.unwrap().template, "gift_cards");
let count_path = get_path(GiftCard::PATHS, ResourceOperation::Count, &[]);
assert!(count_path.is_some());
assert_eq!(count_path.unwrap().template, "gift_cards/count");
let create_path = get_path(GiftCard::PATHS, ResourceOperation::Create, &[]);
assert!(create_path.is_some());
assert_eq!(create_path.unwrap().template, "gift_cards");
assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
let update_path = get_path(GiftCard::PATHS, ResourceOperation::Update, &["id"]);
assert!(update_path.is_some());
assert_eq!(update_path.unwrap().template, "gift_cards/{id}");
assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
let delete_path = get_path(GiftCard::PATHS, ResourceOperation::Delete, &["id"]);
assert!(delete_path.is_none());
assert_eq!(GiftCard::NAME, "GiftCard");
assert_eq!(GiftCard::PLURAL, "gift_cards");
}
#[test]
fn test_disable_method_signature() {
fn _assert_disable_signature<F, Fut>(f: F)
where
F: Fn(&GiftCard, &RestClient) -> Fut,
Fut: std::future::Future<Output = Result<GiftCard, ResourceError>>,
{
let _ = f;
}
let gift_card_without_id = GiftCard::default();
assert!(gift_card_without_id.get_id().is_none());
}
#[test]
fn test_search_method_signature() {
fn _assert_search_signature<F, Fut>(f: F)
where
F: Fn(&RestClient, &str) -> Fut,
Fut: std::future::Future<Output = Result<Vec<GiftCard>, ResourceError>>,
{
let _ = f;
}
}
#[test]
fn test_gift_card_is_enabled_disabled() {
let enabled_gift_card = GiftCard {
id: Some(123),
balance: Some("100.00".to_string()),
disabled_at: None,
..Default::default()
};
assert!(enabled_gift_card.is_enabled());
assert!(!enabled_gift_card.is_disabled());
let disabled_gift_card = GiftCard {
id: Some(456),
balance: Some("0.00".to_string()),
disabled_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.unwrap()
.with_timezone(&Utc),
),
..Default::default()
};
assert!(!disabled_gift_card.is_enabled());
assert!(disabled_gift_card.is_disabled());
}
#[test]
fn test_gift_card_get_id_returns_correct_value() {
let gift_card_with_id = GiftCard {
id: Some(1035197676),
balance: Some("100.00".to_string()),
..Default::default()
};
assert_eq!(gift_card_with_id.get_id(), Some(1035197676));
let gift_card_without_id = GiftCard {
id: None,
initial_value: Some("50.00".to_string()),
..Default::default()
};
assert_eq!(gift_card_without_id.get_id(), None);
}
#[test]
fn test_gift_card_list_params_serialization() {
let params = GiftCardListParams {
limit: Some(50),
since_id: Some(12345),
status: Some("enabled".to_string()),
fields: Some("id,balance,last_characters".to_string()),
page_info: None,
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["limit"], 50);
assert_eq!(json["since_id"], 12345);
assert_eq!(json["status"], "enabled");
assert_eq!(json["fields"], "id,balance,last_characters");
assert!(json.get("page_info").is_none());
let empty_params = GiftCardListParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_gift_card_count_params_serialization() {
let params = GiftCardCountParams {
status: Some("disabled".to_string()),
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["status"], "disabled");
let empty_params = GiftCardCountParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_gift_card_updatable_fields() {
let update_gift_card = GiftCard {
expires_on: Some(NaiveDate::from_ymd_opt(2026, 6, 30).unwrap()),
note: Some("Updated note".to_string()),
template_suffix: Some("custom".to_string()),
customer_id: Some(999999),
..Default::default()
};
let json = serde_json::to_value(&update_gift_card).unwrap();
assert_eq!(json["expires_on"], "2026-06-30");
assert_eq!(json["note"], "Updated note");
assert_eq!(json["template_suffix"], "custom");
assert_eq!(json["customer_id"], 999999);
}
#[test]
fn test_gift_card_code_is_write_only() {
let create_gift_card = GiftCard {
initial_value: Some("100.00".to_string()),
code: Some("MYGIFTCODE1234".to_string()),
..Default::default()
};
let json = serde_json::to_value(&create_gift_card).unwrap();
assert_eq!(json["code"], "MYGIFTCODE1234");
assert_eq!(json["initial_value"], "100.00");
let api_response = r#"{
"id": 123,
"balance": "100.00",
"initial_value": "100.00",
"last_characters": "1234"
}"#;
let gift_card: GiftCard = serde_json::from_str(api_response).unwrap();
assert_eq!(gift_card.code, None);
assert_eq!(gift_card.last_characters.as_deref(), Some("1234"));
}
}