koprogo_api/infrastructure/web/handlers/
convocation_handlers.rs

1use crate::application::dto::{
2    CreateConvocationRequest, ScheduleConvocationRequest, SendConvocationRequest, SetProxyRequest,
3    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(state: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
59    match state.convocation_use_cases.get_convocation(*id).await {
60        Ok(convocation) => HttpResponse::Ok().json(convocation),
61        Err(err) => HttpResponse::NotFound().json(serde_json::json!({"error": err})),
62    }
63}
64
65#[get("/meetings/{meeting_id}/convocation")]
66pub async fn get_convocation_by_meeting(
67    state: web::Data<AppState>,
68    meeting_id: web::Path<Uuid>,
69) -> impl Responder {
70    match state
71        .convocation_use_cases
72        .get_convocation_by_meeting(*meeting_id)
73        .await
74    {
75        Ok(Some(convocation)) => HttpResponse::Ok().json(convocation),
76        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
77            "error": "Convocation not found for this meeting"
78        })),
79        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
80    }
81}
82
83#[get("/buildings/{building_id}/convocations")]
84pub async fn list_building_convocations(
85    state: web::Data<AppState>,
86    building_id: web::Path<Uuid>,
87) -> impl Responder {
88    match state
89        .convocation_use_cases
90        .list_building_convocations(*building_id)
91        .await
92    {
93        Ok(convocations) => HttpResponse::Ok().json(convocations),
94        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
95    }
96}
97
98#[get("/organizations/{organization_id}/convocations")]
99pub async fn list_organization_convocations(
100    state: web::Data<AppState>,
101    organization_id: web::Path<Uuid>,
102) -> impl Responder {
103    match state
104        .convocation_use_cases
105        .list_organization_convocations(*organization_id)
106        .await
107    {
108        Ok(convocations) => HttpResponse::Ok().json(convocations),
109        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
110    }
111}
112
113#[delete("/convocations/{id}")]
114pub async fn delete_convocation(
115    state: web::Data<AppState>,
116    user: AuthenticatedUser,
117    id: web::Path<Uuid>,
118) -> impl Responder {
119    let organization_id = match user.require_organization() {
120        Ok(org_id) => org_id,
121        Err(e) => {
122            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
123        }
124    };
125
126    match state.convocation_use_cases.delete_convocation(*id).await {
127        Ok(true) => {
128            AuditLogEntry::new(
129                AuditEventType::ConvocationDeleted,
130                Some(user.user_id),
131                Some(organization_id),
132            )
133            .with_resource("Convocation", *id)
134            .log();
135
136            HttpResponse::NoContent().finish()
137        }
138        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
139            "error": "Convocation not found"
140        })),
141        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
142    }
143}
144
145// ==================== Convocation Actions ====================
146
147#[put("/convocations/{id}/schedule")]
148pub async fn schedule_convocation(
149    state: web::Data<AppState>,
150    user: AuthenticatedUser,
151    id: web::Path<Uuid>,
152    request: web::Json<ScheduleConvocationRequest>,
153) -> impl Responder {
154    let organization_id = match user.require_organization() {
155        Ok(org_id) => org_id,
156        Err(e) => {
157            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
158        }
159    };
160
161    match state
162        .convocation_use_cases
163        .schedule_convocation(*id, request.into_inner())
164        .await
165    {
166        Ok(convocation) => {
167            AuditLogEntry::new(
168                AuditEventType::ConvocationScheduled,
169                Some(user.user_id),
170                Some(organization_id),
171            )
172            .with_resource("Convocation", convocation.id)
173            .log();
174
175            HttpResponse::Ok().json(convocation)
176        }
177        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
178    }
179}
180
181#[post("/convocations/{id}/send")]
182pub async fn send_convocation(
183    state: web::Data<AppState>,
184    user: AuthenticatedUser,
185    id: web::Path<Uuid>,
186    request: web::Json<SendConvocationRequest>,
187) -> impl Responder {
188    let organization_id = match user.require_organization() {
189        Ok(org_id) => org_id,
190        Err(e) => {
191            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
192        }
193    };
194
195    // PDF generation now happens in the use case layer
196    match state
197        .convocation_use_cases
198        .send_convocation(*id, request.into_inner())
199        .await
200    {
201        Ok(convocation) => {
202            AuditLogEntry::new(
203                AuditEventType::ConvocationSent,
204                Some(user.user_id),
205                Some(organization_id),
206            )
207            .with_resource("Convocation", convocation.id)
208            .with_details(format!("recipients: {}", convocation.total_recipients))
209            .log();
210
211            HttpResponse::Ok().json(convocation)
212        }
213        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
214    }
215}
216
217#[put("/convocations/{id}/cancel")]
218pub async fn cancel_convocation(
219    state: web::Data<AppState>,
220    user: AuthenticatedUser,
221    id: web::Path<Uuid>,
222) -> impl Responder {
223    let organization_id = match user.require_organization() {
224        Ok(org_id) => org_id,
225        Err(e) => {
226            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
227        }
228    };
229
230    match state.convocation_use_cases.cancel_convocation(*id).await {
231        Ok(convocation) => {
232            AuditLogEntry::new(
233                AuditEventType::ConvocationCancelled,
234                Some(user.user_id),
235                Some(organization_id),
236            )
237            .with_resource("Convocation", convocation.id)
238            .log();
239
240            HttpResponse::Ok().json(convocation)
241        }
242        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
243    }
244}
245
246// ==================== Recipient Endpoints ====================
247
248#[get("/convocations/{id}/recipients")]
249pub async fn list_convocation_recipients(
250    state: web::Data<AppState>,
251    id: web::Path<Uuid>,
252) -> impl Responder {
253    match state
254        .convocation_use_cases
255        .list_convocation_recipients(*id)
256        .await
257    {
258        Ok(recipients) => HttpResponse::Ok().json(recipients),
259        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
260    }
261}
262
263#[get("/convocations/{id}/tracking-summary")]
264pub async fn get_convocation_tracking_summary(
265    state: web::Data<AppState>,
266    id: web::Path<Uuid>,
267) -> impl Responder {
268    match state.convocation_use_cases.get_tracking_summary(*id).await {
269        Ok(summary) => HttpResponse::Ok().json(summary),
270        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
271    }
272}
273
274#[put("/convocation-recipients/{id}/email-opened")]
275pub async fn mark_recipient_email_opened(
276    state: web::Data<AppState>,
277    id: web::Path<Uuid>,
278) -> impl Responder {
279    match state
280        .convocation_use_cases
281        .mark_recipient_email_opened(*id)
282        .await
283    {
284        Ok(recipient) => HttpResponse::Ok().json(recipient),
285        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
286    }
287}
288
289#[put("/convocation-recipients/{id}/attendance")]
290pub async fn update_recipient_attendance(
291    state: web::Data<AppState>,
292    user: AuthenticatedUser,
293    id: web::Path<Uuid>,
294    request: web::Json<UpdateAttendanceRequest>,
295) -> impl Responder {
296    let organization_id = match user.require_organization() {
297        Ok(org_id) => org_id,
298        Err(e) => {
299            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
300        }
301    };
302
303    match state
304        .convocation_use_cases
305        .update_recipient_attendance(*id, request.attendance_status.clone())
306        .await
307    {
308        Ok(recipient) => {
309            AuditLogEntry::new(
310                AuditEventType::ConvocationAttendanceUpdated,
311                Some(user.user_id),
312                Some(organization_id),
313            )
314            .with_resource("ConvocationRecipient", recipient.id)
315            .with_details(format!("status: {:?}", recipient.attendance_status))
316            .log();
317
318            HttpResponse::Ok().json(recipient)
319        }
320        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
321    }
322}
323
324#[put("/convocation-recipients/{id}/proxy")]
325pub async fn set_recipient_proxy(
326    state: web::Data<AppState>,
327    user: AuthenticatedUser,
328    id: web::Path<Uuid>,
329    request: web::Json<SetProxyRequest>,
330) -> impl Responder {
331    let organization_id = match user.require_organization() {
332        Ok(org_id) => org_id,
333        Err(e) => {
334            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
335        }
336    };
337
338    match state
339        .convocation_use_cases
340        .set_recipient_proxy(*id, request.proxy_owner_id)
341        .await
342    {
343        Ok(recipient) => {
344            AuditLogEntry::new(
345                AuditEventType::ConvocationProxySet,
346                Some(user.user_id),
347                Some(organization_id),
348            )
349            .with_resource("ConvocationRecipient", recipient.id)
350            .with_details(format!("proxy_owner_id: {:?}", recipient.proxy_owner_id))
351            .log();
352
353            HttpResponse::Ok().json(recipient)
354        }
355        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
356    }
357}
358
359#[post("/convocations/{id}/send-reminders")]
360pub async fn send_convocation_reminders(
361    state: web::Data<AppState>,
362    user: AuthenticatedUser,
363    id: web::Path<Uuid>,
364) -> impl Responder {
365    let organization_id = match user.require_organization() {
366        Ok(org_id) => org_id,
367        Err(e) => {
368            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
369        }
370    };
371
372    match state.convocation_use_cases.send_reminders(*id).await {
373        Ok(recipients) => {
374            AuditLogEntry::new(
375                AuditEventType::ConvocationReminderSent,
376                Some(user.user_id),
377                Some(organization_id),
378            )
379            .with_resource("Convocation", *id)
380            .with_details(format!("recipients: {}", recipients.len()))
381            .log();
382
383            HttpResponse::Ok().json(recipients)
384        }
385        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
386    }
387}