Skip to main content

koprogo_api/infrastructure/web/handlers/
ag_session_handlers.rs

1use crate::application::dto::ag_session_dto::{
2    CreateAgSessionDto, EndAgSessionDto, RecordRemoteJoinDto,
3};
4use crate::infrastructure::web::classification_erreurs;
5use crate::infrastructure::web::{AppState, AuthenticatedUser};
6use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
7use rust_decimal::Decimal;
8use uuid::Uuid;
9
10/// POST /meetings/:id/ag-session — Crée une session visio pour une réunion
11#[post("/meetings/{meeting_id}/ag-session")]
12pub async fn create_ag_session(
13    state: web::Data<AppState>,
14    user: AuthenticatedUser,
15    path: web::Path<Uuid>,
16    body: web::Json<CreateAgSessionDto>,
17) -> impl Responder {
18    let organization_id = match user.require_organization() {
19        Ok(id) => id,
20        Err(e) => {
21            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
22        }
23    };
24
25    let mut dto = body.into_inner();
26    dto.meeting_id = path.into_inner();
27
28    match state
29        .ag_session_use_cases
30        .create_session(organization_id, dto, user.user_id)
31        .await
32    {
33        Ok(session) => HttpResponse::Created().json(session),
34        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
35    }
36}
37
38/// GET /meetings/:id/ag-session — Récupère la session visio d'une réunion
39#[get("/meetings/{meeting_id}/ag-session")]
40pub async fn get_ag_session_for_meeting(
41    state: web::Data<AppState>,
42    user: AuthenticatedUser,
43    path: web::Path<Uuid>,
44) -> impl Responder {
45    let organization_id = match user.require_organization() {
46        Ok(id) => id,
47        Err(e) => {
48            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
49        }
50    };
51
52    match state
53        .ag_session_use_cases
54        .get_session_for_meeting(path.into_inner(), organization_id)
55        .await
56    {
57        Ok(Some(session)) => HttpResponse::Ok().json(session),
58        Ok(None) => HttpResponse::NotFound()
59            .json(serde_json::json!({"error": "Aucune session visio pour cette réunion"})),
60        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
61    }
62}
63
64/// GET /ag-sessions — Liste les sessions de l'organisation
65#[get("/ag-sessions")]
66pub async fn list_ag_sessions(
67    state: web::Data<AppState>,
68    user: AuthenticatedUser,
69) -> impl Responder {
70    let organization_id = match user.require_organization() {
71        Ok(id) => id,
72        Err(e) => {
73            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
74        }
75    };
76
77    match state
78        .ag_session_use_cases
79        .list_sessions(organization_id)
80        .await
81    {
82        Ok(sessions) => HttpResponse::Ok().json(sessions),
83        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
84    }
85}
86
87/// GET /ag-sessions/:id — Récupère une session par son ID
88#[get("/ag-sessions/{id}")]
89pub async fn get_ag_session(
90    state: web::Data<AppState>,
91    user: AuthenticatedUser,
92    path: web::Path<Uuid>,
93) -> impl Responder {
94    let organization_id = match user.require_organization() {
95        Ok(id) => id,
96        Err(e) => {
97            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
98        }
99    };
100
101    match state
102        .ag_session_use_cases
103        .get_session(path.into_inner(), organization_id)
104        .await
105    {
106        Ok(session) => HttpResponse::Ok().json(session),
107        Err(e) => {
108            if classification_erreurs::est_introuvable(&e) {
109                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
110            } else if classification_erreurs::est_interdit(&e) {
111                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
112            } else {
113                HttpResponse::InternalServerError().json(serde_json::json!({"error": e}))
114            }
115        }
116    }
117}
118
119/// PUT /ag-sessions/:id/start — Démarre la session (Scheduled → Live)
120#[put("/ag-sessions/{id}/start")]
121pub async fn start_ag_session(
122    state: web::Data<AppState>,
123    user: AuthenticatedUser,
124    path: web::Path<Uuid>,
125) -> impl Responder {
126    let organization_id = match user.require_organization() {
127        Ok(id) => id,
128        Err(e) => {
129            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
130        }
131    };
132
133    match state
134        .ag_session_use_cases
135        .start_session(path.into_inner(), organization_id)
136        .await
137    {
138        Ok(session) => HttpResponse::Ok().json(session),
139        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
140    }
141}
142
143/// PUT /ag-sessions/:id/end — Termine la session (Live → Ended)
144#[put("/ag-sessions/{id}/end")]
145pub async fn end_ag_session(
146    state: web::Data<AppState>,
147    user: AuthenticatedUser,
148    path: web::Path<Uuid>,
149    body: web::Json<EndAgSessionDto>,
150) -> impl Responder {
151    let organization_id = match user.require_organization() {
152        Ok(id) => id,
153        Err(e) => {
154            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
155        }
156    };
157
158    match state
159        .ag_session_use_cases
160        .end_session(path.into_inner(), organization_id, body.into_inner())
161        .await
162    {
163        Ok(session) => HttpResponse::Ok().json(session),
164        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
165    }
166}
167
168/// PUT /ag-sessions/:id/cancel — Annule la session (Scheduled → Cancelled)
169#[put("/ag-sessions/{id}/cancel")]
170pub async fn cancel_ag_session(
171    state: web::Data<AppState>,
172    user: AuthenticatedUser,
173    path: web::Path<Uuid>,
174) -> impl Responder {
175    let organization_id = match user.require_organization() {
176        Ok(id) => id,
177        Err(e) => {
178            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
179        }
180    };
181
182    match state
183        .ag_session_use_cases
184        .cancel_session(path.into_inner(), organization_id)
185        .await
186    {
187        Ok(session) => HttpResponse::Ok().json(session),
188        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
189    }
190}
191
192/// POST /ag-sessions/:id/join — Enregistre un participant distant
193#[post("/ag-sessions/{id}/join")]
194pub async fn record_remote_join(
195    state: web::Data<AppState>,
196    user: AuthenticatedUser,
197    path: web::Path<Uuid>,
198    body: web::Json<RecordRemoteJoinDto>,
199) -> impl Responder {
200    let organization_id = match user.require_organization() {
201        Ok(id) => id,
202        Err(e) => {
203            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
204        }
205    };
206
207    match state
208        .ag_session_use_cases
209        .record_remote_join(path.into_inner(), organization_id, body.into_inner())
210        .await
211    {
212        Ok(session) => HttpResponse::Ok().json(session),
213        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
214    }
215}
216
217/// GET /ag-sessions/:id/quorum — Calcule le quorum combiné (présentiel + distanciel)
218#[get("/ag-sessions/{id}/quorum")]
219pub async fn get_combined_quorum(
220    state: web::Data<AppState>,
221    user: AuthenticatedUser,
222    path: web::Path<Uuid>,
223    query: web::Query<CombinedQuorumQuery>,
224) -> impl Responder {
225    let organization_id = match user.require_organization() {
226        Ok(id) => id,
227        Err(e) => {
228            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
229        }
230    };
231
232    match state
233        .ag_session_use_cases
234        .calculate_combined_quorum(
235            path.into_inner(),
236            organization_id,
237            query.physical_quotas,
238            query.total_building_quotas,
239            query.physical_owners_count,
240            query.total_owners_count,
241        )
242        .await
243    {
244        Ok(result) => HttpResponse::Ok().json(result),
245        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
246    }
247}
248
249/// DELETE /ag-sessions/:id — Supprime une session annulée ou planifiée
250#[delete("/ag-sessions/{id}")]
251pub async fn delete_ag_session(
252    state: web::Data<AppState>,
253    user: AuthenticatedUser,
254    path: web::Path<Uuid>,
255) -> impl Responder {
256    let organization_id = match user.require_organization() {
257        Ok(id) => id,
258        Err(e) => {
259            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
260        }
261    };
262
263    match state
264        .ag_session_use_cases
265        .delete_session(path.into_inner(), organization_id)
266        .await
267    {
268        Ok(()) => HttpResponse::NoContent().finish(),
269        Err(e) => {
270            if classification_erreurs::est_introuvable(&e) {
271                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
272            } else if classification_erreurs::est_interdit(&e) {
273                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
274            } else {
275                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
276            }
277        }
278    }
279}
280
281/// GET /ag-sessions/platform-stats — Platform-wide AG session statistics (SuperAdmin only)
282/// Issue #274: AG Visioconférence Refinements
283#[get("/ag-sessions/platform-stats")]
284pub async fn get_ag_session_platform_stats(
285    state: web::Data<AppState>,
286    user: AuthenticatedUser,
287) -> impl Responder {
288    // Only superadmin can see platform-wide stats
289    //
290    // #661-audit : même bug de casse que dans `api_key_handlers` — le JWT rend
291    // "superadmin" en minuscules, la comparaison visait "SUPERADMIN".
292    // L'endpoint répondait 403 même à un superadmin.
293    if !user.role.eq_ignore_ascii_case("superadmin") {
294        return HttpResponse::Forbidden().json(serde_json::json!({
295            "error": "SuperAdmin only"
296        }));
297    }
298
299    // Platform stats - count pending sessions as a basic metric
300    match state.ag_session_use_cases.list_pending_sessions().await {
301        Ok(pending) => HttpResponse::Ok().json(serde_json::json!({
302            "pending_sessions_count": pending.len(),
303            "note": "Platform-wide statistics endpoint - detailed metrics coming soon"
304        })),
305        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
306    }
307}
308
309#[derive(serde::Deserialize)]
310pub struct CombinedQuorumQuery {
311    /// Quotes-parts présentes physiquement. `Decimal` : la valeur alimente
312    /// directement le seuil légal de quorum (Art. 3.87 §5 CC, ADR-0008).
313    pub physical_quotas: Decimal,
314    pub total_building_quotas: Decimal,
315    /// Volet « têtes » du quorum double (Art. 3.87 §5 CC, #661). Absents =
316    /// 0 : le volet têtes est alors non atteint, et le quorum ne peut être
317    /// déclaré que par l'alternative des 3/4 de quotités.
318    #[serde(default)]
319    pub physical_owners_count: i32,
320    #[serde(default)]
321    pub total_owners_count: i32,
322}