1use crate::application::dto::{
2 CreateQuoteDto, QuoteComparisonRequestDto, QuoteDecisionDto, SubmitQuoteDto,
3};
4use crate::infrastructure::web::middleware::scope_guard::{
5 verify_building_org_access, verify_quote_org_access,
6};
7use crate::infrastructure::web::middleware::AuthenticatedUser;
8use crate::infrastructure::web::AppState;
9use actix_web::{delete, get, post, put, web, HttpResponse, Responder, ResponseError};
10use uuid::Uuid;
11
12#[post("/quotes")]
15pub async fn create_quote(
16 data: web::Data<AppState>,
17 auth: AuthenticatedUser,
18 request: web::Json<CreateQuoteDto>,
19) -> impl Responder {
20 let building_id = match Uuid::parse_str(&request.building_id) {
33 Ok(id) => id,
34 Err(_) => {
35 return HttpResponse::BadRequest().json(serde_json::json!({
36 "error": "Invalid building_id format"
37 }))
38 }
39 };
40 if let Err(err) = verify_building_org_access(
41 &auth,
42 building_id,
43 &data.building_use_cases,
44 &data.acp_use_cases,
45 )
46 .await
47 {
48 return err.error_response();
49 }
50
51 match data
52 .quote_use_cases
53 .create_quote(request.into_inner())
54 .await
55 {
56 Ok(quote) => HttpResponse::Created().json(quote),
57 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
58 "error": e
59 })),
60 }
61}
62
63#[get("/quotes/{id}")]
66pub async fn get_quote(
67 data: web::Data<AppState>,
68 auth: AuthenticatedUser,
69 id: web::Path<Uuid>,
70) -> impl Responder {
71 if let Err(err) = verify_quote_org_access(
79 &auth,
80 *id,
81 &data.quote_use_cases,
82 &data.building_use_cases,
83 &data.acp_use_cases,
84 )
85 .await
86 {
87 return err.error_response();
88 }
89
90 match data.quote_use_cases.get_quote(id.into_inner()).await {
91 Ok(Some(quote)) => HttpResponse::Ok().json(quote),
92 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
93 "error": "Quote not found"
94 })),
95 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
96 "error": e
97 })),
98 }
99}
100
101#[get("/buildings/{building_id}/quotes")]
104pub async fn list_building_quotes(
105 data: web::Data<AppState>,
106 auth: AuthenticatedUser,
107 building_id: web::Path<Uuid>,
108) -> impl Responder {
109 if let Err(err) = verify_building_org_access(
114 &auth,
115 *building_id,
116 &data.building_use_cases,
117 &data.acp_use_cases,
118 )
119 .await
120 {
121 return err.error_response();
122 }
123
124 match data
125 .quote_use_cases
126 .list_by_building(building_id.into_inner())
127 .await
128 {
129 Ok(quotes) => HttpResponse::Ok().json(quotes),
130 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
131 "error": e
132 })),
133 }
134}
135
136#[get("/contractors/{contractor_id}/quotes")]
155pub async fn list_contractor_quotes(
156 data: web::Data<AppState>,
157 auth: AuthenticatedUser,
158 contractor_id: web::Path<Uuid>,
159) -> impl Responder {
160 let quotes = match data
161 .quote_use_cases
162 .list_by_contractor(contractor_id.into_inner())
163 .await
164 {
165 Ok(quotes) => quotes,
166 Err(e) => {
167 return HttpResponse::InternalServerError().json(serde_json::json!({
168 "error": e
169 }))
170 }
171 };
172
173 let mut vus: std::collections::HashMap<Uuid, bool> = std::collections::HashMap::new();
190 let mut visibles = Vec::with_capacity(quotes.len());
191
192 for devis in quotes {
193 let building_id = match Uuid::parse_str(&devis.building_id) {
194 Ok(id) => id,
195 Err(_) => continue,
198 };
199
200 let autorise = match vus.get(&building_id) {
201 Some(deja) => *deja,
202 None => {
203 let ok = verify_building_org_access(
204 &auth,
205 building_id,
206 &data.building_use_cases,
207 &data.acp_use_cases,
208 )
209 .await
210 .is_ok();
211 vus.insert(building_id, ok);
212 ok
213 }
214 };
215
216 if autorise {
217 visibles.push(devis);
218 }
219 }
220
221 HttpResponse::Ok().json(visibles)
222}
223
224#[get("/buildings/{building_id}/quotes/status/{status}")]
227pub async fn list_quotes_by_status(
228 data: web::Data<AppState>,
229 auth: AuthenticatedUser,
230 path: web::Path<(Uuid, String)>,
231) -> impl Responder {
232 if let Err(err) =
237 verify_building_org_access(&auth, path.0, &data.building_use_cases, &data.acp_use_cases)
238 .await
239 {
240 return err.error_response();
241 }
242
243 let (building_id, status) = path.into_inner();
244
245 match data
246 .quote_use_cases
247 .list_by_status(building_id, &status)
248 .await
249 {
250 Ok(quotes) => HttpResponse::Ok().json(quotes),
251 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
252 "error": e
253 })),
254 }
255}
256
257#[post("/quotes/{id}/submit")]
262pub async fn submit_quote(
263 data: web::Data<AppState>,
264 auth: AuthenticatedUser,
265 id: web::Path<Uuid>,
266 body: web::Bytes,
267) -> impl Responder {
268 if let Err(err) = verify_quote_org_access(
276 &auth,
277 *id,
278 &data.quote_use_cases,
279 &data.building_use_cases,
280 &data.acp_use_cases,
281 )
282 .await
283 {
284 return err.error_response();
285 }
286
287 let pricing = if body.is_empty() {
292 None
293 } else {
294 match serde_json::from_slice::<SubmitQuoteDto>(&body) {
295 Ok(dto) => Some(dto),
296 Err(e) => {
297 return HttpResponse::BadRequest().json(serde_json::json!({
298 "error": format!("Invalid pricing payload: {}", e)
299 }))
300 }
301 }
302 };
303
304 match data
305 .quote_use_cases
306 .submit_quote(id.into_inner(), pricing)
307 .await
308 {
309 Ok(quote) => HttpResponse::Ok().json(quote),
310 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
311 "error": e
312 })),
313 }
314}
315
316#[post("/quotes/{id}/review")]
319pub async fn start_review(
320 data: web::Data<AppState>,
321 auth: AuthenticatedUser,
322 id: web::Path<Uuid>,
323) -> impl Responder {
324 if let Err(err) = verify_quote_org_access(
332 &auth,
333 *id,
334 &data.quote_use_cases,
335 &data.building_use_cases,
336 &data.acp_use_cases,
337 )
338 .await
339 {
340 return err.error_response();
341 }
342
343 match data.quote_use_cases.start_review(id.into_inner()).await {
344 Ok(quote) => HttpResponse::Ok().json(quote),
345 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
346 "error": e
347 })),
348 }
349}
350
351#[post("/quotes/{id}/accept")]
354pub async fn accept_quote(
355 data: web::Data<AppState>,
356 auth: AuthenticatedUser,
357 id: web::Path<Uuid>,
358 request: web::Json<QuoteDecisionDto>,
359) -> impl Responder {
360 if let Err(err) = verify_quote_org_access(
364 &auth,
365 *id,
366 &data.quote_use_cases,
367 &data.building_use_cases,
368 &data.acp_use_cases,
369 )
370 .await
371 {
372 return err.error_response();
373 }
374
375 match data
376 .quote_use_cases
377 .accept_quote(id.into_inner(), auth.user_id, request.into_inner())
378 .await
379 {
380 Ok(quote) => HttpResponse::Ok().json(quote),
381 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
382 "error": e
383 })),
384 }
385}
386
387#[post("/quotes/{id}/reject")]
390pub async fn reject_quote(
391 data: web::Data<AppState>,
392 auth: AuthenticatedUser,
393 id: web::Path<Uuid>,
394 request: web::Json<QuoteDecisionDto>,
395) -> impl Responder {
396 if let Err(err) = verify_quote_org_access(
400 &auth,
401 *id,
402 &data.quote_use_cases,
403 &data.building_use_cases,
404 &data.acp_use_cases,
405 )
406 .await
407 {
408 return err.error_response();
409 }
410
411 match data
412 .quote_use_cases
413 .reject_quote(id.into_inner(), auth.user_id, request.into_inner())
414 .await
415 {
416 Ok(quote) => HttpResponse::Ok().json(quote),
417 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
418 "error": e
419 })),
420 }
421}
422
423#[post("/quotes/{id}/withdraw")]
426pub async fn withdraw_quote(
427 data: web::Data<AppState>,
428 auth: AuthenticatedUser,
429 id: web::Path<Uuid>,
430) -> impl Responder {
431 if let Err(err) = verify_quote_org_access(
439 &auth,
440 *id,
441 &data.quote_use_cases,
442 &data.building_use_cases,
443 &data.acp_use_cases,
444 )
445 .await
446 {
447 return err.error_response();
448 }
449
450 match data.quote_use_cases.withdraw_quote(id.into_inner()).await {
451 Ok(quote) => HttpResponse::Ok().json(quote),
452 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
453 "error": e
454 })),
455 }
456}
457
458#[post("/quotes/compare")]
462pub async fn compare_quotes(
463 data: web::Data<AppState>,
464 auth: AuthenticatedUser,
465 request: web::Json<QuoteComparisonRequestDto>,
466) -> impl Responder {
467 for id_str in &request.quote_ids {
473 let quote_id = match Uuid::parse_str(id_str) {
474 Ok(id) => id,
475 Err(_) => {
476 return HttpResponse::BadRequest().json(serde_json::json!({
477 "error": format!("Invalid quote_id format: {}", id_str)
478 }))
479 }
480 };
481 if let Err(err) = verify_quote_org_access(
482 &auth,
483 quote_id,
484 &data.quote_use_cases,
485 &data.building_use_cases,
486 &data.acp_use_cases,
487 )
488 .await
489 {
490 return err.error_response();
491 }
492 }
493
494 match data
495 .quote_use_cases
496 .compare_quotes(request.into_inner())
497 .await
498 {
499 Ok(comparison) => HttpResponse::Ok().json(comparison),
500 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
501 "error": e
502 })),
503 }
504}
505
506#[put("/quotes/{id}/contractor-rating")]
509pub async fn update_contractor_rating(
510 data: web::Data<AppState>,
511 auth: AuthenticatedUser,
512 id: web::Path<Uuid>,
513 request: web::Json<serde_json::Value>,
514) -> impl Responder {
515 if let Err(err) = verify_quote_org_access(
523 &auth,
524 *id,
525 &data.quote_use_cases,
526 &data.building_use_cases,
527 &data.acp_use_cases,
528 )
529 .await
530 {
531 return err.error_response();
532 }
533
534 let rating = match request.get("rating").and_then(|v| v.as_i64()) {
535 Some(r) => r as i32,
536 None => {
537 return HttpResponse::BadRequest().json(serde_json::json!({
538 "error": "Rating field is required and must be an integer (0-100)"
539 }))
540 }
541 };
542
543 match data
544 .quote_use_cases
545 .update_contractor_rating(id.into_inner(), rating)
546 .await
547 {
548 Ok(quote) => HttpResponse::Ok().json(quote),
549 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
550 "error": e
551 })),
552 }
553}
554
555#[delete("/quotes/{id}")]
558pub async fn delete_quote(
559 data: web::Data<AppState>,
560 auth: AuthenticatedUser,
561 id: web::Path<Uuid>,
562) -> impl Responder {
563 if let Err(err) = verify_quote_org_access(
571 &auth,
572 *id,
573 &data.quote_use_cases,
574 &data.building_use_cases,
575 &data.acp_use_cases,
576 )
577 .await
578 {
579 return err.error_response();
580 }
581
582 match data.quote_use_cases.delete_quote(id.into_inner()).await {
583 Ok(true) => HttpResponse::NoContent().finish(),
584 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
585 "error": "Quote not found"
586 })),
587 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
588 "error": e
589 })),
590 }
591}
592
593#[get("/buildings/{building_id}/quotes/count")]
596pub async fn count_building_quotes(
597 data: web::Data<AppState>,
598 auth: AuthenticatedUser,
599 building_id: web::Path<Uuid>,
600) -> impl Responder {
601 if let Err(err) = verify_building_org_access(
606 &auth,
607 *building_id,
608 &data.building_use_cases,
609 &data.acp_use_cases,
610 )
611 .await
612 {
613 return err.error_response();
614 }
615
616 match data
617 .quote_use_cases
618 .count_by_building(building_id.into_inner())
619 .await
620 {
621 Ok(count) => HttpResponse::Ok().json(serde_json::json!({
622 "count": count
623 })),
624 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
625 "error": e
626 })),
627 }
628}
629
630#[get("/buildings/{building_id}/quotes/status/{status}/count")]
633pub async fn count_quotes_by_status(
634 data: web::Data<AppState>,
635 auth: AuthenticatedUser,
636 path: web::Path<(Uuid, String)>,
637) -> impl Responder {
638 if let Err(err) =
643 verify_building_org_access(&auth, path.0, &data.building_use_cases, &data.acp_use_cases)
644 .await
645 {
646 return err.error_response();
647 }
648
649 let (building_id, status) = path.into_inner();
650
651 match data
652 .quote_use_cases
653 .count_by_status(building_id, &status)
654 .await
655 {
656 Ok(count) => HttpResponse::Ok().json(serde_json::json!({
657 "count": count
658 })),
659 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
660 "error": e
661 })),
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 #[test]
670 fn test_handler_structure_quotes() {
671 }
674}