Skip to main content

shopify_sdk/rest/resources/v2025_10/
event.rs

1//! Event resource implementation.
2//!
3//! This module provides the [`Event`] resource for viewing events that have
4//! occurred in a Shopify store. Events represent actions taken on resources
5//! like products, orders, and articles.
6//!
7//! # Read-Only Resource
8//!
9//! Events implement [`ReadOnlyResource`](crate::rest::ReadOnlyResource) - they
10//! can only be retrieved, not created, updated, or deleted. Events are
11//! automatically recorded by Shopify when actions occur.
12//!
13//! # Polymorphic Owner Paths
14//!
15//! Events support optional polymorphic paths based on the owner resource:
16//! - Global: `/events.json` (all events)
17//! - Orders: `/orders/{order_id}/events.json`
18//! - Products: `/products/{product_id}/events.json`
19//!
20//! # Example
21//!
22//! ```rust,ignore
23//! use shopify_sdk::rest::{RestResource, ResourceResponse};
24//! use shopify_sdk::rest::resources::v2025_10::{Event, EventListParams};
25//!
26//! // List all events
27//! let events = Event::all(&client, None).await?;
28//! for event in events.iter() {
29//!     println!("{}: {} {}", event.verb.as_deref().unwrap_or(""),
30//!         event.subject_type.as_deref().unwrap_or(""),
31//!         event.subject_id.unwrap_or(0));
32//! }
33//!
34//! // List events for a specific order
35//! let events = Event::all_for_owner(
36//!     &client,
37//!     "order_id",
38//!     450789469,
39//!     None
40//! ).await?;
41//!
42//! // Filter events by verb
43//! let params = EventListParams {
44//!     verb: Some("create".to_string()),
45//!     filter: Some("Product".to_string()),
46//!     limit: Some(50),
47//!     ..Default::default()
48//! };
49//! let events = Event::all(&client, Some(params)).await?;
50//! ```
51
52use std::collections::HashMap;
53
54use chrono::{DateTime, Utc};
55use serde::{Deserialize, Serialize};
56
57use crate::clients::RestClient;
58use crate::rest::{
59    build_path, get_path, ReadOnlyResource, ResourceError, ResourceOperation, ResourcePath,
60    ResourceResponse, RestResource,
61};
62use crate::HttpMethod;
63
64/// An event representing an action in the store.
65///
66/// Events are read-only records of actions that have occurred on resources.
67/// They are automatically created by Shopify and cannot be modified.
68///
69/// # Read-Only Resource
70///
71/// This resource implements [`ReadOnlyResource`] - only GET operations are
72/// available. Events are recorded automatically when actions occur.
73///
74/// # Fields
75///
76/// All fields are read-only:
77/// - `id` - The unique identifier
78/// - `subject_id` - The ID of the resource the event is for
79/// - `subject_type` - The type of resource (e.g., "Product", "Order")
80/// - `verb` - The action that occurred (e.g., "create", "update", "destroy")
81/// - `arguments` - Any additional arguments for the event
82/// - `message` - Human-readable description of the event
83/// - `description` - Detailed description of the event
84/// - `body` - The body of the event (for some event types)
85/// - `path` - The relative path to the resource
86/// - `created_at` - When the event occurred
87#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
88pub struct Event {
89    /// The unique identifier of the event.
90    #[serde(skip_serializing)]
91    pub id: Option<u64>,
92
93    /// The ID of the resource the event is for.
94    #[serde(skip_serializing)]
95    pub subject_id: Option<u64>,
96
97    /// The type of resource (e.g., "Product", "Order", "Article").
98    #[serde(skip_serializing)]
99    pub subject_type: Option<String>,
100
101    /// The action that occurred (e.g., "create", "update", "destroy").
102    #[serde(skip_serializing)]
103    pub verb: Option<String>,
104
105    /// Additional arguments for the event.
106    #[serde(skip_serializing)]
107    pub arguments: Option<Vec<String>>,
108
109    /// Human-readable description of the event.
110    #[serde(skip_serializing)]
111    pub message: Option<String>,
112
113    /// Detailed description of the event.
114    #[serde(skip_serializing)]
115    pub description: Option<String>,
116
117    /// The body of the event (for some event types).
118    #[serde(skip_serializing)]
119    pub body: Option<String>,
120
121    /// The relative path to the resource.
122    #[serde(skip_serializing)]
123    pub path: Option<String>,
124
125    /// When the event occurred.
126    #[serde(skip_serializing)]
127    pub created_at: Option<DateTime<Utc>>,
128}
129
130impl Event {
131    /// Lists events for a specific owner resource.
132    ///
133    /// This supports polymorphic paths for different resource types.
134    ///
135    /// # Arguments
136    ///
137    /// * `client` - The REST client
138    /// * `owner_id_name` - The name of the owner ID parameter (e.g., "order_id", "product_id")
139    /// * `owner_id` - The owner's ID
140    /// * `params` - Optional list parameters
141    ///
142    /// # Example
143    ///
144    /// ```rust,ignore
145    /// // Events for an order
146    /// let events = Event::all_for_owner(&client, "order_id", 450789469, None).await?;
147    ///
148    /// // Events for a product
149    /// let events = Event::all_for_owner(&client, "product_id", 632910392, None).await?;
150    /// ```
151    pub async fn all_for_owner<OwnerId: std::fmt::Display + Send>(
152        client: &RestClient,
153        owner_id_name: &str,
154        owner_id: OwnerId,
155        params: Option<EventListParams>,
156    ) -> Result<ResourceResponse<Vec<Self>>, ResourceError> {
157        let mut ids: HashMap<&str, String> = HashMap::new();
158        ids.insert(owner_id_name, owner_id.to_string());
159
160        let available_ids: Vec<&str> = ids.keys().copied().collect();
161        let path = get_path(Self::PATHS, ResourceOperation::All, &available_ids).ok_or(
162            ResourceError::PathResolutionFailed {
163                resource: Self::NAME,
164                operation: "all",
165            },
166        )?;
167
168        let url = build_path(path.template, &ids);
169
170        // Build query params
171        let query = params
172            .map(|p| {
173                let value = serde_json::to_value(&p).map_err(|e| {
174                    ResourceError::Http(crate::clients::HttpError::Response(
175                        crate::clients::HttpResponseError {
176                            code: 400,
177                            message: format!("Failed to serialize params: {e}"),
178                            error_reference: None,
179                        },
180                    ))
181                })?;
182
183                let mut query = HashMap::new();
184                if let serde_json::Value::Object(map) = value {
185                    for (key, val) in map {
186                        match val {
187                            serde_json::Value::String(s) => {
188                                query.insert(key, s);
189                            }
190                            serde_json::Value::Number(n) => {
191                                query.insert(key, n.to_string());
192                            }
193                            serde_json::Value::Bool(b) => {
194                                query.insert(key, b.to_string());
195                            }
196                            _ => {}
197                        }
198                    }
199                }
200                Ok::<_, ResourceError>(query)
201            })
202            .transpose()?
203            .filter(|q| !q.is_empty());
204
205        let response = client.get(&url, query).await?;
206
207        if !response.is_ok() {
208            return Err(ResourceError::from_http_response(
209                response.code,
210                &response.body,
211                Self::NAME,
212                None,
213                response.request_id(),
214            ));
215        }
216
217        let key = Self::PLURAL;
218        ResourceResponse::from_http_response(response, key)
219    }
220
221    /// Counts events for a specific owner resource.
222    ///
223    /// # Arguments
224    ///
225    /// * `client` - The REST client
226    /// * `owner_id_name` - The name of the owner ID parameter
227    /// * `owner_id` - The owner's ID
228    /// * `params` - Optional count parameters
229    pub async fn count_for_owner<OwnerId: std::fmt::Display + Send>(
230        client: &RestClient,
231        owner_id_name: &str,
232        owner_id: OwnerId,
233        params: Option<EventCountParams>,
234    ) -> Result<u64, ResourceError> {
235        let mut ids: HashMap<&str, String> = HashMap::new();
236        ids.insert(owner_id_name, owner_id.to_string());
237
238        let available_ids: Vec<&str> = ids.keys().copied().collect();
239        let path = get_path(Self::PATHS, ResourceOperation::Count, &available_ids).ok_or(
240            ResourceError::PathResolutionFailed {
241                resource: Self::NAME,
242                operation: "count",
243            },
244        )?;
245
246        let url = build_path(path.template, &ids);
247
248        // Build query params
249        let query = params
250            .map(|p| {
251                let value = serde_json::to_value(&p).map_err(|e| {
252                    ResourceError::Http(crate::clients::HttpError::Response(
253                        crate::clients::HttpResponseError {
254                            code: 400,
255                            message: format!("Failed to serialize params: {e}"),
256                            error_reference: None,
257                        },
258                    ))
259                })?;
260
261                let mut query = HashMap::new();
262                if let serde_json::Value::Object(map) = value {
263                    for (key, val) in map {
264                        match val {
265                            serde_json::Value::String(s) => {
266                                query.insert(key, s);
267                            }
268                            serde_json::Value::Number(n) => {
269                                query.insert(key, n.to_string());
270                            }
271                            serde_json::Value::Bool(b) => {
272                                query.insert(key, b.to_string());
273                            }
274                            _ => {}
275                        }
276                    }
277                }
278                Ok::<_, ResourceError>(query)
279            })
280            .transpose()?
281            .filter(|q| !q.is_empty());
282
283        let response = client.get(&url, query).await?;
284
285        if !response.is_ok() {
286            return Err(ResourceError::from_http_response(
287                response.code,
288                &response.body,
289                Self::NAME,
290                None,
291                response.request_id(),
292            ));
293        }
294
295        let count = response
296            .body
297            .get("count")
298            .and_then(serde_json::Value::as_u64)
299            .ok_or_else(|| {
300                ResourceError::Http(crate::clients::HttpError::Response(
301                    crate::clients::HttpResponseError {
302                        code: response.code,
303                        message: "Missing 'count' in response".to_string(),
304                        error_reference: response.request_id().map(ToString::to_string),
305                    },
306                ))
307            })?;
308
309        Ok(count)
310    }
311}
312
313impl RestResource for Event {
314    type Id = u64;
315    type FindParams = EventFindParams;
316    type AllParams = EventListParams;
317    type CountParams = EventCountParams;
318
319    const NAME: &'static str = "Event";
320    const PLURAL: &'static str = "events";
321
322    /// Paths for the Event resource.
323    ///
324    /// Supports both global paths and polymorphic owner paths.
325    /// Only GET operations - events are read-only.
326    const PATHS: &'static [ResourcePath] = &[
327        // Global paths
328        ResourcePath::new(
329            HttpMethod::Get,
330            ResourceOperation::Find,
331            &["id"],
332            "events/{id}",
333        ),
334        ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "events"),
335        ResourcePath::new(
336            HttpMethod::Get,
337            ResourceOperation::Count,
338            &[],
339            "events/count",
340        ),
341        // Polymorphic owner paths - orders
342        ResourcePath::new(
343            HttpMethod::Get,
344            ResourceOperation::All,
345            &["order_id"],
346            "orders/{order_id}/events",
347        ),
348        ResourcePath::new(
349            HttpMethod::Get,
350            ResourceOperation::Count,
351            &["order_id"],
352            "orders/{order_id}/events/count",
353        ),
354        // Polymorphic owner paths - products
355        ResourcePath::new(
356            HttpMethod::Get,
357            ResourceOperation::All,
358            &["product_id"],
359            "products/{product_id}/events",
360        ),
361        ResourcePath::new(
362            HttpMethod::Get,
363            ResourceOperation::Count,
364            &["product_id"],
365            "products/{product_id}/events/count",
366        ),
367    ];
368
369    fn get_id(&self) -> Option<Self::Id> {
370        self.id
371    }
372}
373
374impl ReadOnlyResource for Event {}
375
376/// Parameters for finding a single event.
377#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
378pub struct EventFindParams {
379    /// Comma-separated list of fields to include in the response.
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub fields: Option<String>,
382}
383
384/// Parameters for listing events.
385#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
386pub struct EventListParams {
387    /// Maximum number of results to return (default: 50, max: 250).
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub limit: Option<u32>,
390
391    /// Return events after this ID.
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub since_id: Option<u64>,
394
395    /// Show events created after this date.
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub created_at_min: Option<DateTime<Utc>>,
398
399    /// Show events created before this date.
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub created_at_max: Option<DateTime<Utc>>,
402
403    /// Filter events by subject type (e.g., "Product", "Order").
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub filter: Option<String>,
406
407    /// Filter events by verb (e.g., "create", "update", "destroy").
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub verb: Option<String>,
410
411    /// Comma-separated list of fields to include in the response.
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub fields: Option<String>,
414}
415
416/// Parameters for counting events.
417#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
418pub struct EventCountParams {
419    /// Show events created after this date.
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub created_at_min: Option<DateTime<Utc>>,
422
423    /// Show events created before this date.
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub created_at_max: Option<DateTime<Utc>>,
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431    use crate::rest::{get_path, ReadOnlyResource, ResourceOperation, RestResource};
432
433    #[test]
434    fn test_event_implements_read_only_resource() {
435        // This test verifies that Event implements ReadOnlyResource
436        fn assert_read_only<T: ReadOnlyResource>() {}
437        assert_read_only::<Event>();
438    }
439
440    #[test]
441    fn test_event_deserialization() {
442        let json = r#"{
443            "id": 677313116,
444            "subject_id": 921728736,
445            "subject_type": "Product",
446            "verb": "create",
447            "arguments": ["IPod Touch 8GB", "White"],
448            "message": "IPod Touch 8GB was created with colors White.",
449            "description": "Product created",
450            "body": null,
451            "path": "/admin/products/921728736",
452            "created_at": "2024-06-15T10:30:00Z"
453        }"#;
454
455        let event: Event = serde_json::from_str(json).unwrap();
456
457        assert_eq!(event.id, Some(677313116));
458        assert_eq!(event.subject_id, Some(921728736));
459        assert_eq!(event.subject_type, Some("Product".to_string()));
460        assert_eq!(event.verb, Some("create".to_string()));
461        assert!(event.arguments.is_some());
462        let args = event.arguments.unwrap();
463        assert_eq!(args.len(), 2);
464        assert_eq!(args[0], "IPod Touch 8GB");
465        assert_eq!(args[1], "White");
466        assert_eq!(
467            event.message,
468            Some("IPod Touch 8GB was created with colors White.".to_string())
469        );
470        assert_eq!(event.description, Some("Product created".to_string()));
471        assert!(event.body.is_none());
472        assert_eq!(
473            event.path,
474            Some("/admin/products/921728736".to_string())
475        );
476        assert!(event.created_at.is_some());
477    }
478
479    #[test]
480    fn test_event_read_only_paths() {
481        // Global paths
482        let find_path = get_path(Event::PATHS, ResourceOperation::Find, &["id"]);
483        assert!(find_path.is_some());
484        assert_eq!(find_path.unwrap().template, "events/{id}");
485
486        let all_path = get_path(Event::PATHS, ResourceOperation::All, &[]);
487        assert!(all_path.is_some());
488        assert_eq!(all_path.unwrap().template, "events");
489
490        let count_path = get_path(Event::PATHS, ResourceOperation::Count, &[]);
491        assert!(count_path.is_some());
492        assert_eq!(count_path.unwrap().template, "events/count");
493
494        // No create, update, or delete paths
495        let create_path = get_path(Event::PATHS, ResourceOperation::Create, &[]);
496        assert!(create_path.is_none());
497
498        let update_path = get_path(Event::PATHS, ResourceOperation::Update, &["id"]);
499        assert!(update_path.is_none());
500
501        let delete_path = get_path(Event::PATHS, ResourceOperation::Delete, &["id"]);
502        assert!(delete_path.is_none());
503    }
504
505    #[test]
506    fn test_event_polymorphic_owner_paths() {
507        // Order events path
508        let order_events = get_path(Event::PATHS, ResourceOperation::All, &["order_id"]);
509        assert!(order_events.is_some());
510        assert_eq!(
511            order_events.unwrap().template,
512            "orders/{order_id}/events"
513        );
514
515        // Order events count path
516        let order_count = get_path(Event::PATHS, ResourceOperation::Count, &["order_id"]);
517        assert!(order_count.is_some());
518        assert_eq!(
519            order_count.unwrap().template,
520            "orders/{order_id}/events/count"
521        );
522
523        // Product events path
524        let product_events = get_path(Event::PATHS, ResourceOperation::All, &["product_id"]);
525        assert!(product_events.is_some());
526        assert_eq!(
527            product_events.unwrap().template,
528            "products/{product_id}/events"
529        );
530
531        // Product events count path
532        let product_count = get_path(Event::PATHS, ResourceOperation::Count, &["product_id"]);
533        assert!(product_count.is_some());
534        assert_eq!(
535            product_count.unwrap().template,
536            "products/{product_id}/events/count"
537        );
538    }
539
540    #[test]
541    fn test_event_list_params() {
542        let params = EventListParams {
543            limit: Some(50),
544            filter: Some("Product".to_string()),
545            verb: Some("create".to_string()),
546            since_id: Some(100),
547            ..Default::default()
548        };
549
550        let json = serde_json::to_value(&params).unwrap();
551
552        assert_eq!(json["limit"], 50);
553        assert_eq!(json["filter"], "Product");
554        assert_eq!(json["verb"], "create");
555        assert_eq!(json["since_id"], 100);
556    }
557
558    #[test]
559    fn test_event_constants() {
560        assert_eq!(Event::NAME, "Event");
561        assert_eq!(Event::PLURAL, "events");
562    }
563
564    #[test]
565    fn test_event_get_id() {
566        let event_with_id = Event {
567            id: Some(677313116),
568            verb: Some("create".to_string()),
569            ..Default::default()
570        };
571        assert_eq!(event_with_id.get_id(), Some(677313116));
572
573        let event_without_id = Event::default();
574        assert_eq!(event_without_id.get_id(), None);
575    }
576}