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    organization_id: web::Path<Uuid>,
224) -> impl Responder {
225    match state
226        .payment_use_cases
227        .list_organization_payments(*organization_id)
228        .await
229    {
230        Ok(payments) => HttpResponse::Ok().json(payments),
231        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
232    }
233}
234
235#[utoipa::path(
236    get,
237    path = "/payments/status/{status}",
238    tag = "Payments",
239    summary = "List payments by transaction status",
240    params(("status" = String, Path, description = "Transaction status (pending, processing, requires_action, succeeded, failed, cancelled, refunded)")),
241    responses(
242        (status = 200, description = "List of payments with given status"),
243        (status = 400, description = "Invalid status value"),
244        (status = 401, description = "Unauthorized"),
245        (status = 500, description = "Internal server error"),
246    ),
247    security(("bearer_auth" = []))
248)]
249#[get("/payments/status/{status}")]
250pub async fn list_payments_by_status(
251    state: web::Data<AppState>,
252    user: AuthenticatedUser,
253    status_str: web::Path<String>,
254) -> impl Responder {
255    let organization_id = match user.require_organization() {
256        Ok(org_id) => org_id,
257        Err(e) => {
258            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
259        }
260    };
261
262    // Parse status string to TransactionStatus enum
263    let status = match status_str.as_str() {
264        "pending" => TransactionStatus::Pending,
265        "processing" => TransactionStatus::Processing,
266        "requires_action" => TransactionStatus::RequiresAction,
267        "succeeded" => TransactionStatus::Succeeded,
268        "failed" => TransactionStatus::Failed,
269        "cancelled" => TransactionStatus::Cancelled,
270        "refunded" => TransactionStatus::Refunded,
271        _ => {
272            return HttpResponse::BadRequest().json(serde_json::json!({
273                "error": "Invalid status. Must be one of: pending, processing, requires_action, succeeded, failed, cancelled, refunded"
274            }))
275        }
276    };
277
278    match state
279        .payment_use_cases
280        .list_payments_by_status(organization_id, status)
281        .await
282    {
283        Ok(payments) => HttpResponse::Ok().json(payments),
284        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
285    }
286}
287
288#[utoipa::path(
289    get,
290    path = "/payments/pending",
291    tag = "Payments",
292    summary = "List all pending payments for the organization",
293    responses(
294        (status = 200, description = "List of pending payments"),
295        (status = 401, description = "Unauthorized"),
296        (status = 500, description = "Internal server error"),
297    ),
298    security(("bearer_auth" = []))
299)]
300#[get("/payments/pending")]
301pub async fn list_pending_payments(
302    state: web::Data<AppState>,
303    user: AuthenticatedUser,
304) -> impl Responder {
305    let organization_id = match user.require_organization() {
306        Ok(org_id) => org_id,
307        Err(e) => {
308            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
309        }
310    };
311
312    match state
313        .payment_use_cases
314        .list_pending_payments(organization_id)
315        .await
316    {
317        Ok(payments) => HttpResponse::Ok().json(payments),
318        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
319    }
320}
321
322#[utoipa::path(
323    get,
324    path = "/payments/failed",
325    tag = "Payments",
326    summary = "List all failed payments for the organization",
327    responses(
328        (status = 200, description = "List of failed payments"),
329        (status = 401, description = "Unauthorized"),
330        (status = 500, description = "Internal server error"),
331    ),
332    security(("bearer_auth" = []))
333)]
334#[get("/payments/failed")]
335pub async fn list_failed_payments(
336    state: web::Data<AppState>,
337    user: AuthenticatedUser,
338) -> impl Responder {
339    let organization_id = match user.require_organization() {
340        Ok(org_id) => org_id,
341        Err(e) => {
342            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
343        }
344    };
345
346    match state
347        .payment_use_cases
348        .list_failed_payments(organization_id)
349        .await
350    {
351        Ok(payments) => HttpResponse::Ok().json(payments),
352        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
353    }
354}
355
356// ==================== Payment Status Update Endpoints ====================
357
358#[utoipa::path(
359    put,
360    path = "/payments/{id}/processing",
361    tag = "Payments",
362    summary = "Mark a payment as processing",
363    params(("id" = Uuid, Path, description = "Payment ID")),
364    responses(
365        (status = 200, description = "Payment marked as processing"),
366        (status = 400, description = "Invalid state transition"),
367        (status = 401, description = "Unauthorized"),
368    ),
369    security(("bearer_auth" = []))
370)]
371#[put("/payments/{id}/processing")]
372pub async fn mark_payment_processing(
373    state: web::Data<AppState>,
374    user: AuthenticatedUser,
375    id: web::Path<Uuid>,
376) -> impl Responder {
377    let organization_id = match user.require_organization() {
378        Ok(org_id) => org_id,
379        Err(e) => {
380            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
381        }
382    };
383
384    match state.payment_use_cases.mark_processing(*id).await {
385        Ok(payment) => {
386            AuditLogEntry::new(
387                AuditEventType::PaymentProcessing,
388                Some(user.user_id),
389                Some(organization_id),
390            )
391            .with_resource("Payment", payment.id)
392            .log();
393
394            HttpResponse::Ok().json(payment)
395        }
396        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
397    }
398}
399
400#[utoipa::path(
401    put,
402    path = "/payments/{id}/requires-action",
403    tag = "Payments",
404    summary = "Mark a payment as requiring action",
405    params(("id" = Uuid, Path, description = "Payment ID")),
406    responses(
407        (status = 200, description = "Payment marked as requires action"),
408        (status = 400, description = "Invalid state transition"),
409        (status = 401, description = "Unauthorized"),
410    ),
411    security(("bearer_auth" = []))
412)]
413#[put("/payments/{id}/requires-action")]
414pub async fn mark_payment_requires_action(
415    state: web::Data<AppState>,
416    user: AuthenticatedUser,
417    id: web::Path<Uuid>,
418) -> impl Responder {
419    let organization_id = match user.require_organization() {
420        Ok(org_id) => org_id,
421        Err(e) => {
422            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
423        }
424    };
425
426    match state.payment_use_cases.mark_requires_action(*id).await {
427        Ok(payment) => {
428            AuditLogEntry::new(
429                AuditEventType::PaymentRequiresAction,
430                Some(user.user_id),
431                Some(organization_id),
432            )
433            .with_resource("Payment", payment.id)
434            .log();
435
436            HttpResponse::Ok().json(payment)
437        }
438        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
439    }
440}
441
442#[utoipa::path(
443    put,
444    path = "/payments/{id}/succeeded",
445    tag = "Payments",
446    summary = "Mark a payment as succeeded",
447    params(("id" = Uuid, Path, description = "Payment ID")),
448    responses(
449        (status = 200, description = "Payment marked as succeeded"),
450        (status = 400, description = "Invalid state transition"),
451        (status = 401, description = "Unauthorized"),
452    ),
453    security(("bearer_auth" = []))
454)]
455#[put("/payments/{id}/succeeded")]
456pub async fn mark_payment_succeeded(
457    state: web::Data<AppState>,
458    user: AuthenticatedUser,
459    id: web::Path<Uuid>,
460) -> impl Responder {
461    let organization_id = match user.require_organization() {
462        Ok(org_id) => org_id,
463        Err(e) => {
464            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
465        }
466    };
467
468    match state.payment_use_cases.mark_succeeded(*id).await {
469        Ok(payment) => {
470            AuditLogEntry::new(
471                AuditEventType::PaymentSucceeded,
472                Some(user.user_id),
473                Some(organization_id),
474            )
475            .with_resource("Payment", payment.id)
476            .log();
477
478            HttpResponse::Ok().json(payment)
479        }
480        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
481    }
482}
483
484#[utoipa::path(
485    put,
486    path = "/payments/{id}/failed",
487    tag = "Payments",
488    summary = "Mark a payment as failed",
489    params(("id" = Uuid, Path, description = "Payment ID")),
490    request_body = inline(serde_json::Value),
491    responses(
492        (status = 200, description = "Payment marked as failed"),
493        (status = 400, description = "Invalid state transition"),
494        (status = 401, description = "Unauthorized"),
495    ),
496    security(("bearer_auth" = []))
497)]
498#[put("/payments/{id}/failed")]
499pub async fn mark_payment_failed(
500    state: web::Data<AppState>,
501    user: AuthenticatedUser,
502    id: web::Path<Uuid>,
503    request: web::Json<serde_json::Value>,
504) -> impl Responder {
505    let organization_id = match user.require_organization() {
506        Ok(org_id) => org_id,
507        Err(e) => {
508            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
509        }
510    };
511
512    let reason = request
513        .get("reason")
514        .and_then(|v| v.as_str())
515        .unwrap_or("Unknown failure reason")
516        .to_string();
517
518    match state.payment_use_cases.mark_failed(*id, reason).await {
519        Ok(payment) => {
520            AuditLogEntry::new(
521                AuditEventType::PaymentFailed,
522                Some(user.user_id),
523                Some(organization_id),
524            )
525            .with_resource("Payment", payment.id)
526            .log();
527
528            HttpResponse::Ok().json(payment)
529        }
530        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
531    }
532}
533
534#[utoipa::path(
535    put,
536    path = "/payments/{id}/cancelled",
537    tag = "Payments",
538    summary = "Mark a payment as cancelled",
539    params(("id" = Uuid, Path, description = "Payment ID")),
540    responses(
541        (status = 200, description = "Payment marked as cancelled"),
542        (status = 400, description = "Invalid state transition"),
543        (status = 401, description = "Unauthorized"),
544    ),
545    security(("bearer_auth" = []))
546)]
547#[put("/payments/{id}/cancelled")]
548pub async fn mark_payment_cancelled(
549    state: web::Data<AppState>,
550    user: AuthenticatedUser,
551    id: web::Path<Uuid>,
552) -> impl Responder {
553    let organization_id = match user.require_organization() {
554        Ok(org_id) => org_id,
555        Err(e) => {
556            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
557        }
558    };
559
560    match state.payment_use_cases.mark_cancelled(*id).await {
561        Ok(payment) => {
562            AuditLogEntry::new(
563                AuditEventType::PaymentCancelled,
564                Some(user.user_id),
565                Some(organization_id),
566            )
567            .with_resource("Payment", payment.id)
568            .log();
569
570            HttpResponse::Ok().json(payment)
571        }
572        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
573    }
574}
575
576#[utoipa::path(
577    post,
578    path = "/payments/{id}/refund",
579    tag = "Payments",
580    summary = "Refund a payment (partial or full)",
581    params(("id" = Uuid, Path, description = "Payment ID")),
582    request_body = RefundPaymentRequest,
583    responses(
584        (status = 200, description = "Payment refunded"),
585        (status = 400, description = "Refund not allowed or exceeds payment amount"),
586        (status = 401, description = "Unauthorized"),
587    ),
588    security(("bearer_auth" = []))
589)]
590#[post("/payments/{id}/refund")]
591pub async fn refund_payment(
592    state: web::Data<AppState>,
593    user: AuthenticatedUser,
594    id: web::Path<Uuid>,
595    request: web::Json<RefundPaymentRequest>,
596) -> impl Responder {
597    let organization_id = match user.require_organization() {
598        Ok(org_id) => org_id,
599        Err(e) => {
600            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
601        }
602    };
603
604    match state
605        .payment_use_cases
606        .refund_payment(*id, request.into_inner())
607        .await
608    {
609        Ok(payment) => {
610            AuditLogEntry::new(
611                AuditEventType::PaymentRefunded,
612                Some(user.user_id),
613                Some(organization_id),
614            )
615            .with_resource("Payment", payment.id)
616            .log();
617
618            HttpResponse::Ok().json(payment)
619        }
620        Err(err) => {
621            AuditLogEntry::new(
622                AuditEventType::PaymentRefunded,
623                Some(user.user_id),
624                Some(organization_id),
625            )
626            .with_error(err.clone())
627            .log();
628
629            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
630        }
631    }
632}
633
634#[utoipa::path(
635    delete,
636    path = "/payments/{id}",
637    tag = "Payments",
638    summary = "Delete a payment",
639    params(("id" = Uuid, Path, description = "Payment ID")),
640    responses(
641        (status = 204, description = "Payment deleted"),
642        (status = 401, description = "Unauthorized"),
643        (status = 404, description = "Payment not found"),
644        (status = 500, description = "Internal server error"),
645    ),
646    security(("bearer_auth" = []))
647)]
648#[delete("/payments/{id}")]
649pub async fn delete_payment(
650    state: web::Data<AppState>,
651    user: AuthenticatedUser,
652    id: web::Path<Uuid>,
653) -> impl Responder {
654    let organization_id = match user.require_organization() {
655        Ok(org_id) => org_id,
656        Err(e) => {
657            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
658        }
659    };
660
661    match state.payment_use_cases.delete_payment(*id).await {
662        Ok(true) => {
663            AuditLogEntry::new(
664                AuditEventType::PaymentDeleted,
665                Some(user.user_id),
666                Some(organization_id),
667            )
668            .with_resource("Payment", *id)
669            .log();
670
671            HttpResponse::NoContent().finish()
672        }
673        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
674            "error": "Payment not found"
675        })),
676        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
677    }
678}
679
680// ==================== Payment Statistics Endpoints ====================
681
682#[utoipa::path(
683    get,
684    path = "/owners/{owner_id}/payments/stats",
685    tag = "Payments",
686    summary = "Get payment statistics for an owner",
687    params(("owner_id" = Uuid, Path, description = "Owner ID")),
688    responses(
689        (status = 200, description = "Owner payment statistics"),
690        (status = 500, description = "Internal server error"),
691    ),
692    security(("bearer_auth" = []))
693)]
694#[get("/owners/{owner_id}/payments/stats")]
695pub async fn get_owner_payment_stats(
696    state: web::Data<AppState>,
697    owner_id: web::Path<Uuid>,
698) -> impl Responder {
699    match state
700        .payment_use_cases
701        .get_owner_payment_stats(*owner_id)
702        .await
703    {
704        Ok(stats) => HttpResponse::Ok().json(stats),
705        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
706    }
707}
708
709#[utoipa::path(
710    get,
711    path = "/buildings/{building_id}/payments/stats",
712    tag = "Payments",
713    summary = "Get payment statistics for a building",
714    params(("building_id" = Uuid, Path, description = "Building ID")),
715    responses(
716        (status = 200, description = "Building payment statistics"),
717        (status = 500, description = "Internal server error"),
718    ),
719    security(("bearer_auth" = []))
720)]
721#[get("/buildings/{building_id}/payments/stats")]
722pub async fn get_building_payment_stats(
723    state: web::Data<AppState>,
724    building_id: web::Path<Uuid>,
725) -> impl Responder {
726    match state
727        .payment_use_cases
728        .get_building_payment_stats(*building_id)
729        .await
730    {
731        Ok(stats) => HttpResponse::Ok().json(stats),
732        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
733    }
734}
735
736#[utoipa::path(
737    get,
738    path = "/expenses/{expense_id}/payments/total",
739    tag = "Payments",
740    summary = "Get total amount paid for an expense",
741    params(("expense_id" = Uuid, Path, description = "Expense ID")),
742    responses(
743        (status = 200, description = "Total paid amount in cents"),
744        (status = 500, description = "Internal server error"),
745    ),
746    security(("bearer_auth" = []))
747)]
748#[get("/expenses/{expense_id}/payments/total")]
749pub async fn get_expense_total_paid(
750    state: web::Data<AppState>,
751    expense_id: web::Path<Uuid>,
752) -> impl Responder {
753    match state
754        .payment_use_cases
755        .get_total_paid_for_expense(*expense_id)
756        .await
757    {
758        Ok(total) => HttpResponse::Ok().json(serde_json::json!({
759            "expense_id": *expense_id,
760            "total_paid_cents": total
761        })),
762        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
763    }
764}
765
766#[utoipa::path(
767    get,
768    path = "/owners/{owner_id}/payments/total",
769    tag = "Payments",
770    summary = "Get total amount paid by an owner",
771    params(("owner_id" = Uuid, Path, description = "Owner ID")),
772    responses(
773        (status = 200, description = "Total paid amount in cents"),
774        (status = 500, description = "Internal server error"),
775    ),
776    security(("bearer_auth" = []))
777)]
778#[get("/owners/{owner_id}/payments/total")]
779pub async fn get_owner_total_paid(
780    state: web::Data<AppState>,
781    owner_id: web::Path<Uuid>,
782) -> impl Responder {
783    match state
784        .payment_use_cases
785        .get_total_paid_by_owner(*owner_id)
786        .await
787    {
788        Ok(total) => HttpResponse::Ok().json(serde_json::json!({
789            "owner_id": *owner_id,
790            "total_paid_cents": total
791        })),
792        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
793    }
794}
795
796#[utoipa::path(
797    get,
798    path = "/buildings/{building_id}/payments/total",
799    tag = "Payments",
800    summary = "Get total amount paid for a building",
801    params(("building_id" = Uuid, Path, description = "Building ID")),
802    responses(
803        (status = 200, description = "Total paid amount in cents"),
804        (status = 500, description = "Internal server error"),
805    ),
806    security(("bearer_auth" = []))
807)]
808#[get("/buildings/{building_id}/payments/total")]
809pub async fn get_building_total_paid(
810    state: web::Data<AppState>,
811    building_id: web::Path<Uuid>,
812) -> impl Responder {
813    match state
814        .payment_use_cases
815        .get_total_paid_for_building(*building_id)
816        .await
817    {
818        Ok(total) => HttpResponse::Ok().json(serde_json::json!({
819            "building_id": *building_id,
820            "total_paid_cents": total
821        })),
822        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
823    }
824}