1use crate::object::{EventId, EventType, IdempotencyToken, Payload};
10use serde::Serialize;
11use std::fmt::Debug;
12use time::OffsetDateTime;
13
14#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
25#[derive(Debug, Clone)]
26pub struct Event<PT> {
27 pub id: EventId,
30 pub idempotency_token: Option<IdempotencyToken>,
34 pub event_type: EventType,
36 #[cfg_attr(feature = "sqlx", sqlx(json))]
38 pub payload: Payload<PT>,
39 pub created_at: OffsetDateTime,
41 pub locked_until: OffsetDateTime,
45 pub status: EventStatus,
47}
48impl<PT> Event<PT>
49where
50 PT: Debug + Clone + Serialize,
51{
52 pub fn new(
64 event_type: EventType,
65 payload: Payload<PT>,
66 idempotency_token: Option<IdempotencyToken>,
67 ) -> Self {
68 Self {
69 id: EventId::default(),
70 idempotency_token,
71 event_type,
72 payload,
73 created_at: OffsetDateTime::now_utc(),
74 locked_until: OffsetDateTime::UNIX_EPOCH,
75 status: EventStatus::Pending,
76 }
77 }
78}
79
80#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
92#[cfg_attr(
93 feature = "sqlx",
94 sqlx(type_name = "status", rename_all = "PascalCase")
95)]
96#[derive(Debug, Clone, PartialEq)]
97pub enum EventStatus {
98 Pending,
102 Processing,
105 Sent,
108}
109
110#[cfg(test)]
111#[allow(clippy::unwrap_used)]
112mod tests {
113 use super::*;
114 use rstest::rstest;
115 use serde::{Deserialize, Serialize};
116
117 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
118 struct TestPayload {
119 value: String,
120 }
121
122 fn payload(v: &str) -> Payload<TestPayload> {
123 Payload::new(TestPayload { value: v.into() })
124 }
125
126 #[rstest]
127 fn event_new_sets_status_to_pending() {
128 let e = Event::new(EventType::new("t"), payload("p"), None);
129 assert_eq!(e.status, EventStatus::Pending);
130 }
131
132 #[rstest]
133 fn event_new_sets_locked_until_to_unix_epoch() {
134 let e = Event::new(EventType::new("t"), payload("p"), None);
135 assert_eq!(e.locked_until, OffsetDateTime::UNIX_EPOCH);
136 }
137
138 #[rstest]
139 fn event_new_sets_created_at_within_wall_clock_window() {
140 let before = OffsetDateTime::now_utc();
141 let e = Event::new(EventType::new("t"), payload("p"), None);
142 let after = OffsetDateTime::now_utc();
143 assert!(
144 e.created_at >= before && e.created_at <= after,
145 "created_at {} not in [{before}, {after}]",
146 e.created_at
147 );
148 }
149
150 #[rstest]
151 fn event_new_assigns_unique_ids_across_calls() {
152 let a = Event::new(EventType::new("t"), payload("p"), None);
153 let b = Event::new(EventType::new("t"), payload("p"), None);
154 assert_ne!(a.id, b.id);
155 }
156
157 #[rstest]
158 fn event_new_preserves_event_type_and_payload() {
159 let e = Event::new(EventType::new("order.created"), payload("hello"), None);
160 assert_eq!(e.event_type.as_str(), "order.created");
161 assert_eq!(e.payload.as_value().value, "hello");
162 }
163
164 #[rstest]
165 fn event_new_preserves_idempotency_token_some() {
166 let tok = IdempotencyToken::new("abc".into());
167 let e = Event::new(EventType::new("t"), payload("p"), Some(tok));
168 assert_eq!(
169 e.idempotency_token.as_ref().map(|t| t.as_str().to_owned()),
170 Some("abc".to_string())
171 );
172 }
173
174 #[rstest]
175 fn event_new_preserves_idempotency_token_none() {
176 let e = Event::new(EventType::new("t"), payload("p"), None);
177 assert!(e.idempotency_token.is_none());
178 }
179
180 #[rstest]
181 #[case(EventStatus::Pending, EventStatus::Pending, true)]
182 #[case(EventStatus::Processing, EventStatus::Processing, true)]
183 #[case(EventStatus::Sent, EventStatus::Sent, true)]
184 #[case(EventStatus::Pending, EventStatus::Processing, false)]
185 #[case(EventStatus::Processing, EventStatus::Sent, false)]
186 #[case(EventStatus::Pending, EventStatus::Sent, false)]
187 fn event_status_partial_eq(
188 #[case] a: EventStatus,
189 #[case] b: EventStatus,
190 #[case] expected: bool,
191 ) {
192 assert_eq!(a == b, expected);
193 }
194}