Skip to main content

shopify_sdk/rest/resources/v2025_10/
policy.rs

1//! Policy resource implementation.
2//!
3//! This module provides the [`Policy`] resource for retrieving store policies
4//! like refund policy, privacy policy, terms of service, and shipping policy.
5//!
6//! # Read-Only Resource
7//!
8//! Policies implement [`ReadOnlyResource`](crate::rest::ReadOnlyResource) - they
9//! can only be retrieved, not created, updated, or deleted through the API.
10//! Policies are managed through the Shopify admin.
11//!
12//! # Special Characteristics
13//!
14//! - **No ID field**: Policies are identified by their `handle`, not a numeric ID
15//! - **List only**: No Find by ID or Count endpoints
16//! - **Read-only**: Policies cannot be modified through the API
17//!
18//! # Example
19//!
20//! ```rust,ignore
21//! use shopify_sdk::rest::{RestResource, ResourceResponse};
22//! use shopify_sdk::rest::resources::v2025_10::Policy;
23//!
24//! // List all store policies
25//! let policies = Policy::all(&client, None).await?;
26//! for policy in policies.iter() {
27//!     println!("{}: {}", policy.title.as_deref().unwrap_or(""),
28//!         policy.handle.as_deref().unwrap_or(""));
29//! }
30//! ```
31
32use chrono::{DateTime, Utc};
33use serde::{Deserialize, Serialize};
34
35use crate::rest::{ReadOnlyResource, ResourceOperation, ResourcePath, RestResource};
36use crate::HttpMethod;
37
38/// A store policy (refund, privacy, terms of service, shipping).
39///
40/// Policies are read-only records that merchants configure through the
41/// Shopify admin. They cannot be created, updated, or deleted through
42/// the API.
43///
44/// # Read-Only Resource
45///
46/// This resource implements [`ReadOnlyResource`] - only GET operations are
47/// available. Policies are managed through the Shopify admin.
48///
49/// # No ID Field
50///
51/// Unlike most resources, policies don't have a numeric ID. They are
52/// identified by their `handle` (e.g., "refund-policy", "privacy-policy").
53///
54/// # Fields
55///
56/// All fields are read-only:
57/// - `title` - The title of the policy (e.g., "Refund Policy")
58/// - `body` - The HTML content of the policy
59/// - `handle` - The URL-friendly identifier
60/// - `url` - The public URL of the policy
61/// - `created_at` - When the policy was created
62/// - `updated_at` - When the policy was last updated
63#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
64pub struct Policy {
65    /// The title of the policy.
66    #[serde(skip_serializing)]
67    pub title: Option<String>,
68
69    /// The HTML content of the policy.
70    #[serde(skip_serializing)]
71    pub body: Option<String>,
72
73    /// The URL-friendly identifier of the policy.
74    /// Used as the identifier since policies don't have numeric IDs.
75    #[serde(skip_serializing)]
76    pub handle: Option<String>,
77
78    /// The public URL where the policy can be viewed.
79    #[serde(skip_serializing)]
80    pub url: Option<String>,
81
82    /// When the policy was created.
83    #[serde(skip_serializing)]
84    pub created_at: Option<DateTime<Utc>>,
85
86    /// When the policy was last updated.
87    #[serde(skip_serializing)]
88    pub updated_at: Option<DateTime<Utc>>,
89}
90
91impl RestResource for Policy {
92    /// Policies don't have a numeric ID - they use handles.
93    /// We use String as the ID type but `get_id()` always returns None.
94    type Id = String;
95    type FindParams = ();
96    type AllParams = ();
97    type CountParams = ();
98
99    const NAME: &'static str = "Policy";
100    const PLURAL: &'static str = "policies";
101
102    /// Paths for the Policy resource.
103    ///
104    /// Only list operation is available - no Find by ID or Count.
105    const PATHS: &'static [ResourcePath] = &[
106        ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "policies"),
107        // Note: No Find path - policies don't have numeric IDs
108        // Note: No Count path - not supported by the API
109        // Note: No Create, Update, or Delete paths - read-only resource
110    ];
111
112    /// Policies don't have a standard ID - they use handles.
113    /// Returns None for compatibility with the RestResource trait.
114    fn get_id(&self) -> Option<Self::Id> {
115        // Return the handle as a fallback identifier
116        self.handle.clone()
117    }
118}
119
120impl ReadOnlyResource for Policy {}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::rest::{get_path, ReadOnlyResource, ResourceOperation, RestResource};
126
127    #[test]
128    fn test_policy_implements_read_only_resource() {
129        // This test verifies that Policy implements ReadOnlyResource
130        fn assert_read_only<T: ReadOnlyResource>() {}
131        assert_read_only::<Policy>();
132    }
133
134    #[test]
135    fn test_policy_deserialization() {
136        let json = r#"{
137            "title": "Refund Policy",
138            "body": "<p>We offer a 30-day return policy...</p>",
139            "handle": "refund-policy",
140            "url": "https://example.myshopify.com/policies/refund-policy",
141            "created_at": "2024-01-15T10:30:00Z",
142            "updated_at": "2024-06-20T15:45:00Z"
143        }"#;
144
145        let policy: Policy = serde_json::from_str(json).unwrap();
146
147        assert_eq!(policy.title, Some("Refund Policy".to_string()));
148        assert_eq!(
149            policy.body,
150            Some("<p>We offer a 30-day return policy...</p>".to_string())
151        );
152        assert_eq!(policy.handle, Some("refund-policy".to_string()));
153        assert_eq!(
154            policy.url,
155            Some("https://example.myshopify.com/policies/refund-policy".to_string())
156        );
157        assert!(policy.created_at.is_some());
158        assert!(policy.updated_at.is_some());
159    }
160
161    #[test]
162    fn test_policy_read_only_paths() {
163        // Only list path available
164        let all_path = get_path(Policy::PATHS, ResourceOperation::All, &[]);
165        assert!(all_path.is_some());
166        assert_eq!(all_path.unwrap().template, "policies");
167
168        // No find path (policies identified by handle, not ID)
169        let find_path = get_path(Policy::PATHS, ResourceOperation::Find, &["id"]);
170        assert!(find_path.is_none());
171
172        // No count path
173        let count_path = get_path(Policy::PATHS, ResourceOperation::Count, &[]);
174        assert!(count_path.is_none());
175
176        // No create, update, or delete paths
177        let create_path = get_path(Policy::PATHS, ResourceOperation::Create, &[]);
178        assert!(create_path.is_none());
179
180        let update_path = get_path(Policy::PATHS, ResourceOperation::Update, &["id"]);
181        assert!(update_path.is_none());
182
183        let delete_path = get_path(Policy::PATHS, ResourceOperation::Delete, &["id"]);
184        assert!(delete_path.is_none());
185    }
186
187    #[test]
188    fn test_policy_has_no_standard_id() {
189        // Policy uses handle instead of numeric ID
190        let policy = Policy {
191            title: Some("Privacy Policy".to_string()),
192            handle: Some("privacy-policy".to_string()),
193            ..Default::default()
194        };
195
196        // get_id returns the handle as a fallback
197        assert_eq!(policy.get_id(), Some("privacy-policy".to_string()));
198
199        // Policy without handle returns None
200        let policy_without_handle = Policy {
201            title: Some("Some Policy".to_string()),
202            handle: None,
203            ..Default::default()
204        };
205        assert_eq!(policy_without_handle.get_id(), None);
206    }
207
208    #[test]
209    fn test_policy_constants() {
210        assert_eq!(Policy::NAME, "Policy");
211        assert_eq!(Policy::PLURAL, "policies");
212    }
213
214    #[test]
215    fn test_policy_all_fields_are_read_only() {
216        // All fields should be skipped during serialization
217        let policy = Policy {
218            title: Some("Test Policy".to_string()),
219            body: Some("<p>Content</p>".to_string()),
220            handle: Some("test-policy".to_string()),
221            url: Some("https://example.com/policies/test".to_string()),
222            created_at: Some(
223                DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
224                    .unwrap()
225                    .with_timezone(&Utc),
226            ),
227            updated_at: Some(
228                DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
229                    .unwrap()
230                    .with_timezone(&Utc),
231            ),
232        };
233
234        let json = serde_json::to_value(&policy).unwrap();
235        // All fields should be omitted (empty object)
236        assert_eq!(json, serde_json::json!({}));
237    }
238}