koprogo_api/domain/entities/
convocation_recipient.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub enum AttendanceStatus {
8 Pending,
10 WillAttend,
12 WillNotAttend,
14 Attended,
16 DidNotAttend,
18}
19
20impl AttendanceStatus {
21 pub fn to_db_string(&self) -> &'static str {
22 match self {
23 AttendanceStatus::Pending => "pending",
24 AttendanceStatus::WillAttend => "will_attend",
25 AttendanceStatus::WillNotAttend => "will_not_attend",
26 AttendanceStatus::Attended => "attended",
27 AttendanceStatus::DidNotAttend => "did_not_attend",
28 }
29 }
30
31 pub fn from_db_string(s: &str) -> Result<Self, String> {
32 match s {
33 "pending" => Ok(AttendanceStatus::Pending),
34 "will_attend" => Ok(AttendanceStatus::WillAttend),
35 "will_not_attend" => Ok(AttendanceStatus::WillNotAttend),
36 "attended" => Ok(AttendanceStatus::Attended),
37 "did_not_attend" => Ok(AttendanceStatus::DidNotAttend),
38 _ => Err(format!("Invalid attendance status: {}", s)),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ConvocationRecipient {
48 pub id: Uuid,
49 pub convocation_id: Uuid,
50 pub owner_id: Uuid,
51 pub email: String,
52
53 pub email_sent_at: Option<DateTime<Utc>>,
55 pub email_opened_at: Option<DateTime<Utc>>, pub email_failed: bool,
57 pub email_failure_reason: Option<String>,
58
59 pub reminder_sent_at: Option<DateTime<Utc>>,
61 pub reminder_opened_at: Option<DateTime<Utc>>,
62
63 pub attendance_status: AttendanceStatus,
65 pub attendance_updated_at: Option<DateTime<Utc>>,
66
67 pub proxy_owner_id: Option<Uuid>, pub created_at: DateTime<Utc>,
72 pub updated_at: DateTime<Utc>,
73}
74
75impl ConvocationRecipient {
76 pub fn new(convocation_id: Uuid, owner_id: Uuid, email: String) -> Result<Self, String> {
78 if email.is_empty() || !email.contains('@') {
80 return Err(format!("Invalid email address: {}", email));
81 }
82
83 let now = Utc::now();
84
85 Ok(Self {
86 id: Uuid::new_v4(),
87 convocation_id,
88 owner_id,
89 email,
90 email_sent_at: None,
91 email_opened_at: None,
92 email_failed: false,
93 email_failure_reason: None,
94 reminder_sent_at: None,
95 reminder_opened_at: None,
96 attendance_status: AttendanceStatus::Pending,
97 attendance_updated_at: None,
98 proxy_owner_id: None,
99 created_at: now,
100 updated_at: now,
101 })
102 }
103
104 pub fn mark_email_sent(&mut self) {
106 self.email_sent_at = Some(Utc::now());
107 self.updated_at = Utc::now();
108 }
109
110 pub fn mark_email_failed(&mut self, reason: String) {
112 self.email_failed = true;
113 self.email_failure_reason = Some(reason);
114 self.updated_at = Utc::now();
115 }
116
117 pub fn mark_email_opened(&mut self) -> Result<(), String> {
119 if self.email_sent_at.is_none() {
120 return Err("Cannot mark email as opened before it's sent".to_string());
121 }
122
123 if self.email_opened_at.is_some() {
124 return Ok(()); }
126
127 self.email_opened_at = Some(Utc::now());
128 self.updated_at = Utc::now();
129 Ok(())
130 }
131
132 pub fn mark_reminder_sent(&mut self) -> Result<(), String> {
134 if self.email_sent_at.is_none() {
135 return Err("Cannot send reminder before initial email".to_string());
136 }
137
138 self.reminder_sent_at = Some(Utc::now());
139 self.updated_at = Utc::now();
140 Ok(())
141 }
142
143 pub fn mark_reminder_opened(&mut self) -> Result<(), String> {
145 if self.reminder_sent_at.is_none() {
146 return Err("Cannot mark reminder as opened before it's sent".to_string());
147 }
148
149 self.reminder_opened_at = Some(Utc::now());
150 self.updated_at = Utc::now();
151 Ok(())
152 }
153
154 pub fn update_attendance_status(&mut self, status: AttendanceStatus) -> Result<(), String> {
156 if matches!(
158 self.attendance_status,
159 AttendanceStatus::Attended | AttendanceStatus::DidNotAttend
160 ) {
161 return Err(format!(
162 "Cannot change attendance after meeting. Current status: {:?}",
163 self.attendance_status
164 ));
165 }
166
167 self.attendance_status = status;
168 self.attendance_updated_at = Some(Utc::now());
169 self.updated_at = Utc::now();
170 Ok(())
171 }
172
173 pub fn set_proxy(&mut self, proxy_owner_id: Uuid) -> Result<(), String> {
175 if proxy_owner_id == self.owner_id {
176 return Err("Cannot delegate to self".to_string());
177 }
178
179 self.proxy_owner_id = Some(proxy_owner_id);
180 self.updated_at = Utc::now();
181 Ok(())
182 }
183
184 pub fn remove_proxy(&mut self) {
186 self.proxy_owner_id = None;
187 self.updated_at = Utc::now();
188 }
189
190 pub fn has_opened_email(&self) -> bool {
192 self.email_opened_at.is_some()
193 }
194
195 pub fn has_opened_reminder(&self) -> bool {
197 self.reminder_opened_at.is_some()
198 }
199
200 pub fn needs_reminder(&self) -> bool {
202 self.email_sent_at.is_some()
203 && self.email_opened_at.is_none()
204 && self.reminder_sent_at.is_none()
205 && !self.email_failed
206 }
207
208 pub fn has_confirmed_attendance(&self) -> bool {
210 matches!(
211 self.attendance_status,
212 AttendanceStatus::WillAttend | AttendanceStatus::WillNotAttend
213 )
214 }
215
216 pub fn days_since_email_sent(&self) -> Option<i64> {
218 self.email_sent_at.map(|sent_at| {
219 let now = Utc::now();
220 now.signed_duration_since(sent_at).num_days()
221 })
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_create_recipient_success() {
231 let conv_id = Uuid::new_v4();
232 let owner_id = Uuid::new_v4();
233
234 let recipient =
235 ConvocationRecipient::new(conv_id, owner_id, "owner@example.com".to_string());
236
237 assert!(recipient.is_ok());
238 let r = recipient.unwrap();
239 assert_eq!(r.convocation_id, conv_id);
240 assert_eq!(r.owner_id, owner_id);
241 assert_eq!(r.email, "owner@example.com");
242 assert_eq!(r.attendance_status, AttendanceStatus::Pending);
243 assert!(!r.email_failed);
244 }
245
246 #[test]
247 fn test_create_recipient_invalid_email() {
248 let result =
249 ConvocationRecipient::new(Uuid::new_v4(), Uuid::new_v4(), "invalid-email".to_string());
250
251 assert!(result.is_err());
252 assert!(result.unwrap_err().contains("Invalid email"));
253 }
254
255 #[test]
256 fn test_mark_email_opened() {
257 let mut recipient = ConvocationRecipient::new(
258 Uuid::new_v4(),
259 Uuid::new_v4(),
260 "owner@example.com".to_string(),
261 )
262 .unwrap();
263
264 assert!(recipient.mark_email_opened().is_err());
266
267 recipient.mark_email_sent();
269 assert!(recipient.email_sent_at.is_some());
270
271 assert!(recipient.mark_email_opened().is_ok());
273 assert!(recipient.has_opened_email());
274
275 assert!(recipient.mark_email_opened().is_ok());
277 }
278
279 #[test]
280 fn test_mark_email_failed() {
281 let mut recipient = ConvocationRecipient::new(
282 Uuid::new_v4(),
283 Uuid::new_v4(),
284 "owner@example.com".to_string(),
285 )
286 .unwrap();
287
288 recipient.mark_email_failed("Invalid email address".to_string());
289
290 assert!(recipient.email_failed);
291 assert_eq!(
292 recipient.email_failure_reason,
293 Some("Invalid email address".to_string())
294 );
295 }
296
297 #[test]
298 fn test_needs_reminder() {
299 let mut recipient = ConvocationRecipient::new(
300 Uuid::new_v4(),
301 Uuid::new_v4(),
302 "owner@example.com".to_string(),
303 )
304 .unwrap();
305
306 assert!(!recipient.needs_reminder());
308
309 recipient.mark_email_sent();
311 assert!(recipient.needs_reminder());
312
313 recipient.mark_email_opened().unwrap();
315 assert!(!recipient.needs_reminder());
316 }
317
318 #[test]
319 fn test_update_attendance_status() {
320 let mut recipient = ConvocationRecipient::new(
321 Uuid::new_v4(),
322 Uuid::new_v4(),
323 "owner@example.com".to_string(),
324 )
325 .unwrap();
326
327 assert!(recipient
329 .update_attendance_status(AttendanceStatus::WillAttend)
330 .is_ok());
331 assert_eq!(recipient.attendance_status, AttendanceStatus::WillAttend);
332 assert!(recipient.has_confirmed_attendance());
333
334 assert!(recipient
336 .update_attendance_status(AttendanceStatus::WillNotAttend)
337 .is_ok());
338 assert_eq!(recipient.attendance_status, AttendanceStatus::WillNotAttend);
339
340 assert!(recipient
342 .update_attendance_status(AttendanceStatus::Attended)
343 .is_ok());
344
345 assert!(recipient
347 .update_attendance_status(AttendanceStatus::DidNotAttend)
348 .is_err());
349 }
350
351 #[test]
352 fn test_set_proxy() {
353 let mut recipient = ConvocationRecipient::new(
354 Uuid::new_v4(),
355 Uuid::new_v4(),
356 "owner@example.com".to_string(),
357 )
358 .unwrap();
359
360 let proxy_owner = Uuid::new_v4();
361
362 assert!(recipient.set_proxy(proxy_owner).is_ok());
364 assert_eq!(recipient.proxy_owner_id, Some(proxy_owner));
365
366 assert!(recipient.set_proxy(recipient.owner_id).is_err());
368
369 recipient.remove_proxy();
371 assert_eq!(recipient.proxy_owner_id, None);
372 }
373
374 #[test]
375 fn test_mark_reminder_sent() {
376 let mut recipient = ConvocationRecipient::new(
377 Uuid::new_v4(),
378 Uuid::new_v4(),
379 "owner@example.com".to_string(),
380 )
381 .unwrap();
382
383 assert!(recipient.mark_reminder_sent().is_err());
385
386 recipient.mark_email_sent();
388
389 assert!(recipient.mark_reminder_sent().is_ok());
391 assert!(recipient.reminder_sent_at.is_some());
392 }
393}