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    user: AuthenticatedUser,
112    organization_id: web::Path<Uuid>,
113) -> impl Responder {
114    if let Err(e) = user.verify_org_access(*organization_id) {
115        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
116    }
117    match state
118        .convocation_use_cases
119        .list_organization_convocations(*organization_id)
120        .await
121    {
122        Ok(convocations) => HttpResponse::Ok().json(convocations),
123        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
124    }
125}
126
127#[delete("/convocations/{id}")]
128pub async fn delete_convocation(
129    state: web::Data<AppState>,
130    user: AuthenticatedUser,
131    id: web::Path<Uuid>,
132) -> impl Responder {
133    let organization_id = match user.require_organization() {
134        Ok(org_id) => org_id,
135        Err(e) => {
136            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
137        }
138    };
139
140    match state.convocation_use_cases.delete_convocation(*id).await {
141        Ok(true) => {
142            AuditLogEntry::new(
143                AuditEventType::ConvocationDeleted,
144                Some(user.user_id),
145                Some(organization_id),
146            )
147            .with_resource("Convocation", *id)
148            .log();
149
150            HttpResponse::NoContent().finish()
151        }
152        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
153            "error": "Convocation not found"
154        })),
155        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
156    }
157}
158
159// ==================== Convocation Actions ====================
160
161#[put("/convocations/{id}/schedule")]
162pub async fn schedule_convocation(
163    state: web::Data<AppState>,
164    user: AuthenticatedUser,
165    id: web::Path<Uuid>,
166    request: web::Json<ScheduleConvocationRequest>,
167) -> impl Responder {
168    let organization_id = match user.require_organization() {
169        Ok(org_id) => org_id,
170        Err(e) => {
171            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
172        }
173    };
174
175    match state
176        .convocation_use_cases
177        .schedule_convocation(*id, request.into_inner())
178        .await
179    {
180        Ok(convocation) => {
181            AuditLogEntry::new(
182                AuditEventType::ConvocationScheduled,
183                Some(user.user_id),
184                Some(organization_id),
185            )
186            .with_resource("Convocation", convocation.id)
187            .log();
188
189            HttpResponse::Ok().json(convocation)
190        }
191        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
192    }
193}
194
195#[post("/convocations/{id}/send")]
196pub async fn send_convocation(
197    state: web::Data<AppState>,
198    user: AuthenticatedUser,
199    id: web::Path<Uuid>,
200    request: web::Json<SendConvocationRequest>,
201) -> impl Responder {
202    let organization_id = match user.require_organization() {
203        Ok(org_id) => org_id,
204        Err(e) => {
205            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
206        }
207    };
208
209    // PDF generation now happens in the use case layer
210    match state
211        .convocation_use_cases
212        .send_convocation(*id, request.into_inner())
213        .await
214    {
215        Ok(convocation) => {
216            AuditLogEntry::new(
217                AuditEventType::ConvocationSent,
218                Some(user.user_id),
219                Some(organization_id),
220            )
221            .with_resource("Convocation", convocation.id)
222            .with_details(format!("recipients: {}", convocation.total_recipients))
223            .log();
224
225            HttpResponse::Ok().json(convocation)
226        }
227        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
228    }
229}
230
231#[put("/convocations/{id}/cancel")]
232pub async fn cancel_convocation(
233    state: web::Data<AppState>,
234    user: AuthenticatedUser,
235    id: web::Path<Uuid>,
236) -> impl Responder {
237    let organization_id = match user.require_organization() {
238        Ok(org_id) => org_id,
239        Err(e) => {
240            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
241        }
242    };
243
244    match state.convocation_use_cases.cancel_convocation(*id).await {
245        Ok(convocation) => {
246            AuditLogEntry::new(
247                AuditEventType::ConvocationCancelled,
248                Some(user.user_id),
249                Some(organization_id),
250            )
251            .with_resource("Convocation", convocation.id)
252            .log();
253
254            HttpResponse::Ok().json(convocation)
255        }
256        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
257    }
258}
259
260// ==================== Recipient Endpoints ====================
261
262#[get("/convocations/{id}/recipients")]
263pub async fn list_convocation_recipients(
264    state: web::Data<AppState>,
265    id: web::Path<Uuid>,
266) -> impl Responder {
267    match state
268        .convocation_use_cases
269        .list_convocation_recipients(*id)
270        .await
271    {
272        Ok(recipients) => HttpResponse::Ok().json(recipients),
273        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
274    }
275}
276
277#[get("/convocations/{id}/tracking-summary")]
278pub async fn get_convocation_tracking_summary(
279    state: web::Data<AppState>,
280    id: web::Path<Uuid>,
281) -> impl Responder {
282    match state.convocation_use_cases.get_tracking_summary(*id).await {
283        Ok(summary) => HttpResponse::Ok().json(summary),
284        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
285    }
286}
287
288#[put("/convocation-recipients/{id}/email-opened")]
289pub async fn mark_recipient_email_opened(
290    state: web::Data<AppState>,
291    id: web::Path<Uuid>,
292) -> impl Responder {
293    match state
294        .convocation_use_cases
295        .mark_recipient_email_opened(*id)
296        .await
297    {
298        Ok(recipient) => HttpResponse::Ok().json(recipient),
299        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
300    }
301}
302
303#[put("/convocation-recipients/{id}/attendance")]
304pub async fn update_recipient_attendance(
305    state: web::Data<AppState>,
306    user: AuthenticatedUser,
307    id: web::Path<Uuid>,
308    request: web::Json<UpdateAttendanceRequest>,
309) -> impl Responder {
310    let organization_id = match user.require_organization() {
311        Ok(org_id) => org_id,
312        Err(e) => {
313            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
314        }
315    };
316
317    match state
318        .convocation_use_cases
319        .update_recipient_attendance(*id, request.attendance_status.clone())
320        .await
321    {
322        Ok(recipient) => {
323            AuditLogEntry::new(
324                AuditEventType::ConvocationAttendanceUpdated,
325                Some(user.user_id),
326                Some(organization_id),
327            )
328            .with_resource("ConvocationRecipient", recipient.id)
329            .with_details(format!("status: {:?}", recipient.attendance_status))
330            .log();
331
332            HttpResponse::Ok().json(recipient)
333        }
334        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
335    }
336}
337
338#[put("/convocation-recipients/{id}/proxy")]
339pub async fn set_recipient_proxy(
340    state: web::Data<AppState>,
341    user: AuthenticatedUser,
342    id: web::Path<Uuid>,
343    request: web::Json<SetProxyRequest>,
344) -> impl Responder {
345    let organization_id = match user.require_organization() {
346        Ok(org_id) => org_id,
347        Err(e) => {
348            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
349        }
350    };
351
352    match state
353        .convocation_use_cases
354        .set_recipient_proxy(*id, request.proxy_owner_id)
355        .await
356    {
357        Ok(recipient) => {
358            AuditLogEntry::new(
359                AuditEventType::ConvocationProxySet,
360                Some(user.user_id),
361                Some(organization_id),
362            )
363            .with_resource("ConvocationRecipient", recipient.id)
364            .with_details(format!("proxy_owner_id: {:?}", recipient.proxy_owner_id))
365            .log();
366
367            HttpResponse::Ok().json(recipient)
368        }
369        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
370    }
371}
372
373#[post("/convocations/{id}/reminders")]
374pub async fn send_convocation_reminders(
375    state: web::Data<AppState>,
376    user: AuthenticatedUser,
377    id: web::Path<Uuid>,
378) -> impl Responder {
379    let organization_id = match user.require_organization() {
380        Ok(org_id) => org_id,
381        Err(e) => {
382            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
383        }
384    };
385
386    match state.convocation_use_cases.send_reminders(*id).await {
387        Ok(recipients) => {
388            AuditLogEntry::new(
389                AuditEventType::ConvocationReminderSent,
390                Some(user.user_id),
391                Some(organization_id),
392            )
393            .with_resource("Convocation", *id)
394            .with_details(format!("recipients: {}", recipients.len()))
395            .log();
396
397            HttpResponse::Ok().json(recipients)
398        }
399        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
400    }
401}
402
403// ==================== Second Convocation Endpoint (Art. 3.87 §5 CC) ====================
404
405#[post("/convocations/second")]
406pub async fn schedule_second_convocation(
407    state: web::Data<AppState>,
408    user: AuthenticatedUser,
409    request: web::Json<ScheduleSecondConvocationRequest>,
410) -> impl Responder {
411    let organization_id = match user.require_organization() {
412        Ok(org_id) => org_id,
413        Err(e) => {
414            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
415        }
416    };
417
418    let req = request.into_inner();
419
420    // Create a new meeting for the second convocation
421    let new_meeting_req = crate::application::dto::CreateMeetingRequest {
422        organization_id,
423        building_id: req.building_id,
424        meeting_type: crate::domain::entities::MeetingType::Ordinary,
425        title: format!("Second Convocation (Art. 3.87 §5 CC)"),
426        description: Some("Second convocation after quorum not reached".to_string()),
427        scheduled_date: req.new_meeting_date,
428        location: "Same as first meeting".to_string(),
429        is_second_convocation: true,
430    };
431
432    // Create the new meeting
433    let new_meeting = match state
434        .meeting_use_cases
435        .create_meeting(new_meeting_req)
436        .await
437    {
438        Ok(m) => m,
439        Err(err) => {
440            return HttpResponse::BadRequest()
441                .json(serde_json::json!({"error": format!("Failed to create meeting: {}", err)}))
442        }
443    };
444
445    match state
446        .convocation_use_cases
447        .schedule_second_convocation(
448            organization_id,
449            req.building_id,
450            req.first_meeting_id,
451            new_meeting.id,
452            req.new_meeting_date,
453            req.language,
454            user.user_id,
455        )
456        .await
457    {
458        Ok(convocation) => {
459            AuditLogEntry::new(
460                AuditEventType::SecondConvocationScheduled,
461                Some(user.user_id),
462                Some(organization_id),
463            )
464            .with_resource("Convocation", convocation.id)
465            .with_details(format!(
466                "first_meeting_id: {}, new_meeting_id: {}",
467                req.first_meeting_id, new_meeting.id
468            ))
469            .log();
470
471            HttpResponse::Created().json(convocation)
472        }
473        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
474    }
475}