koprogo_api/infrastructure/web/handlers/
payment_handlers.rs

1use crate::application::dto::{CreatePaymentRequest, RefundPaymentRequest};
2use crate::domain::entities::TransactionStatus;
3use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
4use crate::infrastructure::web::{AppState, AuthenticatedUser};
5use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
6use uuid::Uuid;
7
8// ==================== Payment CRUD Endpoints ====================
9
10#[utoipa::path(
11    post,
12    path = "/payments",
13    tag = "Payments",
14    summary = "Create a payment",
15    request_body = CreatePaymentRequest,
16    responses(
17        (status = 201, description = "Payment created"),
18        (status = 400, description = "Bad request"),
19        (status = 401, description = "Unauthorized"),
20    ),
21    security(("bearer_auth" = []))
22)]
23#[post("/payments")]
24pub async fn create_payment(
25    state: web::Data<AppState>,
26    user: AuthenticatedUser,
27    request: web::Json<CreatePaymentRequest>,
28) -> impl Responder {
29    let organization_id = match user.require_organization() {
30        Ok(org_id) => org_id,
31        Err(e) => {
32            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
33        }
34    };
35
36    match state
37        .payment_use_cases
38        .create_payment(organization_id, request.into_inner())
39        .await
40    {
41        Ok(payment) => {
42            AuditLogEntry::new(
43                AuditEventType::PaymentCreated,
44                Some(user.user_id),
45                Some(organization_id),
46            )
47            .with_resource("Payment", payment.id)
48            .log();
49
50            HttpResponse::Created().json(payment)
51        }
52        Err(err) => {
53            AuditLogEntry::new(
54                AuditEventType::PaymentCreated,
55                Some(user.user_id),
56                Some(organization_id),
57            )
58            .with_error(err.clone())
59            .log();
60
61            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
62        }
63    }
64}
65
66#[utoipa::path(
67    get,
68    path = "/payments/{id}",
69    tag = "Payments",
70    summary = "Get a payment by ID",
71    params(("id" = Uuid, Path, description = "Payment ID")),
72    responses(
73        (status = 200, description = "Payment found"),
74        (status = 404, description = "Payment not found"),
75        (status = 500, description = "Internal server error"),
76    ),
77    security(("bearer_auth" = []))
78)]
79#[get("/payments/{id}")]
80pub async fn get_payment(
81    state: web::Data<AppState>,
82    user: AuthenticatedUser,
83    id: web::Path<Uuid>,
84) -> impl Responder {
85    match state.payment_use_cases.get_payment(*id).await {
86        Ok(Some(payment)) => {
87            // Multi-tenant isolation: verify payment belongs to user's organization
88            if let Err(e) = user.verify_org_access(payment.organization_id) {
89                return HttpResponse::Forbidden().json(serde_json::json!({ "error": e }));
90            }
91            HttpResponse::Ok().json(payment)
92        }
93        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
94            "error": "Payment not found"
95        })),
96        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
97    }
98}
99
100#[utoipa::path(
101    get,
102    path = "/payments/stripe/{stripe_payment_intent_id}",
103    tag = "Payments",
104    summary = "Get a payment by Stripe payment intent ID",
105    params(("stripe_payment_intent_id" = String, Path, description = "Stripe payment intent ID")),
106    responses(
107        (status = 200, description = "Payment found"),
108        (status = 404, description = "Payment not found"),
109        (status = 500, description = "Internal server error"),
110    ),
111    security(("bearer_auth" = []))
112)]
113#[get("/payments/stripe/{stripe_payment_intent_id}")]
114pub async fn get_payment_by_stripe_intent(
115    state: web::Data<AppState>,
116    stripe_payment_intent_id: web::Path<String>,
117) -> impl Responder {
118    match state
119        .payment_use_cases
120        .get_payment_by_stripe_intent(&stripe_payment_intent_id)
121        .await
122    {
123        Ok(Some(payment)) => HttpResponse::Ok().json(payment),
124        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
125            "error": "Payment not found"
126        })),
127        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
128    }
129}
130
131#[utoipa::path(
132    get,
133    path = "/owners/{owner_id}/payments",
134    tag = "Payments",
135    summary = "List all payments for an owner",
136    params(("owner_id" = Uuid, Path, description = "Owner ID")),
137    responses(
138        (status = 200, description = "List of owner payments"),
139        (status = 500, description = "Internal server error"),
140    ),
141    security(("bearer_auth" = []))
142)]
143#[get("/owners/{owner_id}/payments")]
144pub async fn list_owner_payments(
145    state: web::Data<AppState>,
146    owner_id: web::Path<Uuid>,
147) -> impl Responder {
148    match state.payment_use_cases.list_owner_payments(*owner_id).await {
149        Ok(payments) => HttpResponse::Ok().json(payments),
150        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
151    }
152}
153
154#[utoipa::path(
155    get,
156    path = "/buildings/{building_id}/payments",
157    tag = "Payments",
158    summary = "List all payments for a building",
159    params(("building_id" = Uuid, Path, description = "Building ID")),
160    responses(
161        (status = 200, description = "List of building payments"),
162        (status = 500, description = "Internal server error"),
163    ),
164    security(("bearer_auth" = []))
165)]
166#[get("/buildings/{building_id}/payments")]
167pub async fn list_building_payments(
168    state: web::Data<AppState>,
169    building_id: web::Path<Uuid>,
170) -> impl Responder {
171    match state
172        .payment_use_cases
173        .list_building_payments(*building_id)
174        .await
175    {
176        Ok(payments) => HttpResponse::Ok().json(payments),
177        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
178    }
179}
180
181#[utoipa::path(
182    get,
183    path = "/expenses/{expense_id}/payments",
184    tag = "Payments",
185    summary = "List all payments for an expense",
186    params(("expense_id" = Uuid, Path, description = "Expense ID")),
187    responses(
188        (status = 200, description = "List of expense payments"),
189        (status = 500, description = "Internal server error"),
190    ),
191    security(("bearer_auth" = []))
192)]
193#[get("/expenses/{expense_id}/payments")]
194pub async fn list_expense_payments(
195    state: web::Data<AppState>,
196    expense_id: web::Path<Uuid>,
197) -> impl Responder {
198    match state
199        .payment_use_cases
200        .list_expense_payments(*expense_id)
201        .await
202    {
203        Ok(payments) => HttpResponse::Ok().json(payments),
204        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
205    }
206}
207
208#[utoipa::path(
209    get,
210    path = "/organizations/{organization_id}/payments",
211    tag = "Payments",
212    summary = "List all payments for an organization",
213    params(("organization_id" = Uuid, Path, description = "Organization ID")),
214    responses(
215        (status = 200, description = "List of organization payments"),
216        (status = 500, description = "Internal server error"),
217    ),
218    security(("bearer_auth" = []))
219)]
220#[get("/organizations/{organization_id}/payments")]
221pub async fn list_organization_payments(
222    state: web::Data<AppState>,
223    user: AuthenticatedUser,
224    organization_id: web::Path<Uuid>,
225) -> impl Responder {
226    if let Err(e) = user.verify_org_access(*organization_id) {
227        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
228    }
229    match state
230        .payment_use_cases
231        .list_organization_payments(*organization_id)
232        .await
233    {
234        Ok(payments) => HttpResponse::Ok().json(payments),
235        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
236    }
237}
238
239#[utoipa::path(
240    get,
241    path = "/payments/status/{status}",
242    tag = "Payments",
243    summary = "List payments by transaction status",
244    params(("status" = String, Path, description = "Transaction status (pending, processing, requires_action, succeeded, failed, cancelled, refunded)")),
245    responses(
246        (status = 200, description = "List of payments with given status"),
247        (status = 400, description = "Invalid status value"),
248        (status = 401, description = "Unauthorized"),
249        (status = 500, description = "Internal server error"),
250    ),
251    security(("bearer_auth" = []))
252)]
253#[get("/payments/status/{status}")]
254pub async fn list_payments_by_status(
255    state: web::Data<AppState>,
256    user: AuthenticatedUser,
257    status_str: web::Path<String>,
258) -> impl Responder {
259    let organization_id = match user.require_organization() {
260        Ok(org_id) => org_id,
261        Err(e) => {
262            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
263        }
264    };
265
266    // Parse status string to TransactionStatus enum
267    let status = match status_str.as_str() {
268        "pending" => TransactionStatus::Pending,
269        "processing" => TransactionStatus::Processing,
270        "requires_action" => TransactionStatus::RequiresAction,
271        "succeeded" => TransactionStatus::Succeeded,
272        "failed" => TransactionStatus::Failed,
273        "cancelled" => TransactionStatus::Cancelled,
274        "refunded" => TransactionStatus::Refunded,
275        _ => {
276            return HttpResponse::BadRequest().json(serde_json::json!({
277                "error": "Invalid status. Must be one of: pending, processing, requires_action, succeeded, failed, cancelled, refunded"
278            }))
279        }
280    };
281
282    match state
283        .payment_use_cases
284        .list_payments_by_status(organization_id, status)
285        .await
286    {
287        Ok(payments) => HttpResponse::Ok().json(payments),
288        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
289    }
290}
291
292#[utoipa::path(
293    get,
294    path = "/payments/pending",
295    tag = "Payments",
296    summary = "List all pending payments for the organization",
297    responses(
298        (status = 200, description = "List of pending payments"),
299        (status = 401, description = "Unauthorized"),
300        (status = 500, description = "Internal server error"),
301    ),
302    security(("bearer_auth" = []))
303)]
304#[get("/payments/pending")]
305pub async fn list_pending_payments(
306    state: web::Data<AppState>,
307    user: AuthenticatedUser,
308) -> impl Responder {
309    let organization_id = match user.require_organization() {
310        Ok(org_id) => org_id,
311        Err(e) => {
312            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
313        }
314    };
315
316    match state
317        .payment_use_cases
318        .list_pending_payments(organization_id)
319        .await
320    {
321        Ok(payments) => HttpResponse::Ok().json(payments),
322        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
323    }
324}
325
326#[utoipa::path(
327    get,
328    path = "/payments/failed",
329    tag = "Payments",
330    summary = "List all failed payments for the organization",
331    responses(
332        (status = 200, description = "List of failed payments"),
333        (status = 401, description = "Unauthorized"),
334        (status = 500, description = "Internal server error"),
335    ),
336    security(("bearer_auth" = []))
337)]
338#[get("/payments/failed")]
339pub async fn list_failed_payments(
340    state: web::Data<AppState>,
341    user: AuthenticatedUser,
342) -> impl Responder {
343    let organization_id = match user.require_organization() {
344        Ok(org_id) => org_id,
345        Err(e) => {
346            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
347        }
348    };
349
350    match state
351        .payment_use_cases
352        .list_failed_payments(organization_id)
353        .await
354    {
355        Ok(payments) => HttpResponse::Ok().json(payments),
356        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
357    }
358}
359
360// ==================== Payment Status Update Endpoints ====================
361
362#[utoipa::path(
363    put,
364    path = "/payments/{id}/processing",
365    tag = "Payments",
366    summary = "Mark a payment as processing",
367    params(("id" = Uuid, Path, description = "Payment ID")),
368    responses(
369        (status = 200, description = "Payment marked as processing"),
370        (status = 400, description = "Invalid state transition"),
371        (status = 401, description = "Unauthorized"),
372    ),
373    security(("bearer_auth" = []))
374)]
375#[put("/payments/{id}/processing")]
376pub async fn mark_payment_processing(
377    state: web::Data<AppState>,
378    user: AuthenticatedUser,
379    id: web::Path<Uuid>,
380) -> impl Responder {
381    let organization_id = match user.require_organization() {
382        Ok(org_id) => org_id,
383        Err(e) => {
384            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
385        }
386    };
387
388    match state.payment_use_cases.mark_processing(*id).await {
389        Ok(payment) => {
390            AuditLogEntry::new(
391                AuditEventType::PaymentProcessing,
392                Some(user.user_id),
393                Some(organization_id),
394            )
395            .with_resource("Payment", payment.id)
396            .log();
397
398            HttpResponse::Ok().json(payment)
399        }
400        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
401    }
402}
403
404#[utoipa::path(
405    put,
406    path = "/payments/{id}/requires-action",
407    tag = "Payments",
408    summary = "Mark a payment as requiring action",
409    params(("id" = Uuid, Path, description = "Payment ID")),
410    responses(
411        (status = 200, description = "Payment marked as requires action"),
412        (status = 400, description = "Invalid state transition"),
413        (status = 401, description = "Unauthorized"),
414    ),
415    security(("bearer_auth" = []))
416)]
417#[put("/payments/{id}/requires-action")]
418pub async fn mark_payment_requires_action(
419    state: web::Data<AppState>,
420    user: AuthenticatedUser,
421    id: web::Path<Uuid>,
422) -> impl Responder {
423    let organization_id = match user.require_organization() {
424        Ok(org_id) => org_id,
425        Err(e) => {
426            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
427        }
428    };
429
430    match state.payment_use_cases.mark_requires_action(*id).await {
431        Ok(payment) => {
432            AuditLogEntry::new(
433                AuditEventType::PaymentRequiresAction,
434                Some(user.user_id),
435                Some(organization_id),
436            )
437            .with_resource("Payment", payment.id)
438            .log();
439
440            HttpResponse::Ok().json(payment)
441        }
442        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
443    }
444}
445
446#[utoipa::path(
447    put,
448    path = "/payments/{id}/succeeded",
449    tag = "Payments",
450    summary = "Mark a payment as succeeded",
451    params(("id" = Uuid, Path, description = "Payment ID")),
452    responses(
453        (status = 200, description = "Payment marked as succeeded"),
454        (status = 400, description = "Invalid state transition"),
455        (status = 401, description = "Unauthorized"),
456    ),
457    security(("bearer_auth" = []))
458)]
459#[put("/payments/{id}/succeeded")]
460pub async fn mark_payment_succeeded(
461    state: web::Data<AppState>,
462    user: AuthenticatedUser,
463    id: web::Path<Uuid>,
464) -> impl Responder {
465    let organization_id = match user.require_organization() {
466        Ok(org_id) => org_id,
467        Err(e) => {
468            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
469        }
470    };
471
472    match state.payment_use_cases.mark_succeeded(*id).await {
473        Ok(payment) => {
474            AuditLogEntry::new(
475                AuditEventType::PaymentSucceeded,
476                Some(user.user_id),
477                Some(organization_id),
478            )
479            .with_resource("Payment", payment.id)
480            .log();
481
482            HttpResponse::Ok().json(payment)
483        }
484        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
485    }
486}
487
488#[utoipa::path(
489    put,
490    path = "/payments/{id}/failed",
491    tag = "Payments",
492    summary = "Mark a payment as failed",
493    params(("id" = Uuid, Path, description = "Payment ID")),
494    request_body = inline(serde_json::Value),
495    responses(
496        (status = 200, description = "Payment marked as failed"),
497        (status = 400, description = "Invalid state transition"),
498        (status = 401, description = "Unauthorized"),
499    ),
500    security(("bearer_auth" = []))
501)]
502#[put("/payments/{id}/failed")]
503pub async fn mark_payment_failed(
504    state: web::Data<AppState>,
505    user: AuthenticatedUser,
506    id: web::Path<Uuid>,
507    request: web::Json<serde_json::Value>,
508) -> impl Responder {
509    let organization_id = match user.require_organization() {
510        Ok(org_id) => org_id,
511        Err(e) => {
512            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
513        }
514    };
515
516    let reason = request
517        .get("reason")
518        .and_then(|v| v.as_str())
519        .unwrap_or("Unknown failure reason")
520        .to_string();
521
522    match state.payment_use_cases.mark_failed(*id, reason).await {
523        Ok(payment) => {
524            AuditLogEntry::new(
525                AuditEventType::PaymentFailed,
526                Some(user.user_id),
527                Some(organization_id),
528            )
529            .with_resource("Payment", payment.id)
530            .log();
531
532            HttpResponse::Ok().json(payment)
533        }
534        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
535    }
536}
537
538#[utoipa::path(
539    put,
540    path = "/payments/{id}/cancelled",
541    tag = "Payments",
542    summary = "Mark a payment as cancelled",
543    params(("id" = Uuid, Path, description = "Payment ID")),
544    responses(
545        (status = 200, description = "Payment marked as cancelled"),
546        (status = 400, description = "Invalid state transition"),
547        (status = 401, description = "Unauthorized"),
548    ),
549    security(("bearer_auth" = []))
550)]
551#[put("/payments/{id}/cancelled")]
552pub async fn mark_payment_cancelled(
553    state: web::Data<AppState>,
554    user: AuthenticatedUser,
555    id: web::Path<Uuid>,
556) -> impl Responder {
557    let organization_id = match user.require_organization() {
558        Ok(org_id) => org_id,
559        Err(e) => {
560            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
561        }
562    };
563
564    match state.payment_use_cases.mark_cancelled(*id).await {
565        Ok(payment) => {
566            AuditLogEntry::new(
567                AuditEventType::PaymentCancelled,
568                Some(user.user_id),
569                Some(organization_id),
570            )
571            .with_resource("Payment", payment.id)
572            .log();
573
574            HttpResponse::Ok().json(payment)
575        }
576        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
577    }
578}
579
580#[utoipa::path(
581    post,
582    path = "/payments/{id}/refund",
583    tag = "Payments",
584    summary = "Refund a payment (partial or full)",
585    params(("id" = Uuid, Path, description = "Payment ID")),
586    request_body = RefundPaymentRequest,
587    responses(
588        (status = 200, description = "Payment refunded"),
589        (status = 400, description = "Refund not allowed or exceeds payment amount"),
590        (status = 401, description = "Unauthorized"),
591    ),
592    security(("bearer_auth" = []))
593)]
594#[post("/payments/{id}/refund")]
595pub async fn refund_payment(
596    state: web::Data<AppState>,
597    user: AuthenticatedUser,
598    id: web::Path<Uuid>,
599    request: web::Json<RefundPaymentRequest>,
600) -> impl Responder {
601    let organization_id = match user.require_organization() {
602        Ok(org_id) => org_id,
603        Err(e) => {
604            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
605        }
606    };
607
608    match state
609        .payment_use_cases
610        .refund_payment(*id, request.into_inner())
611        .await
612    {
613        Ok(payment) => {
614            AuditLogEntry::new(
615                AuditEventType::PaymentRefunded,
616                Some(user.user_id),
617                Some(organization_id),
618            )
619            .with_resource("Payment", payment.id)
620            .log();
621
622            HttpResponse::Ok().json(payment)
623        }
624        Err(err) => {
625            AuditLogEntry::new(
626                AuditEventType::PaymentRefunded,
627                Some(user.user_id),
628                Some(organization_id),
629            )
630            .with_error(err.clone())
631            .log();
632
633            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
634        }
635    }
636}
637
638#[utoipa::path(
639    delete,
640    path = "/payments/{id}",
641    tag = "Payments",
642    summary = "Delete a payment",
643    params(("id" = Uuid, Path, description = "Payment ID")),
644    responses(
645        (status = 204, description = "Payment deleted"),
646        (status = 401, description = "Unauthorized"),
647        (status = 404, description = "Payment not found"),
648        (status = 500, description = "Internal server error"),
649    ),
650    security(("bearer_auth" = []))
651)]
652#[delete("/payments/{id}")]
653pub async fn delete_payment(
654    state: web::Data<AppState>,
655    user: AuthenticatedUser,
656    id: web::Path<Uuid>,
657) -> impl Responder {
658    let organization_id = match user.require_organization() {
659        Ok(org_id) => org_id,
660        Err(e) => {
661            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
662        }
663    };
664
665    match state.payment_use_cases.delete_payment(*id).await {
666        Ok(true) => {
667            AuditLogEntry::new(
668                AuditEventType::PaymentDeleted,
669                Some(user.user_id),
670                Some(organization_id),
671            )
672            .with_resource("Payment", *id)
673            .log();
674
675            HttpResponse::NoContent().finish()
676        }
677        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
678            "error": "Payment not found"
679        })),
680        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
681    }
682}
683
684// ==================== Payment Statistics Endpoints ====================
685
686#[utoipa::path(
687    get,
688    path = "/owners/{owner_id}/payments/stats",
689    tag = "Payments",
690    summary = "Get payment statistics for an owner",
691    params(("owner_id" = Uuid, Path, description = "Owner ID")),
692    responses(
693        (status = 200, description = "Owner payment statistics"),
694        (status = 500, description = "Internal server error"),
695    ),
696    security(("bearer_auth" = []))
697)]
698#[get("/owners/{owner_id}/payments/stats")]
699pub async fn get_owner_payment_stats(
700    state: web::Data<AppState>,
701    owner_id: web::Path<Uuid>,
702) -> impl Responder {
703    match state
704        .payment_use_cases
705        .get_owner_payment_stats(*owner_id)
706        .await
707    {
708        Ok(stats) => HttpResponse::Ok().json(stats),
709        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
710    }
711}
712
713#[utoipa::path(
714    get,
715    path = "/buildings/{building_id}/payments/stats",
716    tag = "Payments",
717    summary = "Get payment statistics for a building",
718    params(("building_id" = Uuid, Path, description = "Building ID")),
719    responses(
720        (status = 200, description = "Building payment statistics"),
721        (status = 500, description = "Internal server error"),
722    ),
723    security(("bearer_auth" = []))
724)]
725#[get("/buildings/{building_id}/payments/stats")]
726pub async fn get_building_payment_stats(
727    state: web::Data<AppState>,
728    building_id: web::Path<Uuid>,
729) -> impl Responder {
730    match state
731        .payment_use_cases
732        .get_building_payment_stats(*building_id)
733        .await
734    {
735        Ok(stats) => HttpResponse::Ok().json(stats),
736        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
737    }
738}
739
740#[utoipa::path(
741    get,
742    path = "/expenses/{expense_id}/payments/total",
743    tag = "Payments",
744    summary = "Get total amount paid for an expense",
745    params(("expense_id" = Uuid, Path, description = "Expense ID")),
746    responses(
747        (status = 200, description = "Total paid amount in cents"),
748        (status = 500, description = "Internal server error"),
749    ),
750    security(("bearer_auth" = []))
751)]
752#[get("/expenses/{expense_id}/payments/total")]
753pub async fn get_expense_total_paid(
754    state: web::Data<AppState>,
755    expense_id: web::Path<Uuid>,
756) -> impl Responder {
757    match state
758        .payment_use_cases
759        .get_total_paid_for_expense(*expense_id)
760        .await
761    {
762        Ok(total) => HttpResponse::Ok().json(serde_json::json!({
763            "expense_id": *expense_id,
764            "total_paid_cents": total
765        })),
766        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
767    }
768}
769
770#[utoipa::path(
771    get,
772    path = "/owners/{owner_id}/payments/total",
773    tag = "Payments",
774    summary = "Get total amount paid by an owner",
775    params(("owner_id" = Uuid, Path, description = "Owner ID")),
776    responses(
777        (status = 200, description = "Total paid amount in cents"),
778        (status = 500, description = "Internal server error"),
779    ),
780    security(("bearer_auth" = []))
781)]
782#[get("/owners/{owner_id}/payments/total")]
783pub async fn get_owner_total_paid(
784    state: web::Data<AppState>,
785    owner_id: web::Path<Uuid>,
786) -> impl Responder {
787    match state
788        .payment_use_cases
789        .get_total_paid_by_owner(*owner_id)
790        .await
791    {
792        Ok(total) => HttpResponse::Ok().json(serde_json::json!({
793            "owner_id": *owner_id,
794            "total_paid_cents": total
795        })),
796        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
797    }
798}
799
800#[utoipa::path(
801    get,
802    path = "/buildings/{building_id}/payments/total",
803    tag = "Payments",
804    summary = "Get total amount paid for a building",
805    params(("building_id" = Uuid, Path, description = "Building ID")),
806    responses(
807        (status = 200, description = "Total paid amount in cents"),
808        (status = 500, description = "Internal server error"),
809    ),
810    security(("bearer_auth" = []))
811)]
812#[get("/buildings/{building_id}/payments/total")]
813pub async fn get_building_total_paid(
814    state: web::Data<AppState>,
815    building_id: web::Path<Uuid>,
816) -> impl Responder {
817    match state
818        .payment_use_cases
819        .get_total_paid_for_building(*building_id)
820        .await
821    {
822        Ok(total) => HttpResponse::Ok().json(serde_json::json!({
823            "building_id": *building_id,
824            "total_paid_cents": total
825        })),
826        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
827    }
828}