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 let file_content = match std::fs::read(form.file.file.path()) {
78 Ok(content) => content,
79 Err(e) => {
80 return HttpResponse::InternalServerError().json(format!("Failed to read file: {}", e))
81 }
82 };
83
84 match app_state
86 .document_use_cases
87 .upload_document(
88 organization_id,
89 building_id,
90 document_type,
91 form.title.0.clone(),
92 form.description.map(|d| d.0),
93 filename,
94 file_content,
95 mime_type,
96 uploaded_by,
97 )
98 .await
99 {
100 Ok(document) => {
101 AuditLogEntry::new(
103 AuditEventType::DocumentUploaded,
104 Some(user.user_id),
105 Some(organization_id),
106 )
107 .with_resource("Document", document.id)
108 .log();
109
110 HttpResponse::Created().json(document)
111 }
112 Err(e) => {
113 AuditLogEntry::new(
115 AuditEventType::DocumentUploaded,
116 Some(user.user_id),
117 Some(organization_id),
118 )
119 .with_error(e.clone())
120 .log();
121
122 HttpResponse::InternalServerError().json(e)
123 }
124 }
125}
126
127#[get("/documents/{id}")]
129pub async fn get_document(app_state: web::Data<AppState>, path: web::Path<Uuid>) -> impl Responder {
130 let id = path.into_inner();
131
132 match app_state.document_use_cases.get_document(id).await {
133 Ok(document) => HttpResponse::Ok().json(document),
134 Err(e) => HttpResponse::NotFound().json(e),
135 }
136}
137
138#[get("/documents")]
140pub async fn list_documents(
141 app_state: web::Data<AppState>,
142 user: AuthenticatedUser,
143 page_request: web::Query<PageRequest>,
144) -> impl Responder {
145 let organization_id = user.organization_id;
146
147 match app_state
148 .document_use_cases
149 .list_documents_paginated(&page_request, organization_id)
150 .await
151 {
152 Ok((documents, total)) => {
153 let response =
154 PageResponse::new(documents, page_request.page, page_request.per_page, total);
155 HttpResponse::Ok().json(response)
156 }
157 Err(e) => HttpResponse::InternalServerError().json(e),
158 }
159}
160
161#[get("/documents/{id}/download")]
163pub async fn download_document(
164 app_state: web::Data<AppState>,
165 path: web::Path<Uuid>,
166) -> impl Responder {
167 let id = path.into_inner();
168
169 match app_state.document_use_cases.download_document(id).await {
170 Ok((content, mime_type, filename)) => HttpResponse::Ok()
171 .content_type(mime_type)
172 .insert_header((
173 "Content-Disposition",
174 format!("attachment; filename=\"{}\"", filename),
175 ))
176 .body(content),
177 Err(e) => HttpResponse::NotFound().json(e),
178 }
179}
180
181#[get("/buildings/{building_id}/documents")]
183pub async fn list_documents_by_building(
184 app_state: web::Data<AppState>,
185 path: web::Path<Uuid>,
186) -> impl Responder {
187 let building_id = path.into_inner();
188
189 match app_state
190 .document_use_cases
191 .list_documents_by_building(building_id)
192 .await
193 {
194 Ok(documents) => HttpResponse::Ok().json(documents),
195 Err(e) => HttpResponse::InternalServerError().json(e),
196 }
197}
198
199#[get("/meetings/{meeting_id}/documents")]
201pub async fn list_documents_by_meeting(
202 app_state: web::Data<AppState>,
203 path: web::Path<Uuid>,
204) -> impl Responder {
205 let meeting_id = path.into_inner();
206
207 match app_state
208 .document_use_cases
209 .list_documents_by_meeting(meeting_id)
210 .await
211 {
212 Ok(documents) => HttpResponse::Ok().json(documents),
213 Err(e) => HttpResponse::InternalServerError().json(e),
214 }
215}
216
217#[put("/documents/{id}/link-meeting")]
219pub async fn link_document_to_meeting(
220 app_state: web::Data<AppState>,
221 path: web::Path<Uuid>,
222 request: web::Json<LinkDocumentToMeetingRequest>,
223) -> impl Responder {
224 let id = path.into_inner();
225
226 match app_state
227 .document_use_cases
228 .link_to_meeting(id, request.into_inner())
229 .await
230 {
231 Ok(document) => HttpResponse::Ok().json(document),
232 Err(e) => HttpResponse::NotFound().json(e),
233 }
234}
235
236#[put("/documents/{id}/link-expense")]
238pub async fn link_document_to_expense(
239 app_state: web::Data<AppState>,
240 path: web::Path<Uuid>,
241 request: web::Json<LinkDocumentToExpenseRequest>,
242) -> impl Responder {
243 let id = path.into_inner();
244
245 match app_state
246 .document_use_cases
247 .link_to_expense(id, request.into_inner())
248 .await
249 {
250 Ok(document) => HttpResponse::Ok().json(document),
251 Err(e) => HttpResponse::NotFound().json(e),
252 }
253}
254
255#[delete("/documents/{id}")]
257pub async fn delete_document(
258 app_state: web::Data<AppState>,
259 user: AuthenticatedUser,
260 path: web::Path<Uuid>,
261) -> impl Responder {
262 let id = path.into_inner();
263
264 match app_state.document_use_cases.delete_document(id).await {
265 Ok(true) => {
266 AuditLogEntry::new(
268 AuditEventType::DocumentDeleted,
269 Some(user.user_id),
270 user.organization_id,
271 )
272 .with_resource("Document", id)
273 .log();
274
275 HttpResponse::NoContent().finish()
276 }
277 Ok(false) => HttpResponse::NotFound().json("Document not found"),
278 Err(e) => {
279 AuditLogEntry::new(
281 AuditEventType::DocumentDeleted,
282 Some(user.user_id),
283 user.organization_id,
284 )
285 .with_resource("Document", id)
286 .with_error(e.clone())
287 .log();
288
289 HttpResponse::InternalServerError().json(e)
290 }
291 }
292}