koprogo_api/infrastructure/web/handlers/
local_exchange_handlers.rs

1use crate::application::dto::{
2    CancelExchangeDto, CompleteExchangeDto, CreateLocalExchangeDto, RateExchangeDto,
3    RequestExchangeDto,
4};
5use crate::domain::entities::ExchangeType;
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
8use uuid::Uuid;
9
10/// POST /api/v1/exchanges
11/// Create a new exchange offer
12#[post("/exchanges")]
13pub async fn create_exchange(
14    data: web::Data<AppState>,
15    auth: AuthenticatedUser,
16    request: web::Json<CreateLocalExchangeDto>,
17) -> impl Responder {
18    match data
19        .local_exchange_use_cases
20        .create_exchange(auth.user_id, request.into_inner())
21        .await
22    {
23        Ok(exchange) => HttpResponse::Created().json(exchange),
24        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
25    }
26}
27
28/// GET /api/v1/exchanges/:id
29/// Get exchange by ID
30#[get("/exchanges/{id}")]
31pub async fn get_exchange(
32    data: web::Data<AppState>,
33    _auth: AuthenticatedUser,
34    id: web::Path<Uuid>,
35) -> impl Responder {
36    match data
37        .local_exchange_use_cases
38        .get_exchange(id.into_inner())
39        .await
40    {
41        Ok(exchange) => HttpResponse::Ok().json(exchange),
42        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
43    }
44}
45
46/// GET /api/v1/buildings/:building_id/exchanges
47/// List all exchanges for a building
48#[get("/buildings/{building_id}/exchanges")]
49pub async fn list_building_exchanges(
50    data: web::Data<AppState>,
51    _auth: AuthenticatedUser,
52    building_id: web::Path<Uuid>,
53) -> impl Responder {
54    match data
55        .local_exchange_use_cases
56        .list_building_exchanges(building_id.into_inner())
57        .await
58    {
59        Ok(exchanges) => HttpResponse::Ok().json(exchanges),
60        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
61    }
62}
63
64/// GET /api/v1/buildings/:building_id/exchanges/available
65/// List available exchanges (status = Offered)
66#[get("/buildings/{building_id}/exchanges/available")]
67pub async fn list_available_exchanges(
68    data: web::Data<AppState>,
69    _auth: AuthenticatedUser,
70    building_id: web::Path<Uuid>,
71) -> impl Responder {
72    match data
73        .local_exchange_use_cases
74        .list_available_exchanges(building_id.into_inner())
75        .await
76    {
77        Ok(exchanges) => HttpResponse::Ok().json(exchanges),
78        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
79    }
80}
81
82/// GET /api/v1/owners/:owner_id/exchanges
83/// List exchanges for an owner (as provider OR requester)
84#[get("/owners/{owner_id}/exchanges")]
85pub async fn list_owner_exchanges(
86    data: web::Data<AppState>,
87    auth: AuthenticatedUser,
88    owner_id: web::Path<Uuid>,
89) -> impl Responder {
90    let owner_id = owner_id.into_inner();
91
92    // Authorization: users can only see their own exchanges
93    // Fetch owner to verify user_id mapping
94    let owner = match data.owner_use_cases.get_owner(owner_id).await {
95        Ok(Some(owner)) => owner,
96        Ok(None) => {
97            return HttpResponse::NotFound().json(serde_json::json!({
98                "error": format!("Owner not found: {}", owner_id)
99            }))
100        }
101        Err(e) => {
102            return HttpResponse::InternalServerError().json(serde_json::json!({
103                "error": format!("Failed to fetch owner: {}", e)
104            }))
105        }
106    };
107
108    // Check if the authenticated user owns this owner record
109    let owner_user_id = owner
110        .user_id
111        .as_ref()
112        .and_then(|id| Uuid::parse_str(id).ok());
113    if owner_user_id != Some(auth.user_id) {
114        return HttpResponse::Forbidden().json(serde_json::json!({
115            "error": "You can only view your own exchanges"
116        }));
117    }
118
119    match data
120        .local_exchange_use_cases
121        .list_owner_exchanges(owner_id)
122        .await
123    {
124        Ok(exchanges) => HttpResponse::Ok().json(exchanges),
125        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
126    }
127}
128
129/// GET /api/v1/buildings/:building_id/exchanges/type/:exchange_type
130/// List exchanges by type (Service, ObjectLoan, SharedPurchase)
131#[get("/buildings/{building_id}/exchanges/type/{exchange_type}")]
132pub async fn list_exchanges_by_type(
133    data: web::Data<AppState>,
134    _auth: AuthenticatedUser,
135    path: web::Path<(Uuid, String)>,
136) -> impl Responder {
137    let (building_id, exchange_type_str) = path.into_inner();
138
139    // Parse exchange type
140    let exchange_type = match exchange_type_str.as_str() {
141        "Service" => ExchangeType::Service,
142        "ObjectLoan" => ExchangeType::ObjectLoan,
143        "SharedPurchase" => ExchangeType::SharedPurchase,
144        _ => {
145            return HttpResponse::BadRequest().json(serde_json::json!({
146                "error": "Invalid exchange type. Must be Service, ObjectLoan, or SharedPurchase"
147            }));
148        }
149    };
150
151    match data
152        .local_exchange_use_cases
153        .list_exchanges_by_type(building_id, exchange_type)
154        .await
155    {
156        Ok(exchanges) => HttpResponse::Ok().json(exchanges),
157        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
158    }
159}
160
161/// POST /api/v1/exchanges/:id/request
162/// Request an exchange (Offered → Requested)
163#[post("/exchanges/{id}/request")]
164pub async fn request_exchange(
165    data: web::Data<AppState>,
166    auth: AuthenticatedUser,
167    id: web::Path<Uuid>,
168    request: web::Json<RequestExchangeDto>,
169) -> impl Responder {
170    match data
171        .local_exchange_use_cases
172        .request_exchange(id.into_inner(), auth.user_id, request.into_inner())
173        .await
174    {
175        Ok(exchange) => HttpResponse::Ok().json(exchange),
176        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
177    }
178}
179
180/// POST /api/v1/exchanges/:id/start
181/// Start an exchange (Requested → InProgress)
182/// Only provider can start
183#[post("/exchanges/{id}/start")]
184pub async fn start_exchange(
185    data: web::Data<AppState>,
186    auth: AuthenticatedUser,
187    id: web::Path<Uuid>,
188) -> impl Responder {
189    match data
190        .local_exchange_use_cases
191        .start_exchange(id.into_inner(), auth.user_id)
192        .await
193    {
194        Ok(exchange) => HttpResponse::Ok().json(exchange),
195        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
196    }
197}
198
199/// POST /api/v1/exchanges/:id/complete
200/// Complete an exchange (InProgress → Completed)
201/// Updates credit balances automatically
202#[post("/exchanges/{id}/complete")]
203pub async fn complete_exchange(
204    data: web::Data<AppState>,
205    auth: AuthenticatedUser,
206    id: web::Path<Uuid>,
207    request: web::Json<CompleteExchangeDto>,
208) -> impl Responder {
209    match data
210        .local_exchange_use_cases
211        .complete_exchange(id.into_inner(), auth.user_id, request.into_inner())
212        .await
213    {
214        Ok(exchange) => HttpResponse::Ok().json(exchange),
215        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
216    }
217}
218
219/// POST /api/v1/exchanges/:id/cancel
220/// Cancel an exchange
221#[post("/exchanges/{id}/cancel")]
222pub async fn cancel_exchange(
223    data: web::Data<AppState>,
224    auth: AuthenticatedUser,
225    id: web::Path<Uuid>,
226    request: web::Json<CancelExchangeDto>,
227) -> impl Responder {
228    match data
229        .local_exchange_use_cases
230        .cancel_exchange(id.into_inner(), auth.user_id, request.into_inner())
231        .await
232    {
233        Ok(exchange) => HttpResponse::Ok().json(exchange),
234        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
235    }
236}
237
238/// PUT /api/v1/exchanges/:id/rate-provider
239/// Rate the provider (by requester)
240#[put("/exchanges/{id}/rate-provider")]
241pub async fn rate_provider(
242    data: web::Data<AppState>,
243    auth: AuthenticatedUser,
244    id: web::Path<Uuid>,
245    request: web::Json<RateExchangeDto>,
246) -> impl Responder {
247    match data
248        .local_exchange_use_cases
249        .rate_provider(id.into_inner(), auth.user_id, request.into_inner())
250        .await
251    {
252        Ok(exchange) => HttpResponse::Ok().json(exchange),
253        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
254    }
255}
256
257/// PUT /api/v1/exchanges/:id/rate-requester
258/// Rate the requester (by provider)
259#[put("/exchanges/{id}/rate-requester")]
260pub async fn rate_requester(
261    data: web::Data<AppState>,
262    auth: AuthenticatedUser,
263    id: web::Path<Uuid>,
264    request: web::Json<RateExchangeDto>,
265) -> impl Responder {
266    match data
267        .local_exchange_use_cases
268        .rate_requester(id.into_inner(), auth.user_id, request.into_inner())
269        .await
270    {
271        Ok(exchange) => HttpResponse::Ok().json(exchange),
272        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
273    }
274}
275
276/// DELETE /api/v1/exchanges/:id
277/// Delete an exchange (only provider, not completed)
278#[delete("/exchanges/{id}")]
279pub async fn delete_exchange(
280    data: web::Data<AppState>,
281    auth: AuthenticatedUser,
282    id: web::Path<Uuid>,
283) -> impl Responder {
284    match data
285        .local_exchange_use_cases
286        .delete_exchange(id.into_inner(), auth.user_id)
287        .await
288    {
289        Ok(_) => HttpResponse::NoContent().finish(),
290        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
291    }
292}
293
294/// GET /api/v1/owners/:owner_id/buildings/:building_id/credit-balance
295/// Get credit balance for an owner in a building
296#[get("/owners/{owner_id}/buildings/{building_id}/credit-balance")]
297pub async fn get_credit_balance(
298    data: web::Data<AppState>,
299    auth: AuthenticatedUser,
300    path: web::Path<(Uuid, Uuid)>,
301) -> impl Responder {
302    let (owner_id, building_id) = path.into_inner();
303
304    // Authorization: users can only view their own credit balance
305    let owner = match data.owner_use_cases.get_owner(owner_id).await {
306        Ok(Some(owner)) => owner,
307        Ok(None) => {
308            return HttpResponse::NotFound().json(serde_json::json!({
309                "error": format!("Owner not found: {}", owner_id)
310            }))
311        }
312        Err(e) => {
313            return HttpResponse::InternalServerError().json(serde_json::json!({
314                "error": format!("Failed to fetch owner: {}", e)
315            }))
316        }
317    };
318
319    let owner_user_id = owner
320        .user_id
321        .as_ref()
322        .and_then(|id| Uuid::parse_str(id).ok());
323    if owner_user_id != Some(auth.user_id) {
324        return HttpResponse::Forbidden().json(serde_json::json!({
325            "error": "You can only view your own credit balance"
326        }));
327    }
328
329    match data
330        .local_exchange_use_cases
331        .get_credit_balance(owner_id, building_id)
332        .await
333    {
334        Ok(balance) => HttpResponse::Ok().json(balance),
335        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
336    }
337}
338
339/// GET /api/v1/buildings/:building_id/leaderboard
340/// Get leaderboard (top contributors)
341#[get("/buildings/{building_id}/leaderboard")]
342pub async fn get_leaderboard(
343    data: web::Data<AppState>,
344    _auth: AuthenticatedUser,
345    building_id: web::Path<Uuid>,
346    query: web::Query<std::collections::HashMap<String, String>>,
347) -> impl Responder {
348    let limit = query
349        .get("limit")
350        .and_then(|l| l.parse::<i32>().ok())
351        .unwrap_or(10);
352
353    match data
354        .local_exchange_use_cases
355        .get_leaderboard(building_id.into_inner(), limit)
356        .await
357    {
358        Ok(leaderboard) => HttpResponse::Ok().json(leaderboard),
359        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
360    }
361}
362
363/// GET /api/v1/buildings/:building_id/sel-statistics
364/// Get SEL statistics for a building
365#[get("/buildings/{building_id}/sel-statistics")]
366pub async fn get_sel_statistics(
367    data: web::Data<AppState>,
368    _auth: AuthenticatedUser,
369    building_id: web::Path<Uuid>,
370) -> impl Responder {
371    match data
372        .local_exchange_use_cases
373        .get_statistics(building_id.into_inner())
374        .await
375    {
376        Ok(stats) => HttpResponse::Ok().json(stats),
377        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
378    }
379}
380
381/// GET /api/v1/owners/:owner_id/exchange-summary
382/// Get owner exchange summary
383#[get("/owners/{owner_id}/exchange-summary")]
384pub async fn get_owner_summary(
385    data: web::Data<AppState>,
386    auth: AuthenticatedUser,
387    owner_id: web::Path<Uuid>,
388) -> impl Responder {
389    let owner_id = owner_id.into_inner();
390
391    // Authorization: users can only view their own exchange summary
392    let owner = match data.owner_use_cases.get_owner(owner_id).await {
393        Ok(Some(owner)) => owner,
394        Ok(None) => {
395            return HttpResponse::NotFound().json(serde_json::json!({
396                "error": format!("Owner not found: {}", owner_id)
397            }))
398        }
399        Err(e) => {
400            return HttpResponse::InternalServerError().json(serde_json::json!({
401                "error": format!("Failed to fetch owner: {}", e)
402            }))
403        }
404    };
405
406    let owner_user_id = owner
407        .user_id
408        .as_ref()
409        .and_then(|id| Uuid::parse_str(id).ok());
410    if owner_user_id != Some(auth.user_id) {
411        return HttpResponse::Forbidden().json(serde_json::json!({
412            "error": "You can only view your own exchange summary"
413        }));
414    }
415
416    match data
417        .local_exchange_use_cases
418        .get_owner_summary(owner_id)
419        .await
420    {
421        Ok(summary) => HttpResponse::Ok().json(summary),
422        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
423    }
424}