use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::rest::{ResourceOperation, ResourcePath, RestResource};
use crate::HttpMethod;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PriceRuleValueType {
FixedAmount,
Percentage,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PriceRuleAllocationMethod {
Each,
Across,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PriceRuleCustomerSelection {
All,
Prerequisite,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PriceRuleTargetType {
LineItem,
ShippingLine,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PriceRuleTargetSelection {
All,
Entitled,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct PriceRule {
#[serde(skip_serializing)]
pub id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value_type: Option<PriceRuleValueType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub customer_selection: Option<PriceRuleCustomerSelection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_type: Option<PriceRuleTargetType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_selection: Option<PriceRuleTargetSelection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allocation_method: Option<PriceRuleAllocationMethod>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allocation_limit: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub once_per_customer: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_limit: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub starts_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ends_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prerequisite_subtotal_range: Option<PrerequisiteRange>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prerequisite_quantity_range: Option<PrerequisiteRange>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prerequisite_shipping_price_range: Option<PrerequisiteRange>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prerequisite_collection_ids: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prerequisite_variant_ids: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prerequisite_product_ids: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prerequisite_customer_ids: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entitled_collection_ids: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entitled_product_ids: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entitled_variant_ids: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entitled_country_ids: Option<Vec<u64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prerequisite_to_entitlement_purchase: Option<PrerequisiteToEntitlement>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prerequisite_to_entitlement_quantity_ratio: Option<BxgyRatio>,
#[serde(skip_serializing)]
pub times_used: Option<i32>,
#[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>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct PrerequisiteRange {
#[serde(skip_serializing_if = "Option::is_none")]
pub greater_than_or_equal_to: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct PrerequisiteToEntitlement {
#[serde(rename = "prerequisite_amount", skip_serializing_if = "Option::is_none")]
pub prerequisite_amount: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct BxgyRatio {
#[serde(skip_serializing_if = "Option::is_none")]
pub prerequisite_quantity: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entitled_quantity: Option<i32>,
}
impl RestResource for PriceRule {
type Id = u64;
type FindParams = PriceRuleFindParams;
type AllParams = PriceRuleListParams;
type CountParams = PriceRuleCountParams;
const NAME: &'static str = "PriceRule";
const PLURAL: &'static str = "price_rules";
const PATHS: &'static [ResourcePath] = &[
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Find,
&["id"],
"price_rules/{id}",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::All,
&[],
"price_rules",
),
ResourcePath::new(
HttpMethod::Get,
ResourceOperation::Count,
&[],
"price_rules/count",
),
ResourcePath::new(
HttpMethod::Post,
ResourceOperation::Create,
&[],
"price_rules",
),
ResourcePath::new(
HttpMethod::Put,
ResourceOperation::Update,
&["id"],
"price_rules/{id}",
),
ResourcePath::new(
HttpMethod::Delete,
ResourceOperation::Delete,
&["id"],
"price_rules/{id}",
),
];
fn get_id(&self) -> Option<Self::Id> {
self.id
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct PriceRuleFindParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct PriceRuleListParams {
#[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 created_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub starts_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub starts_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ends_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ends_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub times_used: Option<i32>,
#[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 PriceRuleCountParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub starts_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub starts_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ends_at_min: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ends_at_max: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub times_used: Option<i32>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rest::{get_path, ResourceOperation};
#[test]
fn test_price_rule_serialization() {
let rule = PriceRule {
id: Some(12345),
title: Some("20% Off Sale".to_string()),
value_type: Some(PriceRuleValueType::Percentage),
value: Some("-20.0".to_string()),
customer_selection: Some(PriceRuleCustomerSelection::All),
target_type: Some(PriceRuleTargetType::LineItem),
target_selection: Some(PriceRuleTargetSelection::All),
allocation_method: Some(PriceRuleAllocationMethod::Across),
once_per_customer: Some(true),
usage_limit: Some(100),
starts_at: Some(
DateTime::parse_from_rfc3339("2024-01-15T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
),
ends_at: Some(
DateTime::parse_from_rfc3339("2024-12-31T23:59:59Z")
.unwrap()
.with_timezone(&Utc),
),
times_used: Some(50),
created_at: Some(
DateTime::parse_from_rfc3339("2024-01-10T08:00:00Z")
.unwrap()
.with_timezone(&Utc),
),
updated_at: Some(
DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
.unwrap()
.with_timezone(&Utc),
),
admin_graphql_api_id: Some("gid://shopify/PriceRule/12345".to_string()),
..Default::default()
};
let json = serde_json::to_string(&rule).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["title"], "20% Off Sale");
assert_eq!(parsed["value_type"], "percentage");
assert_eq!(parsed["value"], "-20.0");
assert_eq!(parsed["customer_selection"], "all");
assert_eq!(parsed["target_type"], "line_item");
assert_eq!(parsed["target_selection"], "all");
assert_eq!(parsed["allocation_method"], "across");
assert_eq!(parsed["once_per_customer"], true);
assert_eq!(parsed["usage_limit"], 100);
assert!(parsed["starts_at"].as_str().is_some());
assert!(parsed["ends_at"].as_str().is_some());
assert!(parsed.get("id").is_none());
assert!(parsed.get("times_used").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_price_rule_deserialization() {
let json = r#"{
"id": 507328175,
"title": "Summer Sale",
"value_type": "fixed_amount",
"value": "-10.0",
"customer_selection": "all",
"target_type": "line_item",
"target_selection": "all",
"allocation_method": "across",
"allocation_limit": null,
"once_per_customer": false,
"usage_limit": null,
"starts_at": "2024-06-01T00:00:00Z",
"ends_at": "2024-08-31T23:59:59Z",
"times_used": 25,
"created_at": "2024-05-15T10:30:00Z",
"updated_at": "2024-06-20T15:45:00Z",
"admin_graphql_api_id": "gid://shopify/PriceRule/507328175"
}"#;
let rule: PriceRule = serde_json::from_str(json).unwrap();
assert_eq!(rule.id, Some(507328175));
assert_eq!(rule.title, Some("Summer Sale".to_string()));
assert_eq!(rule.value_type, Some(PriceRuleValueType::FixedAmount));
assert_eq!(rule.value, Some("-10.0".to_string()));
assert_eq!(rule.customer_selection, Some(PriceRuleCustomerSelection::All));
assert_eq!(rule.target_type, Some(PriceRuleTargetType::LineItem));
assert_eq!(rule.target_selection, Some(PriceRuleTargetSelection::All));
assert_eq!(rule.allocation_method, Some(PriceRuleAllocationMethod::Across));
assert_eq!(rule.once_per_customer, Some(false));
assert_eq!(rule.times_used, Some(25));
assert!(rule.created_at.is_some());
assert!(rule.updated_at.is_some());
assert_eq!(
rule.admin_graphql_api_id,
Some("gid://shopify/PriceRule/507328175".to_string())
);
}
#[test]
fn test_price_rule_full_crud_paths() {
let find_path = get_path(PriceRule::PATHS, ResourceOperation::Find, &["id"]);
assert!(find_path.is_some());
assert_eq!(find_path.unwrap().template, "price_rules/{id}");
let all_path = get_path(PriceRule::PATHS, ResourceOperation::All, &[]);
assert!(all_path.is_some());
assert_eq!(all_path.unwrap().template, "price_rules");
let count_path = get_path(PriceRule::PATHS, ResourceOperation::Count, &[]);
assert!(count_path.is_some());
assert_eq!(count_path.unwrap().template, "price_rules/count");
let create_path = get_path(PriceRule::PATHS, ResourceOperation::Create, &[]);
assert!(create_path.is_some());
assert_eq!(create_path.unwrap().template, "price_rules");
assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
let update_path = get_path(PriceRule::PATHS, ResourceOperation::Update, &["id"]);
assert!(update_path.is_some());
assert_eq!(update_path.unwrap().template, "price_rules/{id}");
assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
let delete_path = get_path(PriceRule::PATHS, ResourceOperation::Delete, &["id"]);
assert!(delete_path.is_some());
assert_eq!(delete_path.unwrap().template, "price_rules/{id}");
assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
}
#[test]
fn test_price_rule_value_type_enum_serialization() {
let percentage = PriceRuleValueType::Percentage;
let json = serde_json::to_value(&percentage).unwrap();
assert_eq!(json, "percentage");
let fixed = PriceRuleValueType::FixedAmount;
let json = serde_json::to_value(&fixed).unwrap();
assert_eq!(json, "fixed_amount");
let parsed: PriceRuleValueType = serde_json::from_str("\"fixed_amount\"").unwrap();
assert_eq!(parsed, PriceRuleValueType::FixedAmount);
}
#[test]
fn test_price_rule_list_params_with_date_ranges() {
let params = PriceRuleListParams {
limit: Some(50),
created_at_min: Some(
DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
),
starts_at_min: Some(
DateTime::parse_from_rfc3339("2024-06-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
),
ends_at_max: Some(
DateTime::parse_from_rfc3339("2024-12-31T23:59:59Z")
.unwrap()
.with_timezone(&Utc),
),
times_used: Some(10),
..Default::default()
};
let json = serde_json::to_value(¶ms).unwrap();
assert_eq!(json["limit"], 50);
assert!(json["created_at_min"].as_str().is_some());
assert!(json["starts_at_min"].as_str().is_some());
assert!(json["ends_at_max"].as_str().is_some());
assert_eq!(json["times_used"], 10);
assert!(json.get("since_id").is_none());
assert!(json.get("updated_at_min").is_none());
}
#[test]
fn test_price_rule_constants() {
assert_eq!(PriceRule::NAME, "PriceRule");
assert_eq!(PriceRule::PLURAL, "price_rules");
}
#[test]
fn test_price_rule_get_id() {
let rule_with_id = PriceRule {
id: Some(12345),
title: Some("Test Rule".to_string()),
..Default::default()
};
assert_eq!(rule_with_id.get_id(), Some(12345));
let rule_without_id = PriceRule::default();
assert_eq!(rule_without_id.get_id(), None);
}
}