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}