koprogo_api/infrastructure/web/handlers/
document_handlers.rs1use crate::application::dto::{
2 LinkDocumentToExpenseRequest, LinkDocumentToMeetingRequest, PageRequest, PageResponse,
3};
4use crate::domain::entities::DocumentType;
5use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
6use crate::infrastructure::web::{app_state::AppState, AuthenticatedUser};
7use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
8use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
9use uuid::Uuid;
10
11#[derive(Debug, MultipartForm)]
12pub struct UploadForm {
13 #[multipart(limit = "50MB")]
14 file: TempFile,
15 building_id: Text<String>,
16 document_type: Text<String>,
17 title: Text<String>,
18 description: Option<Text<String>>,
19 uploaded_by: Text<String>,
20}
21
22#[post("/documents")]
24pub async fn upload_document(
25 app_state: web::Data<AppState>,
26 user: AuthenticatedUser, MultipartForm(form): MultipartForm<UploadForm>,
28) -> impl Responder {
29 let organization_id = match user.require_organization() {
31 Ok(org_id) => org_id,
32 Err(e) => {
33 return HttpResponse::Unauthorized().json(serde_json::json!({
34 "error": e.to_string()
35 }))
36 }
37 };
38
39 let building_id = match Uuid::parse_str(&form.building_id.0) {
41 Ok(id) => id,
42 Err(_) => return HttpResponse::BadRequest().json("Invalid building_id"),
43 };
44
45 let document_type = match form.document_type.0.as_str() {
47 "meeting_minutes" | "MeetingMinutes" => DocumentType::MeetingMinutes,
48 "financial_statement" | "FinancialStatement" => DocumentType::FinancialStatement,
49 "invoice" | "Invoice" => DocumentType::Invoice,
50 "contract" | "Contract" => DocumentType::Contract,
51 "regulation" | "Regulation" => DocumentType::Regulation,
52 "works_quote" | "WorksQuote" => DocumentType::WorksQuote,
53 "other" | "Other" => DocumentType::Other,
54 _ => return HttpResponse::BadRequest().json("Invalid document_type"),
55 };
56
57 let uploaded_by = match Uuid::parse_str(&form.uploaded_by.0) {
59 Ok(id) => id,
60 Err(_) => return HttpResponse::BadRequest().json("Invalid uploaded_by"),
61 };
62
63 let filename = form
65 .file
66 .file_name
67 .clone()
68 .unwrap_or_else(|| "unnamed".to_string());
69 let mime_type = form
70 .file
71 .content_type
72 .as_ref()
73 .map(|ct| ct.to_string())
74 .unwrap_or_else(|| "application/octet-stream".to_string());
75
76 const MAX_FILE_SIZE: usize = 50 * 1024 * 1024; if form.file.size > MAX_FILE_SIZE {
79 return HttpResponse::PayloadTooLarge().json(serde_json::json!({
80 "error": "File too large. Maximum allowed size is 50MB."
81 }));
82 }
83
84 let file_content = match std::fs::read(form.file.file.path()) {
86 Ok(content) => content,
87 Err(e) => {
88 return HttpResponse::InternalServerError().json(format!("Failed to read file: {}", e))
89 }
90 };
91
92 match app_state
94 .document_use_cases
95 .upload_document(
96 organization_id,
97 building_id,
98 document_type,
99 form.title.0.clone(),
100 form.description.map(|d| d.0),
101 filename,
102 file_content,
103 mime_type,
104 uploaded_by,
105 )
106 .await
107 {
108 Ok(document) => {
109 AuditLogEntry::new(
111 AuditEventType::DocumentUploaded,
112 Some(user.user_id),
113 Some(organization_id),
114 )
115 .with_resource("Document", document.id)
116 .log();
117
118 HttpResponse::Created().json(document)
119 }
120 Err(e) => {
121 AuditLogEntry::new(
123 AuditEventType::DocumentUploaded,
124 Some(user.user_id),
125 Some(organization_id),
126 )
127 .with_error(e.clone())
128 .log();
129
130 HttpResponse::InternalServerError().json(e)
131 }
132 }
133}
134
135#[get("/documents/{id}")]
137pub async fn get_document(app_state: web::Data<AppState>, path: web::Path<Uuid>) -> impl Responder {
138 let id = path.into_inner();
139
140 match app_state.document_use_cases.get_document(id).await {
141 Ok(document) => HttpResponse::Ok().json(document),
142 Err(e) => HttpResponse::NotFound().json(e),
143 }
144}
145
146#[get("/documents")]
148pub async fn list_documents(
149 app_state: web::Data<AppState>,
150 user: AuthenticatedUser,
151 page_request: web::Query<PageRequest>,
152) -> impl Responder {
153 let organization_id = user.organization_id;
154
155 match app_state
156 .document_use_cases
157 .list_documents_paginated(&page_request, organization_id)
158 .await
159 {
160 Ok((documents, total)) => {
161 let response =
162 PageResponse::new(documents, page_request.page, page_request.per_page, total);
163 HttpResponse::Ok().json(response)
164 }
165 Err(e) => HttpResponse::InternalServerError().json(e),
166 }
167}
168
169#[get("/documents/{id}/download")]
171pub async fn download_document(
172 app_state: web::Data<AppState>,
173 path: web::Path<Uuid>,
174) -> impl Responder {
175 let id = path.into_inner();
176
177 match app_state.document_use_cases.download_document(id).await {
178 Ok((content, mime_type, filename)) => HttpResponse::Ok()
179 .content_type(mime_type)
180 .insert_header((
181 "Content-Disposition",
182 format!("attachment; filename=\"{}\"", filename),
183 ))
184 .body(content),
185 Err(e) => HttpResponse::NotFound().json(e),
186 }
187}
188
189#[get("/buildings/{building_id}/documents")]
191pub async fn list_documents_by_building(
192 app_state: web::Data<AppState>,
193 path: web::Path<Uuid>,
194) -> impl Responder {
195 let building_id = path.into_inner();
196
197 match app_state
198 .document_use_cases
199 .list_documents_by_building(building_id)
200 .await
201 {
202 Ok(documents) => HttpResponse::Ok().json(documents),
203 Err(e) => HttpResponse::InternalServerError().json(e),
204 }
205}
206
207#[get("/meetings/{meeting_id}/documents")]
209pub async fn list_documents_by_meeting(
210 app_state: web::Data<AppState>,
211 path: web::Path<Uuid>,
212) -> impl Responder {
213 let meeting_id = path.into_inner();
214
215 match app_state
216 .document_use_cases
217 .list_documents_by_meeting(meeting_id)
218 .await
219 {
220 Ok(documents) => HttpResponse::Ok().json(documents),
221 Err(e) => HttpResponse::InternalServerError().json(e),
222 }
223}
224
225#[get("/expenses/{expense_id}/documents")]
227pub async fn list_documents_by_expense(
228 app_state: web::Data<AppState>,
229 path: web::Path<Uuid>,
230) -> impl Responder {
231 let expense_id = path.into_inner();
232
233 match app_state
234 .document_use_cases
235 .list_documents_by_expense(expense_id)
236 .await
237 {
238 Ok(documents) => HttpResponse::Ok().json(documents),
239 Err(e) => HttpResponse::InternalServerError().json(e),
240 }
241}
242
243#[put("/documents/{id}/link-meeting")]
245pub async fn link_document_to_meeting(
246 app_state: web::Data<AppState>,
247 path: web::Path<Uuid>,
248 request: web::Json<LinkDocumentToMeetingRequest>,
249) -> impl Responder {
250 let id = path.into_inner();
251
252 match app_state
253 .document_use_cases
254 .link_to_meeting(id, request.into_inner())
255 .await
256 {
257 Ok(document) => HttpResponse::Ok().json(document),
258 Err(e) => HttpResponse::NotFound().json(e),
259 }
260}
261
262#[put("/documents/{id}/link-expense")]
264pub async fn link_document_to_expense(
265 app_state: web::Data<AppState>,
266 path: web::Path<Uuid>,
267 request: web::Json<LinkDocumentToExpenseRequest>,
268) -> impl Responder {
269 let id = path.into_inner();
270
271 match app_state
272 .document_use_cases
273 .link_to_expense(id, request.into_inner())
274 .await
275 {
276 Ok(document) => HttpResponse::Ok().json(document),
277 Err(e) => HttpResponse::NotFound().json(e),
278 }
279}
280
281#[delete("/documents/{id}")]
283pub async fn delete_document(
284 app_state: web::Data<AppState>,
285 user: AuthenticatedUser,
286 path: web::Path<Uuid>,
287) -> impl Responder {
288 let id = path.into_inner();
289
290 match app_state.document_use_cases.delete_document(id).await {
291 Ok(true) => {
292 AuditLogEntry::new(
294 AuditEventType::DocumentDeleted,
295 Some(user.user_id),
296 user.organization_id,
297 )
298 .with_resource("Document", id)
299 .log();
300
301 HttpResponse::NoContent().finish()
302 }
303 Ok(false) => HttpResponse::NotFound().json("Document not found"),
304 Err(e) => {
305 AuditLogEntry::new(
307 AuditEventType::DocumentDeleted,
308 Some(user.user_id),
309 user.organization_id,
310 )
311 .with_resource("Document", id)
312 .with_error(e.clone())
313 .log();
314
315 HttpResponse::InternalServerError().json(e)
316 }
317 }
318}