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/// Note: owner_id can be the actual owner ID or the user ID (frontend sends user ID)
297#[get("/owners/{owner_id}/buildings/{building_id}/credit-balance")]
298pub async fn get_credit_balance(
299    data: web::Data<AppState>,
300    auth: AuthenticatedUser,
301    path: web::Path<(Uuid, Uuid)>,
302) -> impl Responder {
303    let (path_id, building_id) = path.into_inner();
304
305    // Try to find owner by ID first, then by user_id as fallback
306    // (frontend sends user_id from auth store, not owner_id)
307    let owner = match data.owner_use_cases.get_owner(path_id).await {
308        Ok(Some(owner)) => owner,
309        Ok(None) => {
310            // Fallback: treat path_id as user_id and look up owner
311            match data.owner_use_cases.find_owner_by_user_id(path_id).await {
312                Ok(Some(owner)) => owner,
313                Ok(None) => {
314                    return HttpResponse::NotFound().json(serde_json::json!({
315                        "error": format!("Owner not found for id: {}", path_id)
316                    }))
317                }
318                Err(e) => {
319                    return HttpResponse::InternalServerError().json(serde_json::json!({
320                        "error": format!("Failed to fetch owner: {}", e)
321                    }))
322                }
323            }
324        }
325        Err(e) => {
326            return HttpResponse::InternalServerError().json(serde_json::json!({
327                "error": format!("Failed to fetch owner: {}", e)
328            }))
329        }
330    };
331
332    // Authorization: users can only view their own credit balance
333    let owner_user_id = owner
334        .user_id
335        .as_ref()
336        .and_then(|id| Uuid::parse_str(id).ok());
337    if owner_user_id != Some(auth.user_id) {
338        return HttpResponse::Forbidden().json(serde_json::json!({
339            "error": "You can only view your own credit balance"
340        }));
341    }
342
343    let owner_uuid = match Uuid::parse_str(&owner.id) {
344        Ok(id) => id,
345        Err(_) => {
346            return HttpResponse::InternalServerError().json(serde_json::json!({
347                "error": "Invalid owner ID format"
348            }))
349        }
350    };
351
352    match data
353        .local_exchange_use_cases
354        .get_credit_balance(owner_uuid, building_id)
355        .await
356    {
357        Ok(balance) => HttpResponse::Ok().json(balance),
358        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
359    }
360}
361
362/// GET /api/v1/buildings/:building_id/leaderboard
363/// Get leaderboard (top contributors)
364#[get("/buildings/{building_id}/leaderboard")]
365pub async fn get_leaderboard(
366    data: web::Data<AppState>,
367    _auth: AuthenticatedUser,
368    building_id: web::Path<Uuid>,
369    query: web::Query<std::collections::HashMap<String, String>>,
370) -> impl Responder {
371    let limit = query
372        .get("limit")
373        .and_then(|l| l.parse::<i32>().ok())
374        .unwrap_or(10);
375
376    match data
377        .local_exchange_use_cases
378        .get_leaderboard(building_id.into_inner(), limit)
379        .await
380    {
381        Ok(leaderboard) => HttpResponse::Ok().json(leaderboard),
382        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
383    }
384}
385
386/// GET /api/v1/buildings/:building_id/sel-statistics
387/// Get SEL statistics for a building
388#[get("/buildings/{building_id}/sel-statistics")]
389pub async fn get_sel_statistics(
390    data: web::Data<AppState>,
391    _auth: AuthenticatedUser,
392    building_id: web::Path<Uuid>,
393) -> impl Responder {
394    match data
395        .local_exchange_use_cases
396        .get_statistics(building_id.into_inner())
397        .await
398    {
399        Ok(stats) => HttpResponse::Ok().json(stats),
400        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
401    }
402}
403
404/// GET /api/v1/owners/:owner_id/exchange-summary
405/// Get owner exchange summary
406#[get("/owners/{owner_id}/exchange-summary")]
407pub async fn get_owner_summary(
408    data: web::Data<AppState>,
409    auth: AuthenticatedUser,
410    owner_id: web::Path<Uuid>,
411) -> impl Responder {
412    let owner_id = owner_id.into_inner();
413
414    // Authorization: users can only view their own exchange summary
415    let owner = match data.owner_use_cases.get_owner(owner_id).await {
416        Ok(Some(owner)) => owner,
417        Ok(None) => {
418            return HttpResponse::NotFound().json(serde_json::json!({
419                "error": format!("Owner not found: {}", owner_id)
420            }))
421        }
422        Err(e) => {
423            return HttpResponse::InternalServerError().json(serde_json::json!({
424                "error": format!("Failed to fetch owner: {}", e)
425            }))
426        }
427    };
428
429    let owner_user_id = owner
430        .user_id
431        .as_ref()
432        .and_then(|id| Uuid::parse_str(id).ok());
433    if owner_user_id != Some(auth.user_id) {
434        return HttpResponse::Forbidden().json(serde_json::json!({
435            "error": "You can only view your own exchange summary"
436        }));
437    }
438
439    match data
440        .local_exchange_use_cases
441        .get_owner_summary(owner_id)
442        .await
443    {
444        Ok(summary) => HttpResponse::Ok().json(summary),
445        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
446    }
447}