use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::clients::RestClient;
use crate::rest::{
build_path, get_path, ResourceError, ResourceOperation, ResourcePath, ResourceResponse,
RestResource,
};
use crate::HttpMethod;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct DiscountCode {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price_rule_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing)]
pub usage_count: Option<i32>,
#[serde(skip_serializing)]
pub errors: Option<Vec<DiscountCodeError>>,
#[serde(skip_serializing)]
pub created_at: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct DiscountCodeError {
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct DiscountCodeBatchResult {
pub id: Option<u64>,
pub price_rule_id: Option<u64>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
pub status: Option<String>,
pub codes_count: Option<i32>,
pub imported_count: Option<i32>,
pub failed_count: Option<i32>,
pub logs: Option<Vec<String>>,
}
impl DiscountCode {
pub async fn count_with_parent(
client: &RestClient,
price_rule_id: u64,
params: Option<DiscountCodeCountParams>,
) -> Result<u64, ResourceError> {
let mut ids: HashMap<&str, String> = HashMap::new();
ids.insert("price_rule_id", price_rule_id.to_string());
let available_ids: Vec<&str> = ids.keys().copied().collect();
let path = get_path(Self::PATHS, ResourceOperation::Count, &available_ids).ok_or(
ResourceError::PathResolutionFailed {
resource: Self::NAME,
operation: "count",
},
)?;
let url = build_path(path.template, &ids);
let query = params
.map(|p| {
let value = serde_json::to_value(&p).map_err(|e| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: 400,
message: format!("Failed to serialize params: {e}"),
error_reference: None,
},
))
})?;
let mut query = HashMap::new();
if let serde_json::Value::Object(map) = value {
for (key, val) in map {
match val {
serde_json::Value::String(s) => {
query.insert(key, s);
}
serde_json::Value::Number(n) => {
query.insert(key, n.to_string());
}
serde_json::Value::Bool(b) => {
query.insert(key, b.to_string());
}
_ => {}
}
}
}
Ok::<_, ResourceError>(query)
})
.transpose()?
.filter(|q| !q.is_empty());
let response = client.get(&url, query).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
None,
response.request_id(),
));
}
let count = response
.body
.get("count")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'count' in response".to_string(),
error_reference: response.request_id().map(ToString::to_string),
},
))
})?;
Ok(count)
}
pub async fn find_with_parent(
client: &RestClient,
price_rule_id: u64,
id: u64,
_params: Option<DiscountCodeFindParams>,
) -> Result<ResourceResponse<Self>, ResourceError> {
let mut ids: HashMap<&str, String> = HashMap::new();
ids.insert("price_rule_id", price_rule_id.to_string());
ids.insert("id", id.to_string());
let available_ids: Vec<&str> = ids.keys().copied().collect();
let path = get_path(Self::PATHS, ResourceOperation::Find, &available_ids).ok_or(
ResourceError::PathResolutionFailed {
resource: Self::NAME,
operation: "find",
},
)?;
let url = build_path(path.template, &ids);
let response = client.get(&url, 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 key = Self::resource_key();
ResourceResponse::from_http_response(response, &key)
}
pub async fn lookup(
client: &RestClient,
code: &str,
) -> Result<ResourceResponse<Self>, ResourceError> {
let url = "discount_codes/lookup";
let mut query = HashMap::new();
query.insert("code".to_string(), code.to_string());
let response = client.get(url, Some(query)).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
Some(code),
response.request_id(),
));
}
let key = Self::resource_key();
ResourceResponse::from_http_response(response, &key)
}
pub async fn batch(
client: &RestClient,
price_rule_id: u64,
codes: Vec<String>,
) -> Result<DiscountCodeBatchResult, ResourceError> {
let url = format!("price_rules/{price_rule_id}/batch");
let discount_codes: Vec<serde_json::Value> = codes
.into_iter()
.map(|code| serde_json::json!({ "code": code }))
.collect();
let body = serde_json::json!({
"discount_codes": discount_codes
});
let response = client.post(&url, body, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
None,
response.request_id(),
));
}
let result = response
.body
.get("discount_code_creation")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'discount_code_creation' in response".to_string(),
error_reference: response.request_id().map(ToString::to_string),
},
))
})?;
let batch_result: DiscountCodeBatchResult =
serde_json::from_value(result.clone()).map_err(|e| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: format!("Failed to parse batch result: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})?;
Ok(batch_result)
}
pub async fn batch_status(
client: &RestClient,
price_rule_id: u64,
batch_id: u64,
) -> Result<DiscountCodeBatchResult, ResourceError> {
let url = format!("price_rules/{price_rule_id}/batch/{batch_id}");
let response = client.get(&url, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
Some(&batch_id.to_string()),
response.request_id(),
));
}
let result = response
.body
.get("discount_code_creation")
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: "Missing 'discount_code_creation' in response".to_string(),
error_reference: response.request_id().map(ToString::to_string),
},
))
})?;
let batch_result: DiscountCodeBatchResult =
serde_json::from_value(result.clone()).map_err(|e| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: format!("Failed to parse batch result: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})?;
Ok(batch_result)
}
pub async fn batch_codes(
client: &RestClient,
price_rule_id: u64,
batch_id: u64,
) -> Result<Vec<Self>, ResourceError> {
let url = format!("price_rules/{price_rule_id}/batch/{batch_id}/discount_codes");
let response = client.get(&url, None).await?;
if !response.is_ok() {
return Err(ResourceError::from_http_response(
response.code,
&response.body,
Self::NAME,
Some(&batch_id.to_string()),
response.request_id(),
));
}
let codes_value = response
.body
.get(Self::PLURAL)
.ok_or_else(|| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: format!("Missing '{}' in response", Self::PLURAL),
error_reference: response.request_id().map(ToString::to_string),
},
))
})?;
let codes: Vec<Self> = serde_json::from_value(codes_value.clone()).map_err(|e| {
ResourceError::Http(crate::clients::HttpError::Response(
crate::clients::HttpResponseError {
code: response.code,
message: format!("Failed to parse discount codes: {e}"),
error_reference: response.request_id().map(ToString::to_string),
},
))
})?;
Ok(codes)
}
}
impl RestResource for DiscountCode {
type Id = u64;
type FindParams = DiscountCodeFindParams;
type AllParams = DiscountCodeListParams;
type CountParams = DiscountCodeCountParams;
const NAME: &'static str = "DiscountCode";
const PLURAL: &'static str = "discount_codes";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["price_rule_id", "id"],
"price_rules/{price_rule_id}/discount_codes/{id}",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::All,
&["price_rule_id"],
"price_rules/{price_rule_id}/discount_codes",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Count,
&["price_rule_id"],
"price_rules/{price_rule_id}/discount_codes/count",
),
ResourcePath::new(
HttpMethod::Post,
ResourceOperation::Create,
&["price_rule_id"],
"price_rules/{price_rule_id}/discount_codes",
),
ResourcePath::new(
HttpMethod::Put,
ResourceOperation::Update,
&["price_rule_id", "id"],
"price_rules/{price_rule_id}/discount_codes/{id}",
),
ResourcePath::new(
HttpMethod::Delete,
ResourceOperation::Delete,
&["price_rule_id", "id"],
"price_rules/{price_rule_id}/discount_codes/{id}",
),
];
fn get_id(&self) -> Option<Self::Id> {
self.id
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct DiscountCodeFindParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct DiscountCodeListParams {
#[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 page_info: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct DiscountCodeCountParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub times_used: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub times_used_min: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub times_used_max: Option<i32>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_discount_code_serialization() {
let code = DiscountCode {
id: Some(12345),
price_rule_id: Some(507328175),
code: Some("SUMMER20".to_string()),
usage_count: Some(42),
errors: None,
created_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
.unwrap()
.with_timezone(&Utc),
),
updated_at: Some(
DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
.unwrap()
.with_timezone(&Utc),
),
};
let json = serde_json::to_string(&code).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["price_rule_id"], 507328175);
assert_eq!(parsed["code"], "SUMMER20");
assert!(parsed.get("id").is_none());
assert!(parsed.get("usage_count").is_none());
assert!(parsed.get("errors").is_none());
assert!(parsed.get("created_at").is_none());
assert!(parsed.get("updated_at").is_none());
}
#[test]
fn test_discount_code_deserialization() {
let json = r#"{
"id": 1054381139,
"price_rule_id": 507328175,
"code": "SUMMERSALE20OFF",
"usage_count": 25,
"errors": [],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-06-20T15:45:00Z"
}"#;
let code: DiscountCode = serde_json::from_str(json).unwrap();
assert_eq!(code.id, Some(1054381139));
assert_eq!(code.price_rule_id, Some(507328175));
assert_eq!(code.code, Some("SUMMERSALE20OFF".to_string()));
assert_eq!(code.usage_count, Some(25));
assert!(code.errors.is_some());
assert!(code.errors.unwrap().is_empty());
assert!(code.created_at.is_some());
assert!(code.updated_at.is_some());
}
#[test]
fn test_discount_code_nested_paths() {
let find_path = get_path(
DiscountCode::PATHS,
ResourceOperation::Find,
&["price_rule_id", "id"],
);
assert!(find_path.is_some());
assert_eq!(
find_path.unwrap().template,
"price_rules/{price_rule_id}/discount_codes/{id}"
);
let find_without_parent = get_path(DiscountCode::PATHS, ResourceOperation::Find, &["id"]);
assert!(find_without_parent.is_none());
let all_path = get_path(
DiscountCode::PATHS,
ResourceOperation::All,
&["price_rule_id"],
);
assert!(all_path.is_some());
assert_eq!(
all_path.unwrap().template,
"price_rules/{price_rule_id}/discount_codes"
);
let all_without_parent = get_path(DiscountCode::PATHS, ResourceOperation::All, &[]);
assert!(all_without_parent.is_none());
let count_path = get_path(
DiscountCode::PATHS,
ResourceOperation::Count,
&["price_rule_id"],
);
assert!(count_path.is_some());
assert_eq!(
count_path.unwrap().template,
"price_rules/{price_rule_id}/discount_codes/count"
);
let create_path = get_path(
DiscountCode::PATHS,
ResourceOperation::Create,
&["price_rule_id"],
);
assert!(create_path.is_some());
assert_eq!(
create_path.unwrap().template,
"price_rules/{price_rule_id}/discount_codes"
);
assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
let update_path = get_path(
DiscountCode::PATHS,
ResourceOperation::Update,
&["price_rule_id", "id"],
);
assert!(update_path.is_some());
assert_eq!(
update_path.unwrap().template,
"price_rules/{price_rule_id}/discount_codes/{id}"
);
assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
let delete_path = get_path(
DiscountCode::PATHS,
ResourceOperation::Delete,
&["price_rule_id", "id"],
);
assert!(delete_path.is_some());
assert_eq!(
delete_path.unwrap().template,
"price_rules/{price_rule_id}/discount_codes/{id}"
);
assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
}
#[test]
fn test_discount_code_lookup_is_standalone_path() {
}
#[test]
fn test_discount_code_batch_result_deserialization() {
let json = r#"{
"id": 173232803,
"price_rule_id": 507328175,
"started_at": "2024-06-15T10:00:00Z",
"completed_at": "2024-06-15T10:05:00Z",
"created_at": "2024-06-15T09:55:00Z",
"updated_at": "2024-06-15T10:05:00Z",
"status": "completed",
"codes_count": 3,
"imported_count": 3,
"failed_count": 0,
"logs": []
}"#;
let result: DiscountCodeBatchResult = serde_json::from_str(json).unwrap();
assert_eq!(result.id, Some(173232803));
assert_eq!(result.price_rule_id, Some(507328175));
assert_eq!(result.status, Some("completed".to_string()));
assert_eq!(result.codes_count, Some(3));
assert_eq!(result.imported_count, Some(3));
assert_eq!(result.failed_count, Some(0));
assert!(result.started_at.is_some());
assert!(result.completed_at.is_some());
}
#[test]
fn test_discount_code_count_params() {
let params = DiscountCodeCountParams {
times_used: Some(5),
times_used_min: Some(1),
times_used_max: Some(100),
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["times_used"], 5);
assert_eq!(json["times_used_min"], 1);
assert_eq!(json["times_used_max"], 100);
let empty_params = DiscountCodeCountParams::default();
let empty_json = serde_json::to_value(&empty_params).unwrap();
assert_eq!(empty_json, serde_json::json!({}));
}
#[test]
fn test_discount_code_constants() {
assert_eq!(DiscountCode::NAME, "DiscountCode");
assert_eq!(DiscountCode::PLURAL, "discount_codes");
}
#[test]
fn test_discount_code_get_id() {
let code_with_id = DiscountCode {
id: Some(12345),
code: Some("TEST".to_string()),
..Default::default()
};
assert_eq!(code_with_id.get_id(), Some(12345));
let code_without_id = DiscountCode::default();
assert_eq!(code_without_id.get_id(), None);
}
}