1use crate::application::dto::{
2 CreateConvocationRequest, ScheduleConvocationRequest, ScheduleSecondConvocationRequest,
3 SendConvocationRequest, SetProxyRequest, UpdateAttendanceRequest,
4};
5use crate::application::error::AppError;
6use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
7use crate::infrastructure::web::middleware::scope_guard::verify_building_org_access;
8use crate::infrastructure::web::{AppState, AuthenticatedUser};
9use actix_web::ResponseError;
10use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
11use uuid::Uuid;
12
13fn require_syndic_or_superadmin(user: &AuthenticatedUser) -> Result<(), AppError> {
20 match user.role.as_str() {
21 "syndic" | "superadmin" => Ok(()),
22 _ => Err(AppError::Forbidden(
23 "Seul le syndic peut envoyer une convocation".to_string(),
24 )),
25 }
26}
27
28#[post("/convocations")]
29pub async fn create_convocation(
30 state: web::Data<AppState>,
31 user: AuthenticatedUser,
32 request: web::Json<CreateConvocationRequest>,
33) -> impl Responder {
34 let organization_id = match user.require_organization() {
35 Ok(org_id) => org_id,
36 Err(e) => {
37 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
38 }
39 };
40
41 if let Err(err) = verify_building_org_access(
45 &user,
46 request.building_id,
47 &state.building_use_cases,
48 &state.acp_use_cases,
49 )
50 .await
51 {
52 return err.error_response();
53 }
54
55 let created_by = user.user_id;
56
57 match state
58 .convocation_use_cases
59 .create_convocation(organization_id, request.into_inner(), created_by)
60 .await
61 {
62 Ok(convocation) => {
63 AuditLogEntry::new(
64 AuditEventType::ConvocationCreated,
65 Some(user.user_id),
66 Some(organization_id),
67 )
68 .with_resource("Convocation", convocation.id)
69 .log();
70
71 HttpResponse::Created().json(convocation)
72 }
73 Err(err) => {
74 AuditLogEntry::new(
75 AuditEventType::ConvocationCreated,
76 Some(user.user_id),
77 Some(organization_id),
78 )
79 .with_error(err.clone())
80 .log();
81
82 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
83 }
84 }
85}
86
87#[get("/convocations/{id}")]
88pub async fn get_convocation(
89 state: web::Data<AppState>,
90 user: AuthenticatedUser,
91 id: web::Path<Uuid>,
92) -> impl Responder {
93 match state.convocation_use_cases.get_convocation(*id).await {
94 Ok(convocation) => {
95 if let Err(err) = user.verify_org_access(convocation.organization_id) {
97 return HttpResponse::Forbidden().json(serde_json::json!({"error": err}));
98 }
99 HttpResponse::Ok().json(convocation)
100 }
101 Err(err) => HttpResponse::NotFound().json(serde_json::json!({"error": err})),
102 }
103}
104
105#[get("/meetings/{meeting_id}/convocation")]
106pub async fn get_convocation_by_meeting(
107 state: web::Data<AppState>,
108 meeting_id: web::Path<Uuid>,
109 user: AuthenticatedUser,
110) -> impl Responder {
111 if let Err(err) =
115 crate::infrastructure::web::middleware::scope_guard::verify_meeting_org_access(
116 &user,
117 *meeting_id,
118 &state.meeting_use_cases,
119 &state.building_use_cases,
120 &state.acp_use_cases,
121 )
122 .await
123 {
124 return err.error_response();
125 }
126
127 match state
128 .convocation_use_cases
129 .get_convocation_by_meeting(*meeting_id)
130 .await
131 {
132 Ok(Some(convocation)) => HttpResponse::Ok().json(convocation),
133 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
134 "error": "Convocation not found for this meeting"
135 })),
136 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
137 }
138}
139
140#[get("/buildings/{building_id}/convocations")]
141pub async fn list_building_convocations(
142 state: web::Data<AppState>,
143 building_id: web::Path<Uuid>,
144 user: AuthenticatedUser,
145) -> impl Responder {
146 if let Err(err) =
150 crate::infrastructure::web::middleware::scope_guard::verify_building_org_access(
151 &user,
152 *building_id,
153 &state.building_use_cases,
154 &state.acp_use_cases,
155 )
156 .await
157 {
158 return err.error_response();
159 }
160
161 match state
162 .convocation_use_cases
163 .list_building_convocations(*building_id)
164 .await
165 {
166 Ok(convocations) => HttpResponse::Ok().json(convocations),
167 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
168 }
169}
170
171#[get("/organizations/{organization_id}/convocations")]
172pub async fn list_organization_convocations(
173 state: web::Data<AppState>,
174 user: AuthenticatedUser,
175 organization_id: web::Path<Uuid>,
176) -> impl Responder {
177 if let Err(e) = user.verify_org_access(*organization_id) {
178 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
179 }
180 match state
181 .convocation_use_cases
182 .list_organization_convocations(*organization_id)
183 .await
184 {
185 Ok(convocations) => HttpResponse::Ok().json(convocations),
186 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
187 }
188}
189
190#[delete("/convocations/{id}")]
191pub async fn delete_convocation(
192 state: web::Data<AppState>,
193 user: AuthenticatedUser,
194 id: web::Path<Uuid>,
195) -> impl Responder {
196 let organization_id = match user.require_organization() {
197 Ok(org_id) => org_id,
198 Err(e) => {
199 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
200 }
201 };
202
203 match state.convocation_use_cases.delete_convocation(*id).await {
204 Ok(true) => {
205 AuditLogEntry::new(
206 AuditEventType::ConvocationDeleted,
207 Some(user.user_id),
208 Some(organization_id),
209 )
210 .with_resource("Convocation", *id)
211 .log();
212
213 HttpResponse::NoContent().finish()
214 }
215 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
216 "error": "Convocation not found"
217 })),
218 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
219 }
220}
221
222#[put("/convocations/{id}/schedule")]
225pub async fn schedule_convocation(
226 state: web::Data<AppState>,
227 user: AuthenticatedUser,
228 id: web::Path<Uuid>,
229 request: web::Json<ScheduleConvocationRequest>,
230) -> impl Responder {
231 let organization_id = match user.require_organization() {
232 Ok(org_id) => org_id,
233 Err(e) => {
234 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
235 }
236 };
237
238 match state
239 .convocation_use_cases
240 .schedule_convocation(*id, request.into_inner())
241 .await
242 {
243 Ok(convocation) => {
244 AuditLogEntry::new(
245 AuditEventType::ConvocationScheduled,
246 Some(user.user_id),
247 Some(organization_id),
248 )
249 .with_resource("Convocation", convocation.id)
250 .log();
251
252 HttpResponse::Ok().json(convocation)
253 }
254 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
255 }
256}
257
258#[post("/convocations/{id}/send")]
259pub async fn send_convocation(
260 state: web::Data<AppState>,
261 user: AuthenticatedUser,
262 id: web::Path<Uuid>,
263 request: web::Json<SendConvocationRequest>,
264) -> impl Responder {
265 let organization_id = match user.require_organization() {
266 Ok(org_id) => org_id,
267 Err(e) => {
268 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
269 }
270 };
271
272 if let Err(err) = require_syndic_or_superadmin(&user) {
273 return err.error_response();
274 }
275
276 match state
278 .convocation_use_cases
279 .send_convocation(*id, request.into_inner())
280 .await
281 {
282 Ok(convocation) => {
283 AuditLogEntry::new(
284 AuditEventType::ConvocationSent,
285 Some(user.user_id),
286 Some(organization_id),
287 )
288 .with_resource("Convocation", convocation.id)
289 .with_details(format!("recipients: {}", convocation.total_recipients))
290 .log();
291
292 HttpResponse::Ok().json(convocation)
293 }
294 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
295 }
296}
297
298#[put("/convocations/{id}/cancel")]
299pub async fn cancel_convocation(
300 state: web::Data<AppState>,
301 user: AuthenticatedUser,
302 id: web::Path<Uuid>,
303) -> impl Responder {
304 let organization_id = match user.require_organization() {
305 Ok(org_id) => org_id,
306 Err(e) => {
307 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
308 }
309 };
310
311 match state.convocation_use_cases.cancel_convocation(*id).await {
312 Ok(convocation) => {
313 AuditLogEntry::new(
314 AuditEventType::ConvocationCancelled,
315 Some(user.user_id),
316 Some(organization_id),
317 )
318 .with_resource("Convocation", convocation.id)
319 .log();
320
321 HttpResponse::Ok().json(convocation)
322 }
323 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
324 }
325}
326
327#[get("/convocations/{id}/recipients")]
330pub async fn list_convocation_recipients(
331 state: web::Data<AppState>,
332 id: web::Path<Uuid>,
333 user: AuthenticatedUser,
334) -> impl Responder {
335 if let Err(err) =
338 crate::infrastructure::web::middleware::scope_guard::verify_convocation_org_access(
339 &user,
340 *id,
341 &state.convocation_use_cases,
342 &state.building_use_cases,
343 &state.acp_use_cases,
344 )
345 .await
346 {
347 return err.error_response();
348 }
349
350 match state
351 .convocation_use_cases
352 .list_convocation_recipients(*id)
353 .await
354 {
355 Ok(recipients) => HttpResponse::Ok().json(recipients),
356 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
357 }
358}
359
360#[utoipa::path(
366 get,
367 path = "/buildings/{building_id}/eligible-convocation-recipients",
368 tag = "Convocations",
369 summary = "Copropriétaires éligibles à une convocation",
370 params(("building_id" = Uuid, Path, description = "UUID de l'immeuble")),
371 responses(
372 (status = 200, description = "Destinataires éligibles", body = Vec<crate::application::dto::convocation_dto::EligibleRecipientResponse>),
373 (status = 403, description = "Hors portée"),
374 ),
375 security(("bearer_auth" = []))
376)]
377#[get("/buildings/{building_id}/eligible-convocation-recipients")]
378pub async fn list_eligible_convocation_recipients(
379 state: web::Data<AppState>,
380 user: AuthenticatedUser,
381 building_id: web::Path<Uuid>,
382) -> impl Responder {
383 if let Err(err) = verify_building_org_access(
384 &user,
385 *building_id,
386 &state.building_use_cases,
387 &state.acp_use_cases,
388 )
389 .await
390 {
391 return err.error_response();
392 }
393
394 match state
395 .convocation_use_cases
396 .list_eligible_recipients(*building_id)
397 .await
398 {
399 Ok(destinataires) => HttpResponse::Ok().json(destinataires),
400 Err(err) => err.error_response(),
401 }
402}
403
404#[get("/convocations/{id}/tracking-summary")]
405pub async fn get_convocation_tracking_summary(
406 state: web::Data<AppState>,
407 id: web::Path<Uuid>,
408 user: AuthenticatedUser,
409) -> impl Responder {
410 if let Err(err) =
413 crate::infrastructure::web::middleware::scope_guard::verify_convocation_org_access(
414 &user,
415 *id,
416 &state.convocation_use_cases,
417 &state.building_use_cases,
418 &state.acp_use_cases,
419 )
420 .await
421 {
422 return err.error_response();
423 }
424
425 match state.convocation_use_cases.get_tracking_summary(*id).await {
426 Ok(summary) => HttpResponse::Ok().json(summary),
427 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
428 }
429}
430
431#[put("/convocation-recipients/{id}/email-opened")]
432pub async fn mark_recipient_email_opened(
433 state: web::Data<AppState>,
434 id: web::Path<Uuid>,
435) -> impl Responder {
436 match state
437 .convocation_use_cases
438 .mark_recipient_email_opened(*id)
439 .await
440 {
441 Ok(recipient) => HttpResponse::Ok().json(recipient),
442 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
443 }
444}
445
446#[put("/convocation-recipients/{id}/attendance")]
447pub async fn update_recipient_attendance(
448 state: web::Data<AppState>,
449 user: AuthenticatedUser,
450 id: web::Path<Uuid>,
451 request: web::Json<UpdateAttendanceRequest>,
452) -> impl Responder {
453 let organization_id = match user.require_organization() {
454 Ok(org_id) => org_id,
455 Err(e) => {
456 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
457 }
458 };
459
460 match state
461 .convocation_use_cases
462 .update_recipient_attendance(*id, request.attendance_status.clone())
463 .await
464 {
465 Ok(recipient) => {
466 AuditLogEntry::new(
467 AuditEventType::ConvocationAttendanceUpdated,
468 Some(user.user_id),
469 Some(organization_id),
470 )
471 .with_resource("ConvocationRecipient", recipient.id)
472 .with_details(format!("status: {:?}", recipient.attendance_status))
473 .log();
474
475 HttpResponse::Ok().json(recipient)
476 }
477 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
478 }
479}
480
481#[put("/convocation-recipients/{id}/proxy")]
482pub async fn set_recipient_proxy(
483 state: web::Data<AppState>,
484 user: AuthenticatedUser,
485 id: web::Path<Uuid>,
486 request: web::Json<SetProxyRequest>,
487) -> impl Responder {
488 let organization_id = match user.require_organization() {
489 Ok(org_id) => org_id,
490 Err(e) => {
491 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
492 }
493 };
494
495 match state
496 .convocation_use_cases
497 .set_recipient_proxy(*id, request.proxy_owner_id)
498 .await
499 {
500 Ok(recipient) => {
501 AuditLogEntry::new(
502 AuditEventType::ConvocationProxySet,
503 Some(user.user_id),
504 Some(organization_id),
505 )
506 .with_resource("ConvocationRecipient", recipient.id)
507 .with_details(format!("proxy_owner_id: {:?}", recipient.proxy_owner_id))
508 .log();
509
510 HttpResponse::Ok().json(recipient)
511 }
512 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
513 }
514}
515
516#[post("/convocations/{id}/reminders")]
517pub async fn send_convocation_reminders(
518 state: web::Data<AppState>,
519 user: AuthenticatedUser,
520 id: web::Path<Uuid>,
521) -> impl Responder {
522 let organization_id = match user.require_organization() {
523 Ok(org_id) => org_id,
524 Err(e) => {
525 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
526 }
527 };
528
529 match state.convocation_use_cases.send_reminders(*id).await {
530 Ok(recipients) => {
531 AuditLogEntry::new(
532 AuditEventType::ConvocationReminderSent,
533 Some(user.user_id),
534 Some(organization_id),
535 )
536 .with_resource("Convocation", *id)
537 .with_details(format!("recipients: {}", recipients.len()))
538 .log();
539
540 HttpResponse::Ok().json(recipients)
541 }
542 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
543 }
544}
545
546#[post("/convocations/second")]
549pub async fn schedule_second_convocation(
550 state: web::Data<AppState>,
551 user: AuthenticatedUser,
552 request: web::Json<ScheduleSecondConvocationRequest>,
553) -> impl Responder {
554 let organization_id = match user.require_organization() {
555 Ok(org_id) => org_id,
556 Err(e) => {
557 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
558 }
559 };
560
561 let req = request.into_inner();
562
563 if let Err(err) = verify_building_org_access(
567 &user,
568 req.building_id,
569 &state.building_use_cases,
570 &state.acp_use_cases,
571 )
572 .await
573 {
574 return err.error_response();
575 }
576
577 let new_meeting_req = crate::application::dto::CreateMeetingRequest {
579 organization_id,
580 building_id: req.building_id,
581 meeting_type: crate::domain::entities::MeetingType::Ordinary,
582 title: format!("Second Convocation (Art. 3.87 §5 CC)"),
583 description: Some("Second convocation after quorum not reached".to_string()),
584 scheduled_date: req.new_meeting_date,
585 location: "Same as first meeting".to_string(),
586 is_second_convocation: true,
587 };
588
589 let new_meeting = match state
591 .meeting_use_cases
592 .create_meeting(new_meeting_req)
593 .await
594 {
595 Ok(m) => m,
596 Err(err) => {
597 return HttpResponse::BadRequest()
598 .json(serde_json::json!({"error": format!("Failed to create meeting: {}", err)}))
599 }
600 };
601
602 match state
603 .convocation_use_cases
604 .schedule_second_convocation(
605 organization_id,
606 req.building_id,
607 req.first_meeting_id,
608 new_meeting.id,
609 req.new_meeting_date,
610 req.language,
611 user.user_id,
612 )
613 .await
614 {
615 Ok(convocation) => {
616 AuditLogEntry::new(
617 AuditEventType::SecondConvocationScheduled,
618 Some(user.user_id),
619 Some(organization_id),
620 )
621 .with_resource("Convocation", convocation.id)
622 .with_details(format!(
623 "first_meeting_id: {}, new_meeting_id: {}",
624 req.first_meeting_id, new_meeting.id
625 ))
626 .log();
627
628 HttpResponse::Created().json(convocation)
629 }
630 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
631 }
632}