koprogo_api/application/use_cases/
convocation_use_cases.rs

1use 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    /// Create a new convocation
41    pub async fn create_convocation(
42        &self,
43        organization_id: Uuid,
44        request: CreateConvocationRequest,
45        created_by: Uuid,
46    ) -> Result<ConvocationResponse, String> {
47        // Create domain entity (validates legal deadline)
48        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    /// Get convocation by ID
64    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    /// Get convocation by meeting ID
75    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    /// List convocations for a building
88    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    /// List convocations for an organization
104    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    /// Schedule convocation to be sent at specific date
120    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    /// Send convocation to owners (generates PDF, creates recipients, sends emails)
139    /// This would typically be called by a background job or email service
140    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        // Fetch building for PDF generation
152        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        // Fetch meeting for PDF generation
159        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        // Generate PDF
166        let pdf_bytes = ConvocationExporter::export_to_pdf(&building, &meeting, &convocation)
167            .map_err(|e| format!("Failed to generate PDF: {}", e))?;
168
169        // Save PDF to file
170        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        // Fetch owner emails
175        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        // Create recipients in database (bulk insert)
188        let created_recipients = self.recipient_repository.create_many(&recipients).await?;
189
190        // Mark convocation as sent
191        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    /// Mark recipient email as sent
199    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    /// Mark recipient email as opened (tracking pixel or link click)
217    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        // Update convocation tracking counts
232        self.update_convocation_tracking(recipient.convocation_id)
233            .await?;
234
235        Ok(ConvocationRecipientResponse::from(updated))
236    }
237
238    /// Update recipient attendance status
239    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        // Update convocation tracking counts
255        self.update_convocation_tracking(recipient.convocation_id)
256            .await?;
257
258        Ok(ConvocationRecipientResponse::from(updated))
259    }
260
261    /// Set proxy delegation for recipient
262    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    /// Send reminders to recipients who haven't opened the convocation (J-3)
281    /// This would typically be called by a background job
282    pub async fn send_reminders(
283        &self,
284        convocation_id: Uuid,
285    ) -> Result<Vec<ConvocationRecipientResponse>, String> {
286        // Get recipients who need reminder
287        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        // Mark convocation as reminder sent
301        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    /// Get tracking summary for convocation
316    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    /// Get all recipients for a convocation
338    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    /// Cancel convocation
354    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    /// Delete convocation (and all recipients via CASCADE)
369    pub async fn delete_convocation(&self, id: Uuid) -> Result<bool, String> {
370        self.convocation_repository.delete(id).await
371    }
372
373    /// Process scheduled convocations (called by background job)
374    /// Returns list of convocations that were sent
375    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            // This would trigger PDF generation and email sending
386            // For now, we just return the list that needs processing
387            sent.push(ConvocationResponse::from(convocation));
388        }
389
390        Ok(sent)
391    }
392
393    /// Process reminder sending (called by background job)
394    /// Returns list of convocations that had reminders sent
395    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            // Send reminders to recipients
406            self.send_reminders(convocation.id).await?;
407            processed.push(ConvocationResponse::from(convocation));
408        }
409
410        Ok(processed)
411    }
412
413    /// Internal helper: Update convocation tracking counts from recipients
414    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}