koprogo_api/infrastructure/web/handlers/
quote_handlers.rs

1use crate::application::dto::{CreateQuoteDto, QuoteComparisonRequestDto, QuoteDecisionDto};
2use crate::infrastructure::web::middleware::AuthenticatedUser;
3use crate::infrastructure::web::AppState;
4use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
5use uuid::Uuid;
6
7/// POST /api/v1/quotes
8/// Create new quote request (Syndic action)
9#[post("/quotes")]
10pub async fn create_quote(
11    data: web::Data<AppState>,
12    _auth: AuthenticatedUser,
13    request: web::Json<CreateQuoteDto>,
14) -> impl Responder {
15    match data
16        .quote_use_cases
17        .create_quote(request.into_inner())
18        .await
19    {
20        Ok(quote) => HttpResponse::Created().json(quote),
21        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
22            "error": e
23        })),
24    }
25}
26
27/// GET /api/v1/quotes/:id
28/// Get quote by ID
29#[get("/quotes/{id}")]
30pub async fn get_quote(
31    data: web::Data<AppState>,
32    _auth: AuthenticatedUser,
33    id: web::Path<Uuid>,
34) -> impl Responder {
35    match data.quote_use_cases.get_quote(id.into_inner()).await {
36        Ok(Some(quote)) => HttpResponse::Ok().json(quote),
37        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
38            "error": "Quote not found"
39        })),
40        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
41            "error": e
42        })),
43    }
44}
45
46/// GET /api/v1/buildings/:building_id/quotes
47/// List all quotes for a building
48#[get("/buildings/{building_id}/quotes")]
49pub async fn list_building_quotes(
50    data: web::Data<AppState>,
51    _auth: AuthenticatedUser,
52    building_id: web::Path<Uuid>,
53) -> impl Responder {
54    match data
55        .quote_use_cases
56        .list_by_building(building_id.into_inner())
57        .await
58    {
59        Ok(quotes) => HttpResponse::Ok().json(quotes),
60        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
61            "error": e
62        })),
63    }
64}
65
66/// GET /api/v1/contractors/:contractor_id/quotes
67/// List all quotes for a contractor
68#[get("/contractors/{contractor_id}/quotes")]
69pub async fn list_contractor_quotes(
70    data: web::Data<AppState>,
71    _auth: AuthenticatedUser,
72    contractor_id: web::Path<Uuid>,
73) -> impl Responder {
74    match data
75        .quote_use_cases
76        .list_by_contractor(contractor_id.into_inner())
77        .await
78    {
79        Ok(quotes) => HttpResponse::Ok().json(quotes),
80        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
81            "error": e
82        })),
83    }
84}
85
86/// GET /api/v1/buildings/:building_id/quotes/status/:status
87/// List quotes by status
88#[get("/buildings/{building_id}/quotes/status/{status}")]
89pub async fn list_quotes_by_status(
90    data: web::Data<AppState>,
91    _auth: AuthenticatedUser,
92    path: web::Path<(Uuid, String)>,
93) -> impl Responder {
94    let (building_id, status) = path.into_inner();
95
96    match data
97        .quote_use_cases
98        .list_by_status(building_id, &status)
99        .await
100    {
101        Ok(quotes) => HttpResponse::Ok().json(quotes),
102        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
103            "error": e
104        })),
105    }
106}
107
108/// POST /api/v1/quotes/:id/submit
109/// Submit quote (Contractor action)
110#[post("/quotes/{id}/submit")]
111pub async fn submit_quote(
112    data: web::Data<AppState>,
113    _auth: AuthenticatedUser,
114    id: web::Path<Uuid>,
115) -> impl Responder {
116    match data.quote_use_cases.submit_quote(id.into_inner()).await {
117        Ok(quote) => HttpResponse::Ok().json(quote),
118        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
119            "error": e
120        })),
121    }
122}
123
124/// POST /api/v1/quotes/:id/review
125/// Start quote review (Syndic action)
126#[post("/quotes/{id}/review")]
127pub async fn start_review(
128    data: web::Data<AppState>,
129    _auth: AuthenticatedUser,
130    id: web::Path<Uuid>,
131) -> impl Responder {
132    match data.quote_use_cases.start_review(id.into_inner()).await {
133        Ok(quote) => HttpResponse::Ok().json(quote),
134        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
135            "error": e
136        })),
137    }
138}
139
140/// POST /api/v1/quotes/:id/accept
141/// Accept quote (Syndic action - winner)
142#[post("/quotes/{id}/accept")]
143pub async fn accept_quote(
144    data: web::Data<AppState>,
145    auth: AuthenticatedUser,
146    id: web::Path<Uuid>,
147    request: web::Json<QuoteDecisionDto>,
148) -> impl Responder {
149    match data
150        .quote_use_cases
151        .accept_quote(id.into_inner(), auth.user_id, request.into_inner())
152        .await
153    {
154        Ok(quote) => HttpResponse::Ok().json(quote),
155        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
156            "error": e
157        })),
158    }
159}
160
161/// POST /api/v1/quotes/:id/reject
162/// Reject quote (Syndic action)
163#[post("/quotes/{id}/reject")]
164pub async fn reject_quote(
165    data: web::Data<AppState>,
166    auth: AuthenticatedUser,
167    id: web::Path<Uuid>,
168    request: web::Json<QuoteDecisionDto>,
169) -> impl Responder {
170    match data
171        .quote_use_cases
172        .reject_quote(id.into_inner(), auth.user_id, request.into_inner())
173        .await
174    {
175        Ok(quote) => HttpResponse::Ok().json(quote),
176        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
177            "error": e
178        })),
179    }
180}
181
182/// POST /api/v1/quotes/:id/withdraw
183/// Withdraw quote (Contractor action)
184#[post("/quotes/{id}/withdraw")]
185pub async fn withdraw_quote(
186    data: web::Data<AppState>,
187    _auth: AuthenticatedUser,
188    id: web::Path<Uuid>,
189) -> impl Responder {
190    match data.quote_use_cases.withdraw_quote(id.into_inner()).await {
191        Ok(quote) => HttpResponse::Ok().json(quote),
192        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
193            "error": e
194        })),
195    }
196}
197
198/// POST /api/v1/quotes/compare
199/// Compare multiple quotes (Belgian legal requirement: 3 quotes minimum)
200/// Returns quotes sorted by automatic score (best first)
201#[post("/quotes/compare")]
202pub async fn compare_quotes(
203    data: web::Data<AppState>,
204    _auth: AuthenticatedUser,
205    request: web::Json<QuoteComparisonRequestDto>,
206) -> impl Responder {
207    match data
208        .quote_use_cases
209        .compare_quotes(request.into_inner())
210        .await
211    {
212        Ok(comparison) => HttpResponse::Ok().json(comparison),
213        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
214            "error": e
215        })),
216    }
217}
218
219/// PUT /api/v1/quotes/:id/contractor-rating
220/// Update contractor rating (for scoring algorithm)
221#[put("/quotes/{id}/contractor-rating")]
222pub async fn update_contractor_rating(
223    data: web::Data<AppState>,
224    _auth: AuthenticatedUser,
225    id: web::Path<Uuid>,
226    request: web::Json<serde_json::Value>,
227) -> impl Responder {
228    let rating = match request.get("rating").and_then(|v| v.as_i64()) {
229        Some(r) => r as i32,
230        None => {
231            return HttpResponse::BadRequest().json(serde_json::json!({
232                "error": "Rating field is required and must be an integer (0-100)"
233            }))
234        }
235    };
236
237    match data
238        .quote_use_cases
239        .update_contractor_rating(id.into_inner(), rating)
240        .await
241    {
242        Ok(quote) => HttpResponse::Ok().json(quote),
243        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
244            "error": e
245        })),
246    }
247}
248
249/// DELETE /api/v1/quotes/:id
250/// Delete quote
251#[delete("/quotes/{id}")]
252pub async fn delete_quote(
253    data: web::Data<AppState>,
254    _auth: AuthenticatedUser,
255    id: web::Path<Uuid>,
256) -> impl Responder {
257    match data.quote_use_cases.delete_quote(id.into_inner()).await {
258        Ok(true) => HttpResponse::NoContent().finish(),
259        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
260            "error": "Quote not found"
261        })),
262        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
263            "error": e
264        })),
265    }
266}
267
268/// GET /api/v1/buildings/:building_id/quotes/count
269/// Count total quotes for building
270#[get("/buildings/{building_id}/quotes/count")]
271pub async fn count_building_quotes(
272    data: web::Data<AppState>,
273    _auth: AuthenticatedUser,
274    building_id: web::Path<Uuid>,
275) -> impl Responder {
276    match data
277        .quote_use_cases
278        .count_by_building(building_id.into_inner())
279        .await
280    {
281        Ok(count) => HttpResponse::Ok().json(serde_json::json!({
282            "count": count
283        })),
284        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
285            "error": e
286        })),
287    }
288}
289
290/// GET /api/v1/buildings/:building_id/quotes/status/:status/count
291/// Count quotes by status for building
292#[get("/buildings/{building_id}/quotes/status/{status}/count")]
293pub async fn count_quotes_by_status(
294    data: web::Data<AppState>,
295    _auth: AuthenticatedUser,
296    path: web::Path<(Uuid, String)>,
297) -> impl Responder {
298    let (building_id, status) = path.into_inner();
299
300    match data
301        .quote_use_cases
302        .count_by_status(building_id, &status)
303        .await
304    {
305        Ok(count) => HttpResponse::Ok().json(serde_json::json!({
306            "count": count
307        })),
308        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
309            "error": e
310        })),
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    // Handler tests are covered by E2E tests in tests/e2e/
317
318    #[test]
319    fn test_handler_structure_quotes() {
320        // This test verifies handler function signatures compile
321        // Real testing happens in E2E tests with testcontainers
322    }
323}