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#[post("/payments")]
11pub async fn create_payment(
12    state: web::Data<AppState>,
13    user: AuthenticatedUser,
14    request: web::Json<CreatePaymentRequest>,
15) -> impl Responder {
16    let organization_id = match user.require_organization() {
17        Ok(org_id) => org_id,
18        Err(e) => {
19            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
20        }
21    };
22
23    match state
24        .payment_use_cases
25        .create_payment(organization_id, request.into_inner())
26        .await
27    {
28        Ok(payment) => {
29            AuditLogEntry::new(
30                AuditEventType::PaymentCreated,
31                Some(user.user_id),
32                Some(organization_id),
33            )
34            .with_resource("Payment", payment.id)
35            .log();
36
37            HttpResponse::Created().json(payment)
38        }
39        Err(err) => {
40            AuditLogEntry::new(
41                AuditEventType::PaymentCreated,
42                Some(user.user_id),
43                Some(organization_id),
44            )
45            .with_error(err.clone())
46            .log();
47
48            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
49        }
50    }
51}
52
53#[get("/payments/{id}")]
54pub async fn get_payment(state: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
55    match state.payment_use_cases.get_payment(*id).await {
56        Ok(Some(payment)) => HttpResponse::Ok().json(payment),
57        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
58            "error": "Payment not found"
59        })),
60        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
61    }
62}
63
64#[get("/payments/stripe/{stripe_payment_intent_id}")]
65pub async fn get_payment_by_stripe_intent(
66    state: web::Data<AppState>,
67    stripe_payment_intent_id: web::Path<String>,
68) -> impl Responder {
69    match state
70        .payment_use_cases
71        .get_payment_by_stripe_intent(&stripe_payment_intent_id)
72        .await
73    {
74        Ok(Some(payment)) => HttpResponse::Ok().json(payment),
75        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
76            "error": "Payment not found"
77        })),
78        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
79    }
80}
81
82#[get("/owners/{owner_id}/payments")]
83pub async fn list_owner_payments(
84    state: web::Data<AppState>,
85    owner_id: web::Path<Uuid>,
86) -> impl Responder {
87    match state.payment_use_cases.list_owner_payments(*owner_id).await {
88        Ok(payments) => HttpResponse::Ok().json(payments),
89        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
90    }
91}
92
93#[get("/buildings/{building_id}/payments")]
94pub async fn list_building_payments(
95    state: web::Data<AppState>,
96    building_id: web::Path<Uuid>,
97) -> impl Responder {
98    match state
99        .payment_use_cases
100        .list_building_payments(*building_id)
101        .await
102    {
103        Ok(payments) => HttpResponse::Ok().json(payments),
104        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
105    }
106}
107
108#[get("/expenses/{expense_id}/payments")]
109pub async fn list_expense_payments(
110    state: web::Data<AppState>,
111    expense_id: web::Path<Uuid>,
112) -> impl Responder {
113    match state
114        .payment_use_cases
115        .list_expense_payments(*expense_id)
116        .await
117    {
118        Ok(payments) => HttpResponse::Ok().json(payments),
119        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
120    }
121}
122
123#[get("/organizations/{organization_id}/payments")]
124pub async fn list_organization_payments(
125    state: web::Data<AppState>,
126    organization_id: web::Path<Uuid>,
127) -> impl Responder {
128    match state
129        .payment_use_cases
130        .list_organization_payments(*organization_id)
131        .await
132    {
133        Ok(payments) => HttpResponse::Ok().json(payments),
134        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
135    }
136}
137
138#[get("/payments/status/{status}")]
139pub async fn list_payments_by_status(
140    state: web::Data<AppState>,
141    user: AuthenticatedUser,
142    status_str: web::Path<String>,
143) -> impl Responder {
144    let organization_id = match user.require_organization() {
145        Ok(org_id) => org_id,
146        Err(e) => {
147            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
148        }
149    };
150
151    // Parse status string to TransactionStatus enum
152    let status = match status_str.as_str() {
153        "pending" => TransactionStatus::Pending,
154        "processing" => TransactionStatus::Processing,
155        "requires_action" => TransactionStatus::RequiresAction,
156        "succeeded" => TransactionStatus::Succeeded,
157        "failed" => TransactionStatus::Failed,
158        "cancelled" => TransactionStatus::Cancelled,
159        "refunded" => TransactionStatus::Refunded,
160        _ => {
161            return HttpResponse::BadRequest().json(serde_json::json!({
162                "error": "Invalid status. Must be one of: pending, processing, requires_action, succeeded, failed, cancelled, refunded"
163            }))
164        }
165    };
166
167    match state
168        .payment_use_cases
169        .list_payments_by_status(organization_id, status)
170        .await
171    {
172        Ok(payments) => HttpResponse::Ok().json(payments),
173        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
174    }
175}
176
177#[get("/payments/pending")]
178pub async fn list_pending_payments(
179    state: web::Data<AppState>,
180    user: AuthenticatedUser,
181) -> impl Responder {
182    let organization_id = match user.require_organization() {
183        Ok(org_id) => org_id,
184        Err(e) => {
185            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
186        }
187    };
188
189    match state
190        .payment_use_cases
191        .list_pending_payments(organization_id)
192        .await
193    {
194        Ok(payments) => HttpResponse::Ok().json(payments),
195        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
196    }
197}
198
199#[get("/payments/failed")]
200pub async fn list_failed_payments(
201    state: web::Data<AppState>,
202    user: AuthenticatedUser,
203) -> impl Responder {
204    let organization_id = match user.require_organization() {
205        Ok(org_id) => org_id,
206        Err(e) => {
207            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
208        }
209    };
210
211    match state
212        .payment_use_cases
213        .list_failed_payments(organization_id)
214        .await
215    {
216        Ok(payments) => HttpResponse::Ok().json(payments),
217        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
218    }
219}
220
221// ==================== Payment Status Update Endpoints ====================
222
223#[put("/payments/{id}/processing")]
224pub async fn mark_payment_processing(
225    state: web::Data<AppState>,
226    user: AuthenticatedUser,
227    id: web::Path<Uuid>,
228) -> impl Responder {
229    let organization_id = match user.require_organization() {
230        Ok(org_id) => org_id,
231        Err(e) => {
232            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
233        }
234    };
235
236    match state.payment_use_cases.mark_processing(*id).await {
237        Ok(payment) => {
238            AuditLogEntry::new(
239                AuditEventType::PaymentProcessing,
240                Some(user.user_id),
241                Some(organization_id),
242            )
243            .with_resource("Payment", payment.id)
244            .log();
245
246            HttpResponse::Ok().json(payment)
247        }
248        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
249    }
250}
251
252#[put("/payments/{id}/requires-action")]
253pub async fn mark_payment_requires_action(
254    state: web::Data<AppState>,
255    user: AuthenticatedUser,
256    id: web::Path<Uuid>,
257) -> impl Responder {
258    let organization_id = match user.require_organization() {
259        Ok(org_id) => org_id,
260        Err(e) => {
261            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
262        }
263    };
264
265    match state.payment_use_cases.mark_requires_action(*id).await {
266        Ok(payment) => {
267            AuditLogEntry::new(
268                AuditEventType::PaymentRequiresAction,
269                Some(user.user_id),
270                Some(organization_id),
271            )
272            .with_resource("Payment", payment.id)
273            .log();
274
275            HttpResponse::Ok().json(payment)
276        }
277        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
278    }
279}
280
281#[put("/payments/{id}/succeeded")]
282pub async fn mark_payment_succeeded(
283    state: web::Data<AppState>,
284    user: AuthenticatedUser,
285    id: web::Path<Uuid>,
286) -> impl Responder {
287    let organization_id = match user.require_organization() {
288        Ok(org_id) => org_id,
289        Err(e) => {
290            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
291        }
292    };
293
294    match state.payment_use_cases.mark_succeeded(*id).await {
295        Ok(payment) => {
296            AuditLogEntry::new(
297                AuditEventType::PaymentSucceeded,
298                Some(user.user_id),
299                Some(organization_id),
300            )
301            .with_resource("Payment", payment.id)
302            .log();
303
304            HttpResponse::Ok().json(payment)
305        }
306        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
307    }
308}
309
310#[put("/payments/{id}/failed")]
311pub async fn mark_payment_failed(
312    state: web::Data<AppState>,
313    user: AuthenticatedUser,
314    id: web::Path<Uuid>,
315    request: web::Json<serde_json::Value>,
316) -> impl Responder {
317    let organization_id = match user.require_organization() {
318        Ok(org_id) => org_id,
319        Err(e) => {
320            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
321        }
322    };
323
324    let reason = request
325        .get("reason")
326        .and_then(|v| v.as_str())
327        .unwrap_or("Unknown failure reason")
328        .to_string();
329
330    match state.payment_use_cases.mark_failed(*id, reason).await {
331        Ok(payment) => {
332            AuditLogEntry::new(
333                AuditEventType::PaymentFailed,
334                Some(user.user_id),
335                Some(organization_id),
336            )
337            .with_resource("Payment", payment.id)
338            .log();
339
340            HttpResponse::Ok().json(payment)
341        }
342        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
343    }
344}
345
346#[put("/payments/{id}/cancelled")]
347pub async fn mark_payment_cancelled(
348    state: web::Data<AppState>,
349    user: AuthenticatedUser,
350    id: web::Path<Uuid>,
351) -> impl Responder {
352    let organization_id = match user.require_organization() {
353        Ok(org_id) => org_id,
354        Err(e) => {
355            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
356        }
357    };
358
359    match state.payment_use_cases.mark_cancelled(*id).await {
360        Ok(payment) => {
361            AuditLogEntry::new(
362                AuditEventType::PaymentCancelled,
363                Some(user.user_id),
364                Some(organization_id),
365            )
366            .with_resource("Payment", payment.id)
367            .log();
368
369            HttpResponse::Ok().json(payment)
370        }
371        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
372    }
373}
374
375#[post("/payments/{id}/refund")]
376pub async fn refund_payment(
377    state: web::Data<AppState>,
378    user: AuthenticatedUser,
379    id: web::Path<Uuid>,
380    request: web::Json<RefundPaymentRequest>,
381) -> impl Responder {
382    let organization_id = match user.require_organization() {
383        Ok(org_id) => org_id,
384        Err(e) => {
385            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
386        }
387    };
388
389    match state
390        .payment_use_cases
391        .refund_payment(*id, request.into_inner())
392        .await
393    {
394        Ok(payment) => {
395            AuditLogEntry::new(
396                AuditEventType::PaymentRefunded,
397                Some(user.user_id),
398                Some(organization_id),
399            )
400            .with_resource("Payment", payment.id)
401            .log();
402
403            HttpResponse::Ok().json(payment)
404        }
405        Err(err) => {
406            AuditLogEntry::new(
407                AuditEventType::PaymentRefunded,
408                Some(user.user_id),
409                Some(organization_id),
410            )
411            .with_error(err.clone())
412            .log();
413
414            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
415        }
416    }
417}
418
419#[delete("/payments/{id}")]
420pub async fn delete_payment(
421    state: web::Data<AppState>,
422    user: AuthenticatedUser,
423    id: web::Path<Uuid>,
424) -> impl Responder {
425    let organization_id = match user.require_organization() {
426        Ok(org_id) => org_id,
427        Err(e) => {
428            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
429        }
430    };
431
432    match state.payment_use_cases.delete_payment(*id).await {
433        Ok(true) => {
434            AuditLogEntry::new(
435                AuditEventType::PaymentDeleted,
436                Some(user.user_id),
437                Some(organization_id),
438            )
439            .with_resource("Payment", *id)
440            .log();
441
442            HttpResponse::NoContent().finish()
443        }
444        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
445            "error": "Payment not found"
446        })),
447        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
448    }
449}
450
451// ==================== Payment Statistics Endpoints ====================
452
453#[get("/owners/{owner_id}/payments/stats")]
454pub async fn get_owner_payment_stats(
455    state: web::Data<AppState>,
456    owner_id: web::Path<Uuid>,
457) -> impl Responder {
458    match state
459        .payment_use_cases
460        .get_owner_payment_stats(*owner_id)
461        .await
462    {
463        Ok(stats) => HttpResponse::Ok().json(stats),
464        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
465    }
466}
467
468#[get("/buildings/{building_id}/payments/stats")]
469pub async fn get_building_payment_stats(
470    state: web::Data<AppState>,
471    building_id: web::Path<Uuid>,
472) -> impl Responder {
473    match state
474        .payment_use_cases
475        .get_building_payment_stats(*building_id)
476        .await
477    {
478        Ok(stats) => HttpResponse::Ok().json(stats),
479        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
480    }
481}
482
483#[get("/expenses/{expense_id}/payments/total")]
484pub async fn get_expense_total_paid(
485    state: web::Data<AppState>,
486    expense_id: web::Path<Uuid>,
487) -> impl Responder {
488    match state
489        .payment_use_cases
490        .get_total_paid_for_expense(*expense_id)
491        .await
492    {
493        Ok(total) => HttpResponse::Ok().json(serde_json::json!({
494            "expense_id": *expense_id,
495            "total_paid_cents": total
496        })),
497        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
498    }
499}
500
501#[get("/owners/{owner_id}/payments/total")]
502pub async fn get_owner_total_paid(
503    state: web::Data<AppState>,
504    owner_id: web::Path<Uuid>,
505) -> impl Responder {
506    match state
507        .payment_use_cases
508        .get_total_paid_by_owner(*owner_id)
509        .await
510    {
511        Ok(total) => HttpResponse::Ok().json(serde_json::json!({
512            "owner_id": *owner_id,
513            "total_paid_cents": total
514        })),
515        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
516    }
517}
518
519#[get("/buildings/{building_id}/payments/total")]
520pub async fn get_building_total_paid(
521    state: web::Data<AppState>,
522    building_id: web::Path<Uuid>,
523) -> impl Responder {
524    match state
525        .payment_use_cases
526        .get_total_paid_for_building(*building_id)
527        .await
528    {
529        Ok(total) => HttpResponse::Ok().json(serde_json::json!({
530            "building_id": *building_id,
531            "total_paid_cents": total
532        })),
533        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
534    }
535}