koprogo_api/infrastructure/web/handlers/
expense_handlers.rs1use 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, mut dto: web::Json<CreateExpenseDto>,
13) -> impl Responder {
14 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 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 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 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 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}