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