koprogo_api/application/dto/
convocation_recipient_dto.rs

1use crate::domain::entities::{AttendanceStatus, ConvocationRecipient};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6#[derive(Debug, Serialize, Deserialize, Clone)]
7pub struct ConvocationRecipientResponse {
8    pub id: Uuid,
9    pub convocation_id: Uuid,
10    pub owner_id: Uuid,
11    pub email: String,
12
13    // Email tracking
14    pub email_sent_at: Option<DateTime<Utc>>,
15    pub email_opened_at: Option<DateTime<Utc>>,
16    pub email_failed: bool,
17    pub email_failure_reason: Option<String>,
18
19    // Reminder tracking
20    pub reminder_sent_at: Option<DateTime<Utc>>,
21    pub reminder_opened_at: Option<DateTime<Utc>>,
22
23    // Attendance tracking
24    pub attendance_status: AttendanceStatus,
25    pub attendance_updated_at: Option<DateTime<Utc>>,
26
27    // Proxy delegation
28    pub proxy_owner_id: Option<Uuid>,
29
30    // Computed fields
31    pub has_opened_email: bool,
32    pub has_opened_reminder: bool,
33    pub needs_reminder: bool,
34    pub has_confirmed_attendance: bool,
35
36    // Audit
37    pub created_at: DateTime<Utc>,
38    pub updated_at: DateTime<Utc>,
39}
40
41impl From<ConvocationRecipient> for ConvocationRecipientResponse {
42    fn from(recipient: ConvocationRecipient) -> Self {
43        let has_opened_email = recipient.has_opened_email();
44        let has_opened_reminder = recipient.has_opened_reminder();
45        let needs_reminder = recipient.needs_reminder();
46        let has_confirmed_attendance = recipient.has_confirmed_attendance();
47
48        Self {
49            id: recipient.id,
50            convocation_id: recipient.convocation_id,
51            owner_id: recipient.owner_id,
52            email: recipient.email,
53            email_sent_at: recipient.email_sent_at,
54            email_opened_at: recipient.email_opened_at,
55            email_failed: recipient.email_failed,
56            email_failure_reason: recipient.email_failure_reason,
57            reminder_sent_at: recipient.reminder_sent_at,
58            reminder_opened_at: recipient.reminder_opened_at,
59            attendance_status: recipient.attendance_status,
60            attendance_updated_at: recipient.attendance_updated_at,
61            proxy_owner_id: recipient.proxy_owner_id,
62            has_opened_email,
63            has_opened_reminder,
64            needs_reminder,
65            has_confirmed_attendance,
66            created_at: recipient.created_at,
67            updated_at: recipient.updated_at,
68        }
69    }
70}
71
72#[derive(Debug, Deserialize)]
73pub struct UpdateAttendanceRequest {
74    pub attendance_status: AttendanceStatus,
75}
76
77#[derive(Debug, Deserialize)]
78pub struct SetProxyRequest {
79    pub proxy_owner_id: Uuid,
80}
81
82#[derive(Debug, Serialize)]
83pub struct RecipientSummaryResponse {
84    pub id: Uuid,
85    pub owner_id: Uuid,
86    pub email: String,
87    pub email_opened: bool,
88    pub attendance_status: AttendanceStatus,
89}
90
91impl From<ConvocationRecipient> for RecipientSummaryResponse {
92    fn from(recipient: ConvocationRecipient) -> Self {
93        Self {
94            id: recipient.id,
95            owner_id: recipient.owner_id,
96            email: recipient.email.clone(),
97            email_opened: recipient.has_opened_email(),
98            attendance_status: recipient.attendance_status.clone(),
99        }
100    }
101}
102
103#[derive(Debug, Serialize, Deserialize, Clone)]
104pub struct RecipientTrackingSummaryResponse {
105    pub total_count: i64,
106    pub opened_count: i64,
107    pub will_attend_count: i64,
108    pub will_not_attend_count: i64,
109    pub attended_count: i64,
110    pub did_not_attend_count: i64,
111    pub pending_count: i64,
112    pub failed_email_count: i64,
113
114    // Computed percentages
115    pub opening_rate: f64,
116    pub attendance_rate: f64,
117}
118
119impl RecipientTrackingSummaryResponse {
120    pub fn new(
121        total_count: i64,
122        opened_count: i64,
123        will_attend_count: i64,
124        will_not_attend_count: i64,
125        attended_count: i64,
126        did_not_attend_count: i64,
127        pending_count: i64,
128        failed_email_count: i64,
129    ) -> Self {
130        let opening_rate = if total_count > 0 {
131            (opened_count as f64 / total_count as f64) * 100.0
132        } else {
133            0.0
134        };
135
136        let attendance_rate = if total_count > 0 {
137            (will_attend_count as f64 / total_count as f64) * 100.0
138        } else {
139            0.0
140        };
141
142        Self {
143            total_count,
144            opened_count,
145            will_attend_count,
146            will_not_attend_count,
147            attended_count,
148            did_not_attend_count,
149            pending_count,
150            failed_email_count,
151            opening_rate,
152            attendance_rate,
153        }
154    }
155}