koprogo_api/application/use_cases/
convocation_use_cases.rs1use crate::application::dto::{
2 ConvocationRecipientResponse, ConvocationResponse, CreateConvocationRequest,
3 RecipientTrackingSummaryResponse, ScheduleConvocationRequest, SendConvocationRequest,
4};
5use crate::application::ports::{
6 BuildingRepository, ConvocationRecipientRepository, ConvocationRepository, MeetingRepository,
7 OwnerRepository,
8};
9use crate::domain::entities::{AttendanceStatus, Convocation, ConvocationRecipient};
10use crate::domain::services::ConvocationExporter;
11use chrono::Utc;
12use std::sync::Arc;
13use uuid::Uuid;
14
15pub struct ConvocationUseCases {
16 convocation_repository: Arc<dyn ConvocationRepository>,
17 recipient_repository: Arc<dyn ConvocationRecipientRepository>,
18 owner_repository: Arc<dyn OwnerRepository>,
19 building_repository: Arc<dyn BuildingRepository>,
20 meeting_repository: Arc<dyn MeetingRepository>,
21}
22
23impl ConvocationUseCases {
24 pub fn new(
25 convocation_repository: Arc<dyn ConvocationRepository>,
26 recipient_repository: Arc<dyn ConvocationRecipientRepository>,
27 owner_repository: Arc<dyn OwnerRepository>,
28 building_repository: Arc<dyn BuildingRepository>,
29 meeting_repository: Arc<dyn MeetingRepository>,
30 ) -> Self {
31 Self {
32 convocation_repository,
33 recipient_repository,
34 owner_repository,
35 building_repository,
36 meeting_repository,
37 }
38 }
39
40 pub async fn create_convocation(
42 &self,
43 organization_id: Uuid,
44 request: CreateConvocationRequest,
45 created_by: Uuid,
46 ) -> Result<ConvocationResponse, String> {
47 let convocation = Convocation::new(
49 organization_id,
50 request.building_id,
51 request.meeting_id,
52 request.meeting_type,
53 request.meeting_date,
54 request.language,
55 created_by,
56 )?;
57
58 let created = self.convocation_repository.create(&convocation).await?;
59
60 Ok(ConvocationResponse::from(created))
61 }
62
63 pub async fn get_convocation(&self, id: Uuid) -> Result<ConvocationResponse, String> {
65 let convocation = self
66 .convocation_repository
67 .find_by_id(id)
68 .await?
69 .ok_or_else(|| format!("Convocation not found: {}", id))?;
70
71 Ok(ConvocationResponse::from(convocation))
72 }
73
74 pub async fn get_convocation_by_meeting(
76 &self,
77 meeting_id: Uuid,
78 ) -> Result<Option<ConvocationResponse>, String> {
79 let convocation = self
80 .convocation_repository
81 .find_by_meeting_id(meeting_id)
82 .await?;
83
84 Ok(convocation.map(ConvocationResponse::from))
85 }
86
87 pub async fn list_building_convocations(
89 &self,
90 building_id: Uuid,
91 ) -> Result<Vec<ConvocationResponse>, String> {
92 let convocations = self
93 .convocation_repository
94 .find_by_building(building_id)
95 .await?;
96
97 Ok(convocations
98 .into_iter()
99 .map(ConvocationResponse::from)
100 .collect())
101 }
102
103 pub async fn list_organization_convocations(
105 &self,
106 organization_id: Uuid,
107 ) -> Result<Vec<ConvocationResponse>, String> {
108 let convocations = self
109 .convocation_repository
110 .find_by_organization(organization_id)
111 .await?;
112
113 Ok(convocations
114 .into_iter()
115 .map(ConvocationResponse::from)
116 .collect())
117 }
118
119 pub async fn schedule_convocation(
121 &self,
122 id: Uuid,
123 request: ScheduleConvocationRequest,
124 ) -> Result<ConvocationResponse, String> {
125 let mut convocation = self
126 .convocation_repository
127 .find_by_id(id)
128 .await?
129 .ok_or_else(|| format!("Convocation not found: {}", id))?;
130
131 convocation.schedule(request.send_date)?;
132
133 let updated = self.convocation_repository.update(&convocation).await?;
134
135 Ok(ConvocationResponse::from(updated))
136 }
137
138 pub async fn send_convocation(
141 &self,
142 id: Uuid,
143 request: SendConvocationRequest,
144 ) -> Result<ConvocationResponse, String> {
145 let mut convocation = self
146 .convocation_repository
147 .find_by_id(id)
148 .await?
149 .ok_or_else(|| format!("Convocation not found: {}", id))?;
150
151 let building = self
153 .building_repository
154 .find_by_id(convocation.building_id)
155 .await?
156 .ok_or_else(|| format!("Building not found: {}", convocation.building_id))?;
157
158 let meeting = self
160 .meeting_repository
161 .find_by_id(convocation.meeting_id)
162 .await?
163 .ok_or_else(|| format!("Meeting not found: {}", convocation.meeting_id))?;
164
165 let pdf_bytes = ConvocationExporter::export_to_pdf(&building, &meeting, &convocation)
167 .map_err(|e| format!("Failed to generate PDF: {}", e))?;
168
169 let pdf_file_path = format!("/uploads/convocations/conv-{}.pdf", id);
171 ConvocationExporter::save_to_file(&pdf_bytes, &pdf_file_path)
172 .map_err(|e| format!("Failed to save PDF: {}", e))?;
173
174 let mut recipients = Vec::new();
176 for owner_id in &request.recipient_owner_ids {
177 let owner = self
178 .owner_repository
179 .find_by_id(*owner_id)
180 .await?
181 .ok_or_else(|| format!("Owner not found: {}", owner_id))?;
182
183 let recipient = ConvocationRecipient::new(id, *owner_id, owner.email)?;
184 recipients.push(recipient);
185 }
186
187 let created_recipients = self.recipient_repository.create_many(&recipients).await?;
189
190 convocation.mark_sent(pdf_file_path, created_recipients.len() as i32)?;
192
193 let updated = self.convocation_repository.update(&convocation).await?;
194
195 Ok(ConvocationResponse::from(updated))
196 }
197
198 pub async fn mark_recipient_email_sent(
200 &self,
201 recipient_id: Uuid,
202 ) -> Result<ConvocationRecipientResponse, String> {
203 let mut recipient = self
204 .recipient_repository
205 .find_by_id(recipient_id)
206 .await?
207 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
208
209 recipient.mark_email_sent();
210
211 let updated = self.recipient_repository.update(&recipient).await?;
212
213 Ok(ConvocationRecipientResponse::from(updated))
214 }
215
216 pub async fn mark_recipient_email_opened(
218 &self,
219 recipient_id: Uuid,
220 ) -> Result<ConvocationRecipientResponse, String> {
221 let mut recipient = self
222 .recipient_repository
223 .find_by_id(recipient_id)
224 .await?
225 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
226
227 recipient.mark_email_opened()?;
228
229 let updated = self.recipient_repository.update(&recipient).await?;
230
231 self.update_convocation_tracking(recipient.convocation_id)
233 .await?;
234
235 Ok(ConvocationRecipientResponse::from(updated))
236 }
237
238 pub async fn update_recipient_attendance(
240 &self,
241 recipient_id: Uuid,
242 status: AttendanceStatus,
243 ) -> Result<ConvocationRecipientResponse, String> {
244 let mut recipient = self
245 .recipient_repository
246 .find_by_id(recipient_id)
247 .await?
248 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
249
250 recipient.update_attendance_status(status)?;
251
252 let updated = self.recipient_repository.update(&recipient).await?;
253
254 self.update_convocation_tracking(recipient.convocation_id)
256 .await?;
257
258 Ok(ConvocationRecipientResponse::from(updated))
259 }
260
261 pub async fn set_recipient_proxy(
263 &self,
264 recipient_id: Uuid,
265 proxy_owner_id: Uuid,
266 ) -> Result<ConvocationRecipientResponse, String> {
267 let mut recipient = self
268 .recipient_repository
269 .find_by_id(recipient_id)
270 .await?
271 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
272
273 recipient.set_proxy(proxy_owner_id)?;
274
275 let updated = self.recipient_repository.update(&recipient).await?;
276
277 Ok(ConvocationRecipientResponse::from(updated))
278 }
279
280 pub async fn send_reminders(
283 &self,
284 convocation_id: Uuid,
285 ) -> Result<Vec<ConvocationRecipientResponse>, String> {
286 let recipients = self
288 .recipient_repository
289 .find_needing_reminder(convocation_id)
290 .await?;
291
292 let mut updated_recipients = Vec::new();
293
294 for mut recipient in recipients {
295 recipient.mark_reminder_sent()?;
296 let updated = self.recipient_repository.update(&recipient).await?;
297 updated_recipients.push(ConvocationRecipientResponse::from(updated));
298 }
299
300 if !updated_recipients.is_empty() {
302 let mut convocation = self
303 .convocation_repository
304 .find_by_id(convocation_id)
305 .await?
306 .ok_or_else(|| format!("Convocation not found: {}", convocation_id))?;
307
308 convocation.mark_reminder_sent()?;
309 self.convocation_repository.update(&convocation).await?;
310 }
311
312 Ok(updated_recipients)
313 }
314
315 pub async fn get_tracking_summary(
317 &self,
318 convocation_id: Uuid,
319 ) -> Result<RecipientTrackingSummaryResponse, String> {
320 let summary = self
321 .recipient_repository
322 .get_tracking_summary(convocation_id)
323 .await?;
324
325 Ok(RecipientTrackingSummaryResponse::new(
326 summary.total_count,
327 summary.opened_count,
328 summary.will_attend_count,
329 summary.will_not_attend_count,
330 summary.attended_count,
331 summary.did_not_attend_count,
332 summary.pending_count,
333 summary.failed_email_count,
334 ))
335 }
336
337 pub async fn list_convocation_recipients(
339 &self,
340 convocation_id: Uuid,
341 ) -> Result<Vec<ConvocationRecipientResponse>, String> {
342 let recipients = self
343 .recipient_repository
344 .find_by_convocation(convocation_id)
345 .await?;
346
347 Ok(recipients
348 .into_iter()
349 .map(ConvocationRecipientResponse::from)
350 .collect())
351 }
352
353 pub async fn cancel_convocation(&self, id: Uuid) -> Result<ConvocationResponse, String> {
355 let mut convocation = self
356 .convocation_repository
357 .find_by_id(id)
358 .await?
359 .ok_or_else(|| format!("Convocation not found: {}", id))?;
360
361 convocation.cancel()?;
362
363 let updated = self.convocation_repository.update(&convocation).await?;
364
365 Ok(ConvocationResponse::from(updated))
366 }
367
368 pub async fn delete_convocation(&self, id: Uuid) -> Result<bool, String> {
370 self.convocation_repository.delete(id).await
371 }
372
373 pub async fn process_scheduled_convocations(&self) -> Result<Vec<ConvocationResponse>, String> {
376 let now = Utc::now();
377 let scheduled = self
378 .convocation_repository
379 .find_pending_scheduled(now)
380 .await?;
381
382 let mut sent = Vec::new();
383
384 for convocation in scheduled {
385 sent.push(ConvocationResponse::from(convocation));
388 }
389
390 Ok(sent)
391 }
392
393 pub async fn process_reminder_sending(&self) -> Result<Vec<ConvocationResponse>, String> {
396 let now = Utc::now();
397 let needing_reminder = self
398 .convocation_repository
399 .find_needing_reminder(now)
400 .await?;
401
402 let mut processed = Vec::new();
403
404 for convocation in needing_reminder {
405 self.send_reminders(convocation.id).await?;
407 processed.push(ConvocationResponse::from(convocation));
408 }
409
410 Ok(processed)
411 }
412
413 async fn update_convocation_tracking(&self, convocation_id: Uuid) -> Result<(), String> {
415 let summary = self
416 .recipient_repository
417 .get_tracking_summary(convocation_id)
418 .await?;
419
420 let mut convocation = self
421 .convocation_repository
422 .find_by_id(convocation_id)
423 .await?
424 .ok_or_else(|| format!("Convocation not found: {}", convocation_id))?;
425
426 convocation.update_tracking_counts(
427 summary.opened_count as i32,
428 summary.will_attend_count as i32,
429 summary.will_not_attend_count as i32,
430 );
431
432 self.convocation_repository.update(&convocation).await?;
433
434 Ok(())
435 }
436}