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
30async 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#[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 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 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 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("/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 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("/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 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#[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#[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#[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#[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 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#[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 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#[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 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#[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 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#[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 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#[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 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#[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 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#[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 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#[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#[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("/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("/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 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}