koprogo_api/domain/copropriete/
convocation_recipient.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
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
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum QualiteDuMandataire {
87 Coproprietaire,
89 Syndic,
91}
92
93impl ConvocationRecipient {
94 pub fn new(convocation_id: Uuid, owner_id: Uuid, email: String) -> Result<Self, String> {
96 if email.is_empty() || !email.contains('@') {
98 return Err(format!("Invalid email address: {}", email));
99 }
100
101 let now = Utc::now();
102
103 Ok(Self {
104 id: Uuid::new_v4(),
105 convocation_id,
106 owner_id,
107 email,
108 email_sent_at: None,
109 email_opened_at: None,
110 email_failed: false,
111 email_failure_reason: None,
112 reminder_sent_at: None,
113 reminder_opened_at: None,
114 attendance_status: AttendanceStatus::Pending,
115 attendance_updated_at: None,
116 proxy_owner_id: None,
117 created_at: now,
118 updated_at: now,
119 })
120 }
121
122 pub fn mark_email_sent(&mut self) {
124 self.email_sent_at = Some(Utc::now());
125 self.updated_at = Utc::now();
126 }
127
128 pub fn mark_email_failed(&mut self, reason: String) {
130 self.email_failed = true;
131 self.email_failure_reason = Some(reason);
132 self.updated_at = Utc::now();
133 }
134
135 pub fn mark_email_opened(&mut self) -> Result<(), String> {
137 if self.email_sent_at.is_none() {
138 return Err("Cannot mark email as opened before it's sent".to_string());
139 }
140
141 if self.email_opened_at.is_some() {
142 return Ok(()); }
144
145 self.email_opened_at = Some(Utc::now());
146 self.updated_at = Utc::now();
147 Ok(())
148 }
149
150 pub fn mark_reminder_sent(&mut self) -> Result<(), String> {
152 if self.email_sent_at.is_none() {
153 return Err("Cannot send reminder before initial email".to_string());
154 }
155
156 self.reminder_sent_at = Some(Utc::now());
157 self.updated_at = Utc::now();
158 Ok(())
159 }
160
161 pub fn mark_reminder_opened(&mut self) -> Result<(), String> {
163 if self.reminder_sent_at.is_none() {
164 return Err("Cannot mark reminder as opened before it's sent".to_string());
165 }
166
167 self.reminder_opened_at = Some(Utc::now());
168 self.updated_at = Utc::now();
169 Ok(())
170 }
171
172 pub fn update_attendance_status(&mut self, status: AttendanceStatus) -> Result<(), String> {
174 if matches!(
176 self.attendance_status,
177 AttendanceStatus::Attended | AttendanceStatus::DidNotAttend
178 ) {
179 return Err(format!(
180 "Cannot change attendance after meeting. Current status: {:?}",
181 self.attendance_status
182 ));
183 }
184
185 self.attendance_status = status;
186 self.attendance_updated_at = Some(Utc::now());
187 self.updated_at = Utc::now();
188 Ok(())
189 }
190
191 pub fn set_proxy(
218 &mut self,
219 proxy_owner_id: Uuid,
220 qualite: QualiteDuMandataire,
221 ) -> Result<(), String> {
222 if proxy_owner_id == self.owner_id {
223 return Err("Cannot delegate to self".to_string());
224 }
225
226 if qualite == QualiteDuMandataire::Syndic {
227 return Err(
228 "Art. 3.87 § 7 : le syndic ne peut intervenir comme mandataire d'un copropriétaire"
229 .to_string(),
230 );
231 }
232
233 self.proxy_owner_id = Some(proxy_owner_id);
234 self.updated_at = Utc::now();
235 Ok(())
236 }
237
238 pub fn remove_proxy(&mut self) {
240 self.proxy_owner_id = None;
241 self.updated_at = Utc::now();
242 }
243
244 pub fn has_opened_email(&self) -> bool {
246 self.email_opened_at.is_some()
247 }
248
249 pub fn has_opened_reminder(&self) -> bool {
251 self.reminder_opened_at.is_some()
252 }
253
254 pub fn needs_reminder(&self) -> bool {
256 self.email_sent_at.is_some()
257 && self.email_opened_at.is_none()
258 && self.reminder_sent_at.is_none()
259 && !self.email_failed
260 }
261
262 pub fn has_confirmed_attendance(&self) -> bool {
264 matches!(
265 self.attendance_status,
266 AttendanceStatus::WillAttend | AttendanceStatus::WillNotAttend
267 )
268 }
269
270 pub fn days_since_email_sent(&self) -> Option<i64> {
272 self.email_sent_at.map(|sent_at| {
273 let now = Utc::now();
274 now.signed_duration_since(sent_at).num_days()
275 })
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_create_recipient_success() {
285 let conv_id = Uuid::new_v4();
286 let owner_id = Uuid::new_v4();
287
288 let recipient =
289 ConvocationRecipient::new(conv_id, owner_id, "owner@example.com".to_string());
290
291 assert!(recipient.is_ok());
292 let r = recipient.unwrap();
293 assert_eq!(r.convocation_id, conv_id);
294 assert_eq!(r.owner_id, owner_id);
295 assert_eq!(r.email, "owner@example.com");
296 assert_eq!(r.attendance_status, AttendanceStatus::Pending);
297 assert!(!r.email_failed);
298 }
299
300 #[test]
301 fn test_create_recipient_invalid_email() {
302 let result =
303 ConvocationRecipient::new(Uuid::new_v4(), Uuid::new_v4(), "invalid-email".to_string());
304
305 assert!(result.is_err());
306 assert!(result.unwrap_err().contains("Invalid email"));
307 }
308
309 #[test]
310 fn test_mark_email_opened() {
311 let mut recipient = ConvocationRecipient::new(
312 Uuid::new_v4(),
313 Uuid::new_v4(),
314 "owner@example.com".to_string(),
315 )
316 .unwrap();
317
318 assert!(recipient.mark_email_opened().is_err());
320
321 recipient.mark_email_sent();
323 assert!(recipient.email_sent_at.is_some());
324
325 assert!(recipient.mark_email_opened().is_ok());
327 assert!(recipient.has_opened_email());
328
329 assert!(recipient.mark_email_opened().is_ok());
331 }
332
333 #[test]
334 fn test_mark_email_failed() {
335 let mut recipient = ConvocationRecipient::new(
336 Uuid::new_v4(),
337 Uuid::new_v4(),
338 "owner@example.com".to_string(),
339 )
340 .unwrap();
341
342 recipient.mark_email_failed("Invalid email address".to_string());
343
344 assert!(recipient.email_failed);
345 assert_eq!(
346 recipient.email_failure_reason,
347 Some("Invalid email address".to_string())
348 );
349 }
350
351 #[test]
352 fn test_needs_reminder() {
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 assert!(!recipient.needs_reminder());
362
363 recipient.mark_email_sent();
365 assert!(recipient.needs_reminder());
366
367 recipient.mark_email_opened().unwrap();
369 assert!(!recipient.needs_reminder());
370 }
371
372 #[test]
373 fn test_update_attendance_status() {
374 let mut recipient = ConvocationRecipient::new(
375 Uuid::new_v4(),
376 Uuid::new_v4(),
377 "owner@example.com".to_string(),
378 )
379 .unwrap();
380
381 assert!(recipient
383 .update_attendance_status(AttendanceStatus::WillAttend)
384 .is_ok());
385 assert_eq!(recipient.attendance_status, AttendanceStatus::WillAttend);
386 assert!(recipient.has_confirmed_attendance());
387
388 assert!(recipient
390 .update_attendance_status(AttendanceStatus::WillNotAttend)
391 .is_ok());
392 assert_eq!(recipient.attendance_status, AttendanceStatus::WillNotAttend);
393
394 assert!(recipient
396 .update_attendance_status(AttendanceStatus::Attended)
397 .is_ok());
398
399 assert!(recipient
401 .update_attendance_status(AttendanceStatus::DidNotAttend)
402 .is_err());
403 }
404
405 #[test]
406 fn test_set_proxy() {
407 let mut recipient = ConvocationRecipient::new(
408 Uuid::new_v4(),
409 Uuid::new_v4(),
410 "owner@example.com".to_string(),
411 )
412 .unwrap();
413
414 let proxy_owner = Uuid::new_v4();
415
416 assert!(recipient
418 .set_proxy(proxy_owner, QualiteDuMandataire::Coproprietaire)
419 .is_ok());
420 assert_eq!(recipient.proxy_owner_id, Some(proxy_owner));
421
422 assert!(recipient
424 .set_proxy(recipient.owner_id, QualiteDuMandataire::Coproprietaire)
425 .is_err());
426
427 recipient.remove_proxy();
429 assert_eq!(recipient.proxy_owner_id, None);
430 }
431
432 #[test]
438 fn le_syndic_ne_peut_pas_recevoir_de_procuration() {
439 let mut destinataire = ConvocationRecipient::new(
440 Uuid::new_v4(),
441 Uuid::new_v4(),
442 "coproprietaire@example.be".to_string(),
443 )
444 .unwrap();
445
446 let syndic = Uuid::new_v4();
447 let erreur = destinataire
448 .set_proxy(syndic, QualiteDuMandataire::Syndic)
449 .expect_err("le mandat au syndic doit être refusé");
450
451 assert!(
452 erreur.contains("3.87"),
453 "le message doit citer l'article qui fonde le refus, reçu : {erreur}"
454 );
455 assert_eq!(
456 destinataire.proxy_owner_id, None,
457 "un mandat refusé ne doit rien laisser derrière lui"
458 );
459 }
460
461 #[test]
469 fn le_syndic_coproprietaire_reste_destinataire_a_part_entiere() {
470 let syndic_coproprietaire = Uuid::new_v4();
471 let mut destinataire = ConvocationRecipient::new(
472 Uuid::new_v4(),
473 syndic_coproprietaire,
474 "syndic@example.be".to_string(),
475 )
476 .unwrap();
477
478 assert!(destinataire
479 .update_attendance_status(AttendanceStatus::WillAttend)
480 .is_ok());
481
482 assert!(destinataire
485 .set_proxy(Uuid::new_v4(), QualiteDuMandataire::Coproprietaire)
486 .is_ok());
487 }
488
489 #[test]
490 fn test_mark_reminder_sent() {
491 let mut recipient = ConvocationRecipient::new(
492 Uuid::new_v4(),
493 Uuid::new_v4(),
494 "owner@example.com".to_string(),
495 )
496 .unwrap();
497
498 assert!(recipient.mark_reminder_sent().is_err());
500
501 recipient.mark_email_sent();
503
504 assert!(recipient.mark_reminder_sent().is_ok());
506 assert!(recipient.reminder_sent_at.is_some());
507 }
508}