Skip to main content

koprogo_api/infrastructure/web/handlers/
etat_date_handlers.rs

1use crate::application::dto::{
2    CreateEtatDateRequest, PageRequest, PageResponse, UpdateEtatDateAdditionalDataRequest,
3    UpdateEtatDateFinancialRequest,
4};
5use crate::domain::entities::EtatDateStatus;
6use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
7use crate::infrastructure::web::handlers::conformity_response::try_build_conformity_response;
8use crate::infrastructure::web::middleware::scope_guard::verify_building_org_access;
9use crate::infrastructure::web::{AppState, AuthenticatedUser};
10use actix_web::{delete, get, post, put, web, HttpResponse, Responder, ResponseError};
11use serde::Deserialize;
12use uuid::Uuid;
13
14#[derive(Debug, Deserialize)]
15pub struct EtatDateListQuery {
16    #[serde(default = "default_page")]
17    pub page: i64,
18    #[serde(default = "default_per_page")]
19    pub per_page: i64,
20    pub status: Option<String>,
21}
22
23fn default_page() -> i64 {
24    1
25}
26fn default_per_page() -> i64 {
27    10
28}
29
30/// Cloisonne un état daté AVANT de le muter (#864).
31///
32/// ── Pourquoi `verify_` et pas un nom français ─────────────────────────────
33///
34/// Ce helper s'appelait `cloisonner_*` à sa première écriture. Le cliquet de
35/// #864 est resté à 95 : son détecteur cherche les idiomes par lesquels CE
36/// dépôt refuse un accès — `verify_`, `scope_guard`, `Forbidden`,
37/// `require_organization` — et `cloisonner_` n'en est pas un. Dix trous
38/// venaient d'être bouchés, et l'instrument ne le voyait pas.
39///
40/// Deux sorties possibles : allonger la liste du détecteur, ou porter le nom
41/// que le dépôt emploie déjà (`verify_org_access`, `verify_acp_org_access`,
42/// `verify_building_org_access`). La première aurait fait tomber le compteur
43/// de dix par une modification de sa DÉFINITION, ce qui est précisément le
44/// geste que la méthode interdit. La seconde corrige une incohérence de
45/// nommage que je venais d'introduire, et la dette tombe à 85 parce que le
46/// travail a été fait.
47///
48/// ── Ce que ces cinq routes laissaient passer ──────────────────────────────
49///
50/// `mark_in_progress`, `mark_generated`, `mark_delivered`,
51/// `update_financial_data` et `update_additional_data` prenaient
52/// `AuthenticatedUser` et ne s'en servaient que pour journaliser après coup.
53/// `get_etat_date`, dans ce même fichier, cloisonne correctement : le
54/// contrôle existait, il manquait sur les écritures.
55///
56/// ── Pourquoi c'est plus lourd qu'un budget ────────────────────────────────
57///
58/// L'état daté est une pièce **légale** : Art. 3.89 § 5, l'information que le
59/// syndic doit au notaire lors d'une mutation. Marquer « délivré » un état
60/// daté d'un autre cabinet fait courir les délais sur un dossier qu'on ne
61/// gère pas ; en altérer les données financières fausse le décompte des
62/// arriérés que l'acquéreur reprend.
63///
64/// Rend `Some(réponse)` quand l'appel doit être refusé, `None` sinon.
65async fn verify_etat_date_org_access(
66    state: &web::Data<AppState>,
67    user: &AuthenticatedUser,
68    id: Uuid,
69) -> Option<HttpResponse> {
70    match state.etat_date_use_cases.get_etat_date(id).await {
71        Ok(Some(etat_date)) => match user.verify_org_access(etat_date.organization_id) {
72            Ok(()) => None,
73            Err(err) => Some(HttpResponse::Forbidden().json(serde_json::json!({ "error": err }))),
74        },
75        Ok(None) => Some(HttpResponse::NotFound().json(serde_json::json!({
76            "error": "État daté not found"
77        }))),
78        Err(err) => Some(HttpResponse::InternalServerError().json(serde_json::json!({
79            "error": err
80        }))),
81    }
82}
83
84/// Create a new état daté request
85#[post("/etats-dates")]
86pub async fn create_etat_date(
87    state: web::Data<AppState>,
88    user: AuthenticatedUser,
89    mut request: web::Json<CreateEtatDateRequest>,
90) -> impl Responder {
91    // Override organization_id from JWT token (security)
92    let organization_id = match user.require_organization() {
93        Ok(org_id) => org_id,
94        Err(e) => {
95            return HttpResponse::Unauthorized().json(serde_json::json!({
96                "error": e.to_string()
97            }))
98        }
99    };
100    request.organization_id = organization_id;
101
102    // Isolation multi-tenant à l'ÉCRITURE : l'immeuble visé doit relever d'une
103    // ACP dont ce syndic a la gestion. L'affectation de `organization_id`
104    // ci-dessus protège le mauvais champ — elle empêche d'estampiller
105    // l'enregistrement au nom d'autrui, pas de le rattacher au patrimoine
106    // d'autrui (audit du 2026-09-02).
107    if let Err(err) = verify_building_org_access(
108        &user,
109        request.building_id,
110        &state.building_use_cases,
111        &state.acp_use_cases,
112    )
113    .await
114    {
115        return err.error_response();
116    }
117
118    match state
119        .etat_date_use_cases
120        .create_etat_date(request.into_inner())
121        .await
122    {
123        Ok(etat_date) => {
124            AuditLogEntry::new(
125                AuditEventType::EtatDateCreated,
126                Some(user.user_id),
127                Some(organization_id),
128            )
129            .with_resource("EtatDate", etat_date.id)
130            .log();
131
132            HttpResponse::Created().json(etat_date)
133        }
134        Err(err) => {
135            AuditLogEntry::new(
136                AuditEventType::EtatDateCreated,
137                Some(user.user_id),
138                Some(organization_id),
139            )
140            .with_error(err.clone())
141            .log();
142
143            // Track H Story H2 — pre-check validate-before-compute → 422 narratif
144            if let Some(resp) = try_build_conformity_response(&err) {
145                return resp;
146            }
147            HttpResponse::BadRequest().json(serde_json::json!({
148                "error": err
149            }))
150        }
151    }
152}
153
154/// Get état daté by ID
155#[get("/etats-dates/{id}")]
156pub async fn get_etat_date(
157    state: web::Data<AppState>,
158    user: AuthenticatedUser,
159    id: web::Path<Uuid>,
160) -> impl Responder {
161    match state.etat_date_use_cases.get_etat_date(*id).await {
162        Ok(Some(etat_date)) => {
163            // Verify organization access
164            if let Err(err) = user.verify_org_access(etat_date.organization_id) {
165                return HttpResponse::Forbidden().json(serde_json::json!({"error": err}));
166            }
167            HttpResponse::Ok().json(etat_date)
168        }
169        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
170            "error": "État daté not found"
171        })),
172        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
173            "error": err
174        })),
175    }
176}
177
178#[derive(Debug, Deserialize)]
179pub struct NotaryLinkTokenQuery {
180    pub token: Option<String>,
181}
182
183/// Get état daté by reference number — **derrière un lien notaire** (#845,
184/// ADR 0048, ADR 0051).
185///
186/// ── Ce que cette route servait avant ──────────────────────────────────────
187///
188/// Aucune identité : ni `AuthenticatedUser`, ni jeton lu à la main. Un état
189/// daté porte les dettes d'un copropriétaire nommé, et sa référence n'est pas
190/// un secret — elle circule dans des courriels, des dossiers de vente. Le
191/// seul obstacle pour lire n'importe quel état daté était de connaître une
192/// référence devinable. Dernière des 30 routes nues relevées par
193/// `garde_identite_absente` (#845).
194///
195/// ── Ce qu'elle sert maintenant ────────────────────────────────────────────
196///
197/// Le notaire présente `?token=<jeton>`, émis par le syndic via
198/// `POST /etats-dates/{id}/notary-link`. `verify_token` (ci-dessous) vérifie
199/// que ce jeton précis ouvre CET état daté, n'est ni expiré ni révoqué. Le
200/// jeton EST l'identité — même idiome que `/c/{token}` (liens magiques),
201/// mais multi-lecture et révocable/renouvelable (ADR 0051), donc gardé
202/// nommément plutôt que rejoint la liste `PUBLIQUES` de la garde.
203#[get("/etats-dates/reference/{reference_number}")]
204pub async fn get_by_reference_number(
205    state: web::Data<AppState>,
206    reference_number: web::Path<String>,
207    query: web::Query<NotaryLinkTokenQuery>,
208) -> impl Responder {
209    let etat_date = match state
210        .etat_date_use_cases
211        .get_by_reference_number(&reference_number)
212        .await
213    {
214        Ok(Some(etat_date)) => etat_date,
215        Ok(None) => {
216            return HttpResponse::NotFound().json(serde_json::json!({
217                "error": "État daté not found"
218            }))
219        }
220        Err(err) => {
221            return HttpResponse::InternalServerError().json(serde_json::json!({
222                "error": err
223            }))
224        }
225    };
226
227    let token = query.token.clone().unwrap_or_default();
228    let lien = match state
229        .lien_notaire_use_cases
230        .verify_token(etat_date.id, &token)
231        .await
232    {
233        Ok(lien) => lien,
234        Err(err) => return err.error_response(),
235    };
236
237    // Consultation anonyme : le lecteur est un notaire sans compte
238    // KoproGo, donc `user_id: None` — la traçabilité passe par
239    // `lien_notaire_id` (cf. ADR 0051, question laissée ouverte sur le
240    // `subject_user_id` d'un notaire sans compte).
241    AuditLogEntry::new(
242        AuditEventType::NotaryLinkConsulted,
243        None,
244        Some(etat_date.organization_id),
245    )
246    .with_resource("EtatDate", etat_date.id)
247    .with_metadata(serde_json::json!({ "lien_notaire_id": lien.id }))
248    .log();
249
250    HttpResponse::Ok().json(etat_date)
251}
252
253/// Émettre un lien notaire pour un état daté (#845 — ADR 0051).
254///
255/// Cloisonné comme les autres écritures de ce fichier (#864) : le syndic doit
256/// avoir la gestion de l'ACP dont relève l'état daté.
257#[utoipa::path(
258    post,
259    path = "/etats-dates/{id}/notary-link",
260    tag = "EtatsDates",
261    summary = "Émettre un lien notaire pour un état daté",
262    params(("id" = Uuid, Path, description = "UUID de l'état daté")),
263    responses(
264        (status = 201, description = "Lien émis — le jeton en clair n'est rendu QU'ICI", body = crate::application::use_cases::lien_notaire_use_cases::IssuedLienNotaireDto),
265        (status = 403, description = "Hors portée"),
266        (status = 404, description = "État daté introuvable"),
267    ),
268    security(("bearer_auth" = []))
269)]
270#[post("/etats-dates/{id}/notary-link")]
271pub async fn issue_notary_link(
272    state: web::Data<AppState>,
273    user: AuthenticatedUser,
274    id: web::Path<Uuid>,
275) -> impl Responder {
276    if let Some(refus) = verify_etat_date_org_access(&state, &user, *id).await {
277        return refus;
278    }
279
280    match state.lien_notaire_use_cases.issue(*id, user.user_id).await {
281        Ok(issued) => {
282            AuditLogEntry::new(
283                AuditEventType::NotaryLinkIssued,
284                Some(user.user_id),
285                user.organization_id,
286            )
287            .with_resource("EtatDate", *id)
288            .log();
289
290            HttpResponse::Created().json(issued)
291        }
292        Err(err) => err.error_response(),
293    }
294}
295
296/// Renouveler le lien notaire actif d'un état daté — sept jours de plus à
297/// partir de maintenant, même jeton (#845 — ADR 0051).
298#[utoipa::path(
299    put,
300    path = "/etats-dates/{id}/notary-link/renew",
301    tag = "EtatsDates",
302    summary = "Prolonger un lien notaire existant",
303    params(("id" = Uuid, Path, description = "UUID de l'état daté")),
304    responses(
305        (status = 200, description = "Lien prolongé", body = crate::application::use_cases::lien_notaire_use_cases::LienNotaireStatusDto),
306        (status = 403, description = "Hors portée"),
307        (status = 404, description = "Lien introuvable"),
308    ),
309    security(("bearer_auth" = []))
310)]
311#[put("/etats-dates/{id}/notary-link/renew")]
312pub async fn renew_notary_link(
313    state: web::Data<AppState>,
314    user: AuthenticatedUser,
315    id: web::Path<Uuid>,
316) -> impl Responder {
317    if let Some(refus) = verify_etat_date_org_access(&state, &user, *id).await {
318        return refus;
319    }
320
321    match state.lien_notaire_use_cases.renew(*id).await {
322        Ok(status) => {
323            AuditLogEntry::new(
324                AuditEventType::NotaryLinkRenewed,
325                Some(user.user_id),
326                user.organization_id,
327            )
328            .with_resource("EtatDate", *id)
329            .log();
330
331            HttpResponse::Ok().json(status)
332        }
333        Err(err) => err.error_response(),
334    }
335}
336
337/// Révoquer le lien notaire actif d'un état daté avant terme (#845 — ADR 0051).
338#[utoipa::path(
339    delete,
340    path = "/etats-dates/{id}/notary-link",
341    tag = "EtatsDates",
342    summary = "Révoquer un lien notaire",
343    params(("id" = Uuid, Path, description = "UUID de l'état daté")),
344    responses(
345        (status = 204, description = "Lien révoqué"),
346        (status = 403, description = "Hors portée"),
347        (status = 404, description = "Lien introuvable"),
348    ),
349    security(("bearer_auth" = []))
350)]
351#[delete("/etats-dates/{id}/notary-link")]
352pub async fn revoke_notary_link(
353    state: web::Data<AppState>,
354    user: AuthenticatedUser,
355    id: web::Path<Uuid>,
356) -> impl Responder {
357    if let Some(refus) = verify_etat_date_org_access(&state, &user, *id).await {
358        return refus;
359    }
360
361    match state.lien_notaire_use_cases.revoke(*id, user.user_id).await {
362        Ok(()) => {
363            AuditLogEntry::new(
364                AuditEventType::NotaryLinkRevoked,
365                Some(user.user_id),
366                user.organization_id,
367            )
368            .with_resource("EtatDate", *id)
369            .log();
370
371            HttpResponse::NoContent().finish()
372        }
373        Err(err) => err.error_response(),
374    }
375}
376
377/// List états datés paginated
378#[get("/etats-dates")]
379pub async fn list_etats_dates(
380    state: web::Data<AppState>,
381    user: AuthenticatedUser,
382    query: web::Query<EtatDateListQuery>,
383) -> impl Responder {
384    let organization_id = user.organization_id;
385
386    // Parse status filter
387    let status = query.status.as_ref().and_then(|s| match s.as_str() {
388        "requested" => Some(EtatDateStatus::Requested),
389        "in_progress" => Some(EtatDateStatus::InProgress),
390        "generated" => Some(EtatDateStatus::Generated),
391        "delivered" => Some(EtatDateStatus::Delivered),
392        "expired" => Some(EtatDateStatus::Expired),
393        _ => None,
394    });
395
396    let page_request = PageRequest {
397        page: query.page,
398        per_page: query.per_page,
399        sort_by: None,
400        order: Default::default(),
401    };
402
403    match state
404        .etat_date_use_cases
405        .list_paginated(&page_request, organization_id, status)
406        .await
407    {
408        Ok((etats, total)) => {
409            let response =
410                PageResponse::new(etats, page_request.page, page_request.per_page, total);
411            HttpResponse::Ok().json(response)
412        }
413        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
414            "error": err
415        })),
416    }
417}
418
419/// List états datés by unit
420#[get("/units/{unit_id}/etats-dates")]
421pub async fn list_etats_dates_by_unit(
422    state: web::Data<AppState>,
423    unit_id: web::Path<Uuid>,
424    user: AuthenticatedUser,
425) -> impl Responder {
426    // Route imbriquee non gardee au releve du 2026-09-06 (issue #772).
427    if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_unit_org_access(
428        &user,
429        *unit_id,
430        &state.unit_use_cases,
431        &state.building_use_cases,
432        &state.acp_use_cases,
433    )
434    .await
435    {
436        return err.error_response();
437    }
438
439    match state.etat_date_use_cases.list_by_unit(*unit_id).await {
440        Ok(etats) => HttpResponse::Ok().json(etats),
441        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
442            "error": err
443        })),
444    }
445}
446
447/// List états datés by building
448#[get("/buildings/{building_id}/etats-dates")]
449pub async fn list_etats_dates_by_building(
450    state: web::Data<AppState>,
451    building_id: web::Path<Uuid>,
452    user: AuthenticatedUser,
453) -> impl Responder {
454    // Route imbriquee non gardee au releve du 2026-09-06 (issue #772) : elle
455    // servait une sous-collection d'un dossier d'ACP a quiconque connaissait
456    // un identifiant, sans demander d'identite.
457    if let Err(err) =
458        crate::infrastructure::web::middleware::scope_guard::verify_building_org_access(
459            &user,
460            *building_id,
461            &state.building_use_cases,
462            &state.acp_use_cases,
463        )
464        .await
465    {
466        return err.error_response();
467    }
468
469    match state
470        .etat_date_use_cases
471        .list_by_building(*building_id)
472        .await
473    {
474        Ok(etats) => HttpResponse::Ok().json(etats),
475        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
476            "error": err
477        })),
478    }
479}
480
481/// Mark état daté as in progress
482#[put("/etats-dates/{id}/mark-in-progress")]
483pub async fn mark_in_progress(
484    state: web::Data<AppState>,
485    user: AuthenticatedUser,
486    id: web::Path<Uuid>,
487) -> impl Responder {
488    // Cloisonnement AVANT la mutation (#864).
489    if let Some(refus) = verify_etat_date_org_access(&state, &user, *id).await {
490        return refus;
491    }
492
493    match state.etat_date_use_cases.mark_in_progress(*id).await {
494        Ok(etat_date) => {
495            AuditLogEntry::new(
496                AuditEventType::EtatDateInProgress,
497                Some(user.user_id),
498                user.organization_id,
499            )
500            .with_resource("EtatDate", etat_date.id)
501            .log();
502
503            HttpResponse::Ok().json(etat_date)
504        }
505        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
506            "error": err
507        })),
508    }
509}
510
511/// Mark état daté as generated (with PDF file path)
512#[put("/etats-dates/{id}/mark-generated")]
513pub async fn mark_generated(
514    state: web::Data<AppState>,
515    user: AuthenticatedUser,
516    id: web::Path<Uuid>,
517    pdf_path: web::Json<serde_json::Value>,
518) -> impl Responder {
519    // Cloisonnement AVANT la mutation (#864).
520    if let Some(refus) = verify_etat_date_org_access(&state, &user, *id).await {
521        return refus;
522    }
523
524    let pdf_file_path = match pdf_path.get("pdf_file_path") {
525        Some(serde_json::Value::String(path)) => path.clone(),
526        _ => {
527            return HttpResponse::BadRequest().json(serde_json::json!({
528                "error": "pdf_file_path is required as a string"
529            }))
530        }
531    };
532
533    match state
534        .etat_date_use_cases
535        .mark_generated(*id, pdf_file_path)
536        .await
537    {
538        Ok(etat_date) => {
539            AuditLogEntry::new(
540                AuditEventType::EtatDateGenerated,
541                Some(user.user_id),
542                user.organization_id,
543            )
544            .with_resource("EtatDate", etat_date.id)
545            .log();
546
547            HttpResponse::Ok().json(etat_date)
548        }
549        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
550            "error": err
551        })),
552    }
553}
554
555/// Mark état daté as delivered to notary
556#[put("/etats-dates/{id}/mark-delivered")]
557pub async fn mark_delivered(
558    state: web::Data<AppState>,
559    user: AuthenticatedUser,
560    id: web::Path<Uuid>,
561) -> impl Responder {
562    // Cloisonnement AVANT la mutation (#864).
563    if let Some(refus) = verify_etat_date_org_access(&state, &user, *id).await {
564        return refus;
565    }
566
567    match state.etat_date_use_cases.mark_delivered(*id).await {
568        Ok(etat_date) => {
569            AuditLogEntry::new(
570                AuditEventType::EtatDateDelivered,
571                Some(user.user_id),
572                user.organization_id,
573            )
574            .with_resource("EtatDate", etat_date.id)
575            .log();
576
577            HttpResponse::Ok().json(etat_date)
578        }
579        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
580            "error": err
581        })),
582    }
583}
584
585/// Update financial data
586#[put("/etats-dates/{id}/financial")]
587pub async fn update_financial_data(
588    state: web::Data<AppState>,
589    user: AuthenticatedUser,
590    id: web::Path<Uuid>,
591    request: web::Json<UpdateEtatDateFinancialRequest>,
592) -> impl Responder {
593    // Cloisonnement AVANT la mutation (#864).
594    if let Some(refus) = verify_etat_date_org_access(&state, &user, *id).await {
595        return refus;
596    }
597
598    match state
599        .etat_date_use_cases
600        .update_financial_data(*id, request.into_inner())
601        .await
602    {
603        Ok(etat_date) => {
604            AuditLogEntry::new(
605                AuditEventType::EtatDateFinancialUpdate,
606                Some(user.user_id),
607                user.organization_id,
608            )
609            .with_resource("EtatDate", etat_date.id)
610            .log();
611
612            HttpResponse::Ok().json(etat_date)
613        }
614        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
615            "error": err
616        })),
617    }
618}
619
620/// Update additional data (sections 7-16)
621#[put("/etats-dates/{id}/additional-data")]
622pub async fn update_additional_data(
623    state: web::Data<AppState>,
624    user: AuthenticatedUser,
625    id: web::Path<Uuid>,
626    request: web::Json<UpdateEtatDateAdditionalDataRequest>,
627) -> impl Responder {
628    // Cloisonnement AVANT la mutation (#864).
629    if let Some(refus) = verify_etat_date_org_access(&state, &user, *id).await {
630        return refus;
631    }
632
633    match state
634        .etat_date_use_cases
635        .update_additional_data(*id, request.into_inner())
636        .await
637    {
638        Ok(etat_date) => {
639            AuditLogEntry::new(
640                AuditEventType::EtatDateAdditionalDataUpdate,
641                Some(user.user_id),
642                user.organization_id,
643            )
644            .with_resource("EtatDate", etat_date.id)
645            .log();
646
647            HttpResponse::Ok().json(etat_date)
648        }
649        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
650            "error": err
651        })),
652    }
653}
654
655/// List overdue états datés (>10 days, not generated yet)
656#[get("/etats-dates/overdue")]
657pub async fn list_overdue(state: web::Data<AppState>, user: AuthenticatedUser) -> impl Responder {
658    let organization_id = match user.require_organization() {
659        Ok(org_id) => org_id,
660        Err(e) => {
661            return HttpResponse::Unauthorized().json(serde_json::json!({
662                "error": e.to_string()
663            }))
664        }
665    };
666
667    match state
668        .etat_date_use_cases
669        .list_overdue(organization_id)
670        .await
671    {
672        Ok(etats) => HttpResponse::Ok().json(etats),
673        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
674            "error": err
675        })),
676    }
677}
678
679/// List expired états datés (>3 months from reference date)
680#[get("/etats-dates/expired")]
681pub async fn list_expired(state: web::Data<AppState>, user: AuthenticatedUser) -> impl Responder {
682    let organization_id = match user.require_organization() {
683        Ok(org_id) => org_id,
684        Err(e) => {
685            return HttpResponse::Unauthorized().json(serde_json::json!({
686                "error": e.to_string()
687            }))
688        }
689    };
690
691    match state
692        .etat_date_use_cases
693        .list_expired(organization_id)
694        .await
695    {
696        Ok(etats) => HttpResponse::Ok().json(etats),
697        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
698            "error": err
699        })),
700    }
701}
702
703/// Get statistics for dashboard
704#[get("/etats-dates/stats")]
705pub async fn get_stats(state: web::Data<AppState>, user: AuthenticatedUser) -> impl Responder {
706    let organization_id = match user.require_organization() {
707        Ok(org_id) => org_id,
708        Err(e) => {
709            return HttpResponse::Unauthorized().json(serde_json::json!({
710                "error": e.to_string()
711            }))
712        }
713    };
714
715    match state.etat_date_use_cases.get_stats(organization_id).await {
716        Ok(stats) => HttpResponse::Ok().json(stats),
717        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
718            "error": err
719        })),
720    }
721}
722
723/// Delete état daté
724#[delete("/etats-dates/{id}")]
725pub async fn delete_etat_date(
726    state: web::Data<AppState>,
727    user: AuthenticatedUser,
728    id: web::Path<Uuid>,
729) -> impl Responder {
730    // Cloisonnement AVANT la suppression (#864).
731    //
732    // Un état daté est la pièce qu'un notaire réclame à la vente. L'effacer
733    // n'était soumis à aucun contrôle d'organisation, alors que le LIRE l'était
734    // déjà (`get_etat_date`, même fichier).
735    match state.etat_date_use_cases.get_etat_date(*id).await {
736        Ok(Some(etat_date)) => {
737            if let Err(err) = user.verify_org_access(etat_date.organization_id) {
738                return HttpResponse::Forbidden().json(serde_json::json!({ "error": err }));
739            }
740        }
741        Ok(None) => {
742            return HttpResponse::NotFound().json(serde_json::json!({
743                "error": "État daté not found"
744            }))
745        }
746        Err(err) => {
747            return HttpResponse::InternalServerError().json(serde_json::json!({
748                "error": err.to_string()
749            }))
750        }
751    }
752
753    match state.etat_date_use_cases.delete_etat_date(*id).await {
754        Ok(true) => {
755            AuditLogEntry::new(
756                AuditEventType::EtatDateDeleted,
757                Some(user.user_id),
758                user.organization_id,
759            )
760            .with_resource("EtatDate", *id)
761            .log();
762
763            HttpResponse::NoContent().finish()
764        }
765        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
766            "error": "État daté not found"
767        })),
768        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
769            "error": err
770        })),
771    }
772}