koprogo_api/infrastructure/web/handlers/
convocation_handlers.rs1use 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#[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 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#[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 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#[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#[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 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 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}