Skip to main content

koprogo_api/infrastructure/web/handlers/
contractor_report_handlers.rs

1use crate::application::dto::contractor_report_dto::{
2    CreateContractorReportDto, GenerateMagicLinkDto, RejectReportDto, RequestCorrectionsDto,
3    UpdateContractorReportDto,
4};
5use crate::application::error::AppError;
6use crate::infrastructure::web::classification_erreurs;
7use crate::infrastructure::web::{AppState, AuthenticatedUser};
8use actix_web::{delete, get, post, put, web, HttpRequest, HttpResponse, Responder};
9use serde::Deserialize;
10use uuid::Uuid;
11
12// ---------------------------------------------------------------------------
13// Endpoints authentifiés (syndic / CdC)
14// ---------------------------------------------------------------------------
15
16/// POST /contractor-reports — Créer un rapport (syndic ou système)
17#[post("/contractor-reports")]
18pub async fn create_contractor_report(
19    state: web::Data<AppState>,
20    user: AuthenticatedUser,
21    body: web::Json<CreateContractorReportDto>,
22) -> impl Responder {
23    let organization_id = match user.require_organization() {
24        Ok(id) => id,
25        Err(e) => {
26            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
27        }
28    };
29
30    match state
31        .contractor_report_use_cases
32        .create(organization_id, body.into_inner())
33        .await
34    {
35        Ok(r) => HttpResponse::Created().json(r),
36        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
37    }
38}
39
40/// GET /contractor-reports/:id — Détail d'un rapport (authentifié)
41#[get("/contractor-reports/{id}")]
42pub async fn get_contractor_report(
43    state: web::Data<AppState>,
44    user: AuthenticatedUser,
45    path: web::Path<Uuid>,
46) -> impl Responder {
47    let organization_id = match user.require_organization() {
48        Ok(id) => id,
49        Err(e) => {
50            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
51        }
52    };
53
54    match state
55        .contractor_report_use_cases
56        .get(path.into_inner(), organization_id)
57        .await
58    {
59        Ok(r) => HttpResponse::Ok().json(r),
60        Err(e) => {
61            if classification_erreurs::est_introuvable(&e) {
62                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
63            } else if classification_erreurs::est_interdit(&e) {
64                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
65            } else {
66                HttpResponse::InternalServerError().json(serde_json::json!({"error": e}))
67            }
68        }
69    }
70}
71
72/// GET /buildings/:building_id/contractor-reports — Liste des rapports d'un bâtiment
73#[get("/buildings/{building_id}/contractor-reports")]
74pub async fn list_contractor_reports_by_building(
75    state: web::Data<AppState>,
76    user: AuthenticatedUser,
77    path: web::Path<Uuid>,
78) -> impl Responder {
79    let organization_id = match user.require_organization() {
80        Ok(id) => id,
81        Err(e) => {
82            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
83        }
84    };
85
86    match state
87        .contractor_report_use_cases
88        .list_by_building(path.into_inner(), organization_id)
89        .await
90    {
91        Ok(r) => HttpResponse::Ok().json(r),
92        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
93    }
94}
95
96/// GET /tickets/:ticket_id/contractor-reports — Rapports liés à un ticket
97#[get("/tickets/{ticket_id}/contractor-reports")]
98pub async fn list_contractor_reports_by_ticket(
99    state: web::Data<AppState>,
100    user: AuthenticatedUser,
101    path: web::Path<Uuid>,
102) -> impl Responder {
103    let organization_id = match user.require_organization() {
104        Ok(id) => id,
105        Err(e) => {
106            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
107        }
108    };
109
110    match state
111        .contractor_report_use_cases
112        .list_by_ticket(path.into_inner(), organization_id)
113        .await
114    {
115        Ok(r) => HttpResponse::Ok().json(r),
116        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
117    }
118}
119
120/// PUT /contractor-reports/:id — Modifier le brouillon
121#[put("/contractor-reports/{id}")]
122pub async fn update_contractor_report(
123    state: web::Data<AppState>,
124    user: AuthenticatedUser,
125    path: web::Path<Uuid>,
126    body: web::Json<UpdateContractorReportDto>,
127) -> impl Responder {
128    let organization_id = match user.require_organization() {
129        Ok(id) => id,
130        Err(e) => {
131            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
132        }
133    };
134
135    match state
136        .contractor_report_use_cases
137        .update(path.into_inner(), organization_id, body.into_inner())
138        .await
139    {
140        Ok(r) => HttpResponse::Ok().json(r),
141        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
142    }
143}
144
145/// POST /contractor-reports/:id/submit — Soumettre pour validation CdC (auth)
146#[post("/contractor-reports/{id}/submit")]
147pub async fn submit_contractor_report(
148    state: web::Data<AppState>,
149    user: AuthenticatedUser,
150    path: web::Path<Uuid>,
151) -> impl Responder {
152    let organization_id = match user.require_organization() {
153        Ok(id) => id,
154        Err(e) => {
155            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
156        }
157    };
158
159    match state
160        .contractor_report_use_cases
161        .submit(path.into_inner(), organization_id)
162        .await
163    {
164        Ok(r) => HttpResponse::Ok().json(r),
165        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
166    }
167}
168
169/// PUT /contractor-reports/:id/validate — CdC valide le rapport → paiement auto
170#[put("/contractor-reports/{id}/validate")]
171pub async fn validate_contractor_report(
172    state: web::Data<AppState>,
173    user: AuthenticatedUser,
174    path: web::Path<Uuid>,
175) -> impl Responder {
176    let organization_id = match user.require_organization() {
177        Ok(id) => id,
178        Err(e) => {
179            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
180        }
181    };
182
183    match state
184        .contractor_report_use_cases
185        .validate(path.into_inner(), organization_id, user.user_id)
186        .await
187    {
188        Ok(r) => HttpResponse::Ok().json(r),
189        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
190    }
191}
192
193/// PUT /contractor-reports/:id/request-corrections — CdC demande des corrections
194#[put("/contractor-reports/{id}/request-corrections")]
195pub async fn request_corrections(
196    state: web::Data<AppState>,
197    user: AuthenticatedUser,
198    path: web::Path<Uuid>,
199    body: web::Json<RequestCorrectionsDto>,
200) -> impl Responder {
201    let organization_id = match user.require_organization() {
202        Ok(id) => id,
203        Err(e) => {
204            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
205        }
206    };
207
208    match state
209        .contractor_report_use_cases
210        .request_corrections(path.into_inner(), organization_id, body.into_inner())
211        .await
212    {
213        Ok(r) => HttpResponse::Ok().json(r),
214        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
215    }
216}
217
218/// PUT /contractor-reports/:id/reject — CdC rejette le rapport
219#[put("/contractor-reports/{id}/reject")]
220pub async fn reject_contractor_report(
221    state: web::Data<AppState>,
222    user: AuthenticatedUser,
223    path: web::Path<Uuid>,
224    body: web::Json<RejectReportDto>,
225) -> impl Responder {
226    let organization_id = match user.require_organization() {
227        Ok(id) => id,
228        Err(e) => {
229            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
230        }
231    };
232
233    match state
234        .contractor_report_use_cases
235        .reject(
236            path.into_inner(),
237            organization_id,
238            body.into_inner(),
239            user.user_id,
240        )
241        .await
242    {
243        Ok(r) => HttpResponse::Ok().json(r),
244        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
245    }
246}
247
248/// POST /contractor-reports/magic-link — Génère un magic link pour le corps de métier
249///
250/// #835 — délègue désormais au système générique de liens magiques (scope
251/// `ContractorReport`) : réponse HTTP inchangée (`MagicLinkResponseDto`), mais
252/// l'URL produite est `/c?t=...` au lieu de `/contractor/?token=...`.
253#[post("/contractor-reports/magic-link")]
254pub async fn generate_magic_link(
255    state: web::Data<AppState>,
256    user: AuthenticatedUser,
257    req: HttpRequest,
258    body: web::Json<GenerateMagicLinkDto>,
259) -> Result<HttpResponse, AppError> {
260    let organization_id = user
261        .require_organization()
262        .map_err(|e| AppError::Forbidden(e.to_string()))?;
263
264    // Déduire la base URL depuis la requête (drop connection_info avant l'await)
265    let base_url = {
266        let connection_info = req.connection_info();
267        format!("{}://{}", connection_info.scheme(), connection_info.host())
268    };
269
270    let response = state
271        .contractor_report_use_cases
272        .generate_magic_link(body.report_id, organization_id, user.user_id, &base_url)
273        .await?;
274
275    Ok(HttpResponse::Ok().json(response))
276}
277
278/// DELETE /contractor-reports/:id — Supprimer un rapport (Draft seulement)
279#[delete("/contractor-reports/{id}")]
280pub async fn delete_contractor_report(
281    state: web::Data<AppState>,
282    user: AuthenticatedUser,
283    path: web::Path<Uuid>,
284) -> impl Responder {
285    let organization_id = match user.require_organization() {
286        Ok(id) => id,
287        Err(e) => {
288            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
289        }
290    };
291
292    match state
293        .contractor_report_use_cases
294        .delete(path.into_inner(), organization_id)
295        .await
296    {
297        Ok(()) => HttpResponse::NoContent().finish(),
298        Err(e) => {
299            if classification_erreurs::est_introuvable(&e) {
300                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
301            } else if classification_erreurs::est_interdit(&e) {
302                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
303            } else {
304                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
305            }
306        }
307    }
308}
309
310// ---------------------------------------------------------------------------
311// Endpoints PWA sans authentification (magic link)
312// ---------------------------------------------------------------------------
313
314/// GET /contractor/token/:token — PWA corps de métier : voir son rapport via magic link
315#[get("/contractor/token/{token}")]
316pub async fn get_report_by_token(
317    state: web::Data<AppState>,
318    path: web::Path<String>,
319) -> impl Responder {
320    match state
321        .contractor_report_use_cases
322        .get_by_token(&path.into_inner())
323        .await
324    {
325        Ok(r) => HttpResponse::Ok().json(r),
326        Err(e) => HttpResponse::Unauthorized().json(serde_json::json!({"error": e})),
327    }
328}
329
330/// POST /contractor/token/:token/submit — PWA corps de métier : soumettre via magic link
331#[post("/contractor/token/{token}/submit")]
332pub async fn submit_report_by_token(
333    state: web::Data<AppState>,
334    path: web::Path<String>,
335) -> impl Responder {
336    match state
337        .contractor_report_use_cases
338        .submit_by_token(&path.into_inner())
339        .await
340    {
341        Ok(r) => HttpResponse::Ok().json(r),
342        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
343    }
344}
345
346// ---------------------------------------------------------------------------
347// New PWA endpoints with improved magic link UX (Issue #275)
348// ---------------------------------------------------------------------------
349
350/// GET /contractor-reports/magic/:token — PWA contractor: view report via magic link (no auth)
351/// Issue #275: Contractor PWA Backoffice Refinements
352#[get("/contractor-reports/magic/{token}")]
353pub async fn get_report_by_magic_token(
354    state: web::Data<AppState>,
355    path: web::Path<String>,
356) -> impl Responder {
357    match state
358        .contractor_report_use_cases
359        .get_by_token(&path.into_inner())
360        .await
361    {
362        Ok(r) => HttpResponse::Ok().json(r),
363        Err(e) => HttpResponse::Unauthorized().json(serde_json::json!({"error": e})),
364    }
365}
366
367/// POST /contractor-reports/magic/:token/submit — PWA contractor: submit report via magic link (no auth)
368/// Issue #275: Contractor PWA Backoffice Refinements
369/// Accepts updated report data in body
370#[derive(Deserialize)]
371pub struct MagicLinkSubmitDto {
372    pub work_date: Option<String>,
373    pub contractor_name: Option<String>,
374    pub compte_rendu: Option<String>,
375    pub parts_replaced: Option<Vec<serde_json::Value>>,
376    pub photos_before: Option<Vec<String>>,
377    pub photos_after: Option<Vec<String>>,
378}
379
380#[post("/contractor-reports/magic/{token}/submit")]
381pub async fn submit_report_by_magic_token(
382    state: web::Data<AppState>,
383    path: web::Path<String>,
384    _body: web::Json<MagicLinkSubmitDto>,
385) -> impl Responder {
386    let token = path.into_inner();
387
388    // First, get the report by token to validate it exists and is in Draft state
389    match state.contractor_report_use_cases.get_by_token(&token).await {
390        Ok(_report) => {
391            // Report exists, now submit it
392            match state
393                .contractor_report_use_cases
394                .submit_by_token(&token)
395                .await
396            {
397                Ok(r) => HttpResponse::Ok().json(r),
398                Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
399            }
400        }
401        Err(e) => HttpResponse::Unauthorized().json(serde_json::json!({"error": e})),
402    }
403}