koprogo_api/infrastructure/web/handlers/
expense_handlers.rs

1use crate::application::dto::{CreateExpenseDto, PageRequest, PageResponse};
2use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
3use crate::infrastructure::web::{AppState, AuthenticatedUser};
4use actix_web::{get, post, put, web, HttpResponse, Responder};
5use uuid::Uuid;
6use validator::Validate;
7
8#[post("/expenses")]
9pub async fn create_expense(
10    state: web::Data<AppState>,
11    user: AuthenticatedUser, // JWT-extracted user info (SECURE!)
12    mut dto: web::Json<CreateExpenseDto>,
13) -> impl Responder {
14    // Override the organization_id from DTO with the one from JWT token
15    // This prevents users from creating expenses in other organizations
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!({
20                "error": e.to_string()
21            }))
22        }
23    };
24    dto.organization_id = organization_id.to_string();
25
26    if let Err(errors) = dto.validate() {
27        return HttpResponse::BadRequest().json(serde_json::json!({
28            "error": "Validation failed",
29            "details": errors.to_string()
30        }));
31    }
32
33    match state
34        .expense_use_cases
35        .create_expense(dto.into_inner())
36        .await
37    {
38        Ok(expense) => {
39            // Audit log: successful expense creation
40            AuditLogEntry::new(
41                AuditEventType::ExpenseCreated,
42                Some(user.user_id),
43                Some(organization_id),
44            )
45            .with_resource("Expense", Uuid::parse_str(&expense.id).unwrap())
46            .log();
47
48            HttpResponse::Created().json(expense)
49        }
50        Err(err) => {
51            // Audit log: failed expense creation
52            AuditLogEntry::new(
53                AuditEventType::ExpenseCreated,
54                Some(user.user_id),
55                Some(organization_id),
56            )
57            .with_error(err.clone())
58            .log();
59
60            HttpResponse::BadRequest().json(serde_json::json!({
61                "error": err
62            }))
63        }
64    }
65}
66
67#[get("/expenses/{id}")]
68pub async fn get_expense(state: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
69    match state.expense_use_cases.get_expense(*id).await {
70        Ok(Some(expense)) => HttpResponse::Ok().json(expense),
71        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
72            "error": "Expense not found"
73        })),
74        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
75            "error": err
76        })),
77    }
78}
79
80#[get("/expenses")]
81pub async fn list_expenses(
82    state: web::Data<AppState>,
83    user: AuthenticatedUser,
84    page_request: web::Query<PageRequest>,
85) -> impl Responder {
86    let organization_id = user.organization_id;
87
88    match state
89        .expense_use_cases
90        .list_expenses_paginated(&page_request, organization_id)
91        .await
92    {
93        Ok((expenses, total)) => {
94            let response =
95                PageResponse::new(expenses, page_request.page, page_request.per_page, total);
96            HttpResponse::Ok().json(response)
97        }
98        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
99            "error": err
100        })),
101    }
102}
103
104#[get("/buildings/{building_id}/expenses")]
105pub async fn list_expenses_by_building(
106    state: web::Data<AppState>,
107    building_id: web::Path<Uuid>,
108) -> impl Responder {
109    match state
110        .expense_use_cases
111        .list_expenses_by_building(*building_id)
112        .await
113    {
114        Ok(expenses) => HttpResponse::Ok().json(expenses),
115        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
116            "error": err
117        })),
118    }
119}
120
121#[put("/expenses/{id}/mark-paid")]
122pub async fn mark_expense_paid(
123    state: web::Data<AppState>,
124    user: AuthenticatedUser,
125    id: web::Path<Uuid>,
126) -> impl Responder {
127    match state.expense_use_cases.mark_as_paid(*id).await {
128        Ok(expense) => {
129            // Audit log: successful expense marked paid
130            AuditLogEntry::new(
131                AuditEventType::ExpenseMarkedPaid,
132                Some(user.user_id),
133                user.organization_id,
134            )
135            .with_resource("Expense", *id)
136            .log();
137
138            HttpResponse::Ok().json(expense)
139        }
140        Err(err) => {
141            // Audit log: failed expense marked paid
142            AuditLogEntry::new(
143                AuditEventType::ExpenseMarkedPaid,
144                Some(user.user_id),
145                user.organization_id,
146            )
147            .with_resource("Expense", *id)
148            .with_error(err.clone())
149            .log();
150
151            HttpResponse::BadRequest().json(serde_json::json!({
152                "error": err
153            }))
154        }
155    }
156}