koprogo_api/infrastructure/web/handlers/
quote_handlers.rs1use 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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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 #[test]
319 fn test_handler_structure_quotes() {
320 }
323}