koprogo_api/infrastructure/web/handlers/
convocation_handlers.rs

1use crate::application::dto::{
2    CreateConvocationRequest, ScheduleConvocationRequest, ScheduleSecondConvocationRequest,
3    SendConvocationRequest, SetProxyRequest, UpdateAttendanceRequest,
4};
5use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
8use uuid::Uuid;
9
10// ==================== Convocation CRUD Endpoints ====================
11
12#[post("/convocations")]
13pub async fn create_convocation(
14    state: web::Data<AppState>,
15    user: AuthenticatedUser,
16    request: web::Json<CreateConvocationRequest>,
17) -> impl Responder {
18    let organization_id = match user.require_organization() {
19        Ok(org_id) => org_id,
20        Err(e) => {
21            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
22        }
23    };
24
25    let created_by = user.user_id;
26
27    match state
28        .convocation_use_cases
29        .create_convocation(organization_id, request.into_inner(), created_by)
30        .await
31    {
32        Ok(convocation) => {
33            AuditLogEntry::new(
34                AuditEventType::ConvocationCreated,
35                Some(user.user_id),
36                Some(organization_id),
37            )
38            .with_resource("Convocation", convocation.id)
39            .log();
40
41            HttpResponse::Created().json(convocation)
42        }
43        Err(err) => {
44            AuditLogEntry::new(
45                AuditEventType::ConvocationCreated,
46                Some(user.user_id),
47                Some(organization_id),
48            )
49            .with_error(err.clone())
50            .log();
51
52            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
53        }
54    }
55}
56
57#[get("/convocations/{id}")]
58pub async fn get_convocation(
59    state: web::Data<AppState>,
60    user: AuthenticatedUser,
61    id: web::Path<Uuid>,
62) -> impl Responder {
63    match state.convocation_use_cases.get_convocation(*id).await {
64        Ok(convocation) => {
65            // Verify organization access
66            if let Err(err) = user.verify_org_access(convocation.organization_id) {
67                return HttpResponse::Forbidden().json(serde_json::json!({"error": err}));
68            }
69            HttpResponse::Ok().json(convocation)
70        }
71        Err(err) => HttpResponse::NotFound().json(serde_json::json!({"error": err})),
72    }
73}
74
75#[get("/meetings/{meeting_id}/convocation")]
76pub async fn get_convocation_by_meeting(
77    state: web::Data<AppState>,
78    meeting_id: web::Path<Uuid>,
79) -> impl Responder {
80    match state
81        .convocation_use_cases
82        .get_convocation_by_meeting(*meeting_id)
83        .await
84    {
85        Ok(Some(convocation)) => HttpResponse::Ok().json(convocation),
86        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
87            "error": "Convocation not found for this meeting"
88        })),
89        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
90    }
91}
92
93#[get("/buildings/{building_id}/convocations")]
94pub async fn list_building_convocations(
95    state: web::Data<AppState>,
96    building_id: web::Path<Uuid>,
97) -> impl Responder {
98    match state
99        .convocation_use_cases
100        .list_building_convocations(*building_id)
101        .await
102    {
103        Ok(convocations) => HttpResponse::Ok().json(convocations),
104        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
105    }
106}
107
108#[get("/organizations/{organization_id}/convocations")]
109pub async fn list_organization_convocations(
110    state: web::Data<AppState>,
111    organization_id: web::Path<Uuid>,
112) -> impl Responder {
113    match state
114        .convocation_use_cases
115        .list_organization_convocations(*organization_id)
116        .await
117    {
118        Ok(convocations) => HttpResponse::Ok().json(convocations),
119        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
120    }
121}
122
123#[delete("/convocations/{id}")]
124pub async fn delete_convocation(
125    state: web::Data<AppState>,
126    user: AuthenticatedUser,
127    id: web::Path<Uuid>,
128) -> impl Responder {
129    let organization_id = match user.require_organization() {
130        Ok(org_id) => org_id,
131        Err(e) => {
132            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
133        }
134    };
135
136    match state.convocation_use_cases.delete_convocation(*id).await {
137        Ok(true) => {
138            AuditLogEntry::new(
139                AuditEventType::ConvocationDeleted,
140                Some(user.user_id),
141                Some(organization_id),
142            )
143            .with_resource("Convocation", *id)
144            .log();
145
146            HttpResponse::NoContent().finish()
147        }
148        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
149            "error": "Convocation not found"
150        })),
151        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
152    }
153}
154
155// ==================== Convocation Actions ====================
156
157#[put("/convocations/{id}/schedule")]
158pub async fn schedule_convocation(
159    state: web::Data<AppState>,
160    user: AuthenticatedUser,
161    id: web::Path<Uuid>,
162    request: web::Json<ScheduleConvocationRequest>,
163) -> impl Responder {
164    let organization_id = match user.require_organization() {
165        Ok(org_id) => org_id,
166        Err(e) => {
167            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
168        }
169    };
170
171    match state
172        .convocation_use_cases
173        .schedule_convocation(*id, request.into_inner())
174        .await
175    {
176        Ok(convocation) => {
177            AuditLogEntry::new(
178                AuditEventType::ConvocationScheduled,
179                Some(user.user_id),
180                Some(organization_id),
181            )
182            .with_resource("Convocation", convocation.id)
183            .log();
184
185            HttpResponse::Ok().json(convocation)
186        }
187        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
188    }
189}
190
191#[post("/convocations/{id}/send")]
192pub async fn send_convocation(
193    state: web::Data<AppState>,
194    user: AuthenticatedUser,
195    id: web::Path<Uuid>,
196    request: web::Json<SendConvocationRequest>,
197) -> impl Responder {
198    let organization_id = match user.require_organization() {
199        Ok(org_id) => org_id,
200        Err(e) => {
201            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
202        }
203    };
204
205    // PDF generation now happens in the use case layer
206    match state
207        .convocation_use_cases
208        .send_convocation(*id, request.into_inner())
209        .await
210    {
211        Ok(convocation) => {
212            AuditLogEntry::new(
213                AuditEventType::ConvocationSent,
214                Some(user.user_id),
215                Some(organization_id),
216            )
217            .with_resource("Convocation", convocation.id)
218            .with_details(format!("recipients: {}", convocation.total_recipients))
219            .log();
220
221            HttpResponse::Ok().json(convocation)
222        }
223        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
224    }
225}
226
227#[put("/convocations/{id}/cancel")]
228pub async fn cancel_convocation(
229    state: web::Data<AppState>,
230    user: AuthenticatedUser,
231    id: web::Path<Uuid>,
232) -> impl Responder {
233    let organization_id = match user.require_organization() {
234        Ok(org_id) => org_id,
235        Err(e) => {
236            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
237        }
238    };
239
240    match state.convocation_use_cases.cancel_convocation(*id).await {
241        Ok(convocation) => {
242            AuditLogEntry::new(
243                AuditEventType::ConvocationCancelled,
244                Some(user.user_id),
245                Some(organization_id),
246            )
247            .with_resource("Convocation", convocation.id)
248            .log();
249
250            HttpResponse::Ok().json(convocation)
251        }
252        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
253    }
254}
255
256// ==================== Recipient Endpoints ====================
257
258#[get("/convocations/{id}/recipients")]
259pub async fn list_convocation_recipients(
260    state: web::Data<AppState>,
261    id: web::Path<Uuid>,
262) -> impl Responder {
263    match state
264        .convocation_use_cases
265        .list_convocation_recipients(*id)
266        .await
267    {
268        Ok(recipients) => HttpResponse::Ok().json(recipients),
269        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
270    }
271}
272
273#[get("/convocations/{id}/tracking-summary")]
274pub async fn get_convocation_tracking_summary(
275    state: web::Data<AppState>,
276    id: web::Path<Uuid>,
277) -> impl Responder {
278    match state.convocation_use_cases.get_tracking_summary(*id).await {
279        Ok(summary) => HttpResponse::Ok().json(summary),
280        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
281    }
282}
283
284#[put("/convocation-recipients/{id}/email-opened")]
285pub async fn mark_recipient_email_opened(
286    state: web::Data<AppState>,
287    id: web::Path<Uuid>,
288) -> impl Responder {
289    match state
290        .convocation_use_cases
291        .mark_recipient_email_opened(*id)
292        .await
293    {
294        Ok(recipient) => HttpResponse::Ok().json(recipient),
295        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
296    }
297}
298
299#[put("/convocation-recipients/{id}/attendance")]
300pub async fn update_recipient_attendance(
301    state: web::Data<AppState>,
302    user: AuthenticatedUser,
303    id: web::Path<Uuid>,
304    request: web::Json<UpdateAttendanceRequest>,
305) -> impl Responder {
306    let organization_id = match user.require_organization() {
307        Ok(org_id) => org_id,
308        Err(e) => {
309            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
310        }
311    };
312
313    match state
314        .convocation_use_cases
315        .update_recipient_attendance(*id, request.attendance_status.clone())
316        .await
317    {
318        Ok(recipient) => {
319            AuditLogEntry::new(
320                AuditEventType::ConvocationAttendanceUpdated,
321                Some(user.user_id),
322                Some(organization_id),
323            )
324            .with_resource("ConvocationRecipient", recipient.id)
325            .with_details(format!("status: {:?}", recipient.attendance_status))
326            .log();
327
328            HttpResponse::Ok().json(recipient)
329        }
330        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
331    }
332}
333
334#[put("/convocation-recipients/{id}/proxy")]
335pub async fn set_recipient_proxy(
336    state: web::Data<AppState>,
337    user: AuthenticatedUser,
338    id: web::Path<Uuid>,
339    request: web::Json<SetProxyRequest>,
340) -> impl Responder {
341    let organization_id = match user.require_organization() {
342        Ok(org_id) => org_id,
343        Err(e) => {
344            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
345        }
346    };
347
348    match state
349        .convocation_use_cases
350        .set_recipient_proxy(*id, request.proxy_owner_id)
351        .await
352    {
353        Ok(recipient) => {
354            AuditLogEntry::new(
355                AuditEventType::ConvocationProxySet,
356                Some(user.user_id),
357                Some(organization_id),
358            )
359            .with_resource("ConvocationRecipient", recipient.id)
360            .with_details(format!("proxy_owner_id: {:?}", recipient.proxy_owner_id))
361            .log();
362
363            HttpResponse::Ok().json(recipient)
364        }
365        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
366    }
367}
368
369#[post("/convocations/{id}/reminders")]
370pub async fn send_convocation_reminders(
371    state: web::Data<AppState>,
372    user: AuthenticatedUser,
373    id: web::Path<Uuid>,
374) -> impl Responder {
375    let organization_id = match user.require_organization() {
376        Ok(org_id) => org_id,
377        Err(e) => {
378            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
379        }
380    };
381
382    match state.convocation_use_cases.send_reminders(*id).await {
383        Ok(recipients) => {
384            AuditLogEntry::new(
385                AuditEventType::ConvocationReminderSent,
386                Some(user.user_id),
387                Some(organization_id),
388            )
389            .with_resource("Convocation", *id)
390            .with_details(format!("recipients: {}", recipients.len()))
391            .log();
392
393            HttpResponse::Ok().json(recipients)
394        }
395        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
396    }
397}
398
399// ==================== Second Convocation Endpoint (Art. 3.87 §5 CC) ====================
400
401#[post("/convocations/second")]
402pub async fn schedule_second_convocation(
403    state: web::Data<AppState>,
404    user: AuthenticatedUser,
405    request: web::Json<ScheduleSecondConvocationRequest>,
406) -> impl Responder {
407    let organization_id = match user.require_organization() {
408        Ok(org_id) => org_id,
409        Err(e) => {
410            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
411        }
412    };
413
414    let req = request.into_inner();
415
416    // Create a new meeting for the second convocation
417    let new_meeting_req = crate::application::dto::CreateMeetingRequest {
418        organization_id,
419        building_id: req.building_id,
420        meeting_type: crate::domain::entities::MeetingType::Ordinary,
421        title: format!("Second Convocation (Art. 3.87 §5 CC)"),
422        description: Some("Second convocation after quorum not reached".to_string()),
423        scheduled_date: req.new_meeting_date,
424        location: "Same as first meeting".to_string(),
425    };
426
427    // Create the new meeting
428    let new_meeting = match state
429        .meeting_use_cases
430        .create_meeting(new_meeting_req)
431        .await
432    {
433        Ok(m) => m,
434        Err(err) => {
435            return HttpResponse::BadRequest()
436                .json(serde_json::json!({"error": format!("Failed to create meeting: {}", err)}))
437        }
438    };
439
440    match state
441        .convocation_use_cases
442        .schedule_second_convocation(
443            organization_id,
444            req.building_id,
445            req.first_meeting_id,
446            new_meeting.id,
447            req.new_meeting_date,
448            req.language,
449            user.user_id,
450        )
451        .await
452    {
453        Ok(convocation) => {
454            AuditLogEntry::new(
455                AuditEventType::SecondConvocationScheduled,
456                Some(user.user_id),
457                Some(organization_id),
458            )
459            .with_resource("Convocation", convocation.id)
460            .with_details(format!(
461                "first_meeting_id: {}, new_meeting_id: {}",
462                req.first_meeting_id, new_meeting.id
463            ))
464            .log();
465
466            HttpResponse::Created().json(convocation)
467        }
468        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
469    }
470}