1use crate::application::ports::DocumentRepository;
2use crate::domain::entities::{Document, DocumentType};
3use crate::infrastructure::database::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use uuid::Uuid;
7
8pub struct PostgresDocumentRepository {
9 pool: DbPool,
10}
11
12impl PostgresDocumentRepository {
13 pub fn new(pool: DbPool) -> Self {
14 Self { pool }
15 }
16}
17
18#[async_trait]
19impl DocumentRepository for PostgresDocumentRepository {
20 async fn create(&self, document: &Document) -> Result<Document, String> {
21 let document_type_str = match document.document_type {
22 DocumentType::MeetingMinutes => "meeting_minutes",
23 DocumentType::FinancialStatement => "financial_statement",
24 DocumentType::Invoice => "invoice",
25 DocumentType::Contract => "contract",
26 DocumentType::Regulation => "regulation",
27 DocumentType::WorksQuote => "works_quote",
28 DocumentType::Other => "other",
29 };
30
31 sqlx::query(
32 r#"
33 INSERT INTO documents (id, organization_id, building_id, document_type, title, description, file_path, file_size, mime_type, uploaded_by, related_meeting_id, related_expense_id, created_at, updated_at)
34 VALUES ($1, $2, $3, CAST($4 AS document_type), $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
35 "#,
36 )
37 .bind(document.id)
38 .bind(document.organization_id)
39 .bind(document.building_id)
40 .bind(document_type_str)
41 .bind(&document.title)
42 .bind(&document.description)
43 .bind(&document.file_path)
44 .bind(document.file_size)
45 .bind(&document.mime_type)
46 .bind(document.uploaded_by)
47 .bind(document.related_meeting_id)
48 .bind(document.related_expense_id)
49 .bind(document.created_at)
50 .bind(document.updated_at)
51 .execute(&self.pool)
52 .await
53 .map_err(|e| format!("Database error: {}", e))?;
54
55 Ok(document.clone())
56 }
57
58 async fn find_by_id(&self, id: Uuid) -> Result<Option<Document>, String> {
59 let row = sqlx::query(
60 r#"
61 SELECT id, organization_id, building_id, document_type::text AS document_type, title, description, file_path, file_size, mime_type, uploaded_by, related_meeting_id, related_expense_id, created_at, updated_at
62 FROM documents
63 WHERE id = $1
64 "#,
65 )
66 .bind(id)
67 .fetch_optional(&self.pool)
68 .await
69 .map_err(|e| format!("Database error: {}", e))?;
70
71 Ok(row.map(|row| {
72 let document_type_str: String = row.get("document_type");
73 let document_type = match document_type_str.as_str() {
74 "meeting_minutes" => DocumentType::MeetingMinutes,
75 "financial_statement" => DocumentType::FinancialStatement,
76 "invoice" => DocumentType::Invoice,
77 "contract" => DocumentType::Contract,
78 "regulation" => DocumentType::Regulation,
79 "works_quote" => DocumentType::WorksQuote,
80 _ => DocumentType::Other,
81 };
82
83 Document {
84 id: row.get("id"),
85 organization_id: row.get("organization_id"),
86 building_id: row.get("building_id"),
87 document_type,
88 title: row.get("title"),
89 description: row.get("description"),
90 file_path: row.get("file_path"),
91 file_size: row.get("file_size"),
92 mime_type: row.get("mime_type"),
93 uploaded_by: row.get("uploaded_by"),
94 related_meeting_id: row.get("related_meeting_id"),
95 related_expense_id: row.get("related_expense_id"),
96 created_at: row.get("created_at"),
97 updated_at: row.get("updated_at"),
98 }
99 }))
100 }
101
102 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Document>, String> {
103 let rows = sqlx::query(
104 r#"
105 SELECT id, organization_id, building_id, document_type::text AS document_type, title, description, file_path, file_size, mime_type, uploaded_by, related_meeting_id, related_expense_id, created_at, updated_at
106 FROM documents
107 WHERE building_id = $1
108 ORDER BY created_at DESC
109 "#,
110 )
111 .bind(building_id)
112 .fetch_all(&self.pool)
113 .await
114 .map_err(|e| format!("Database error: {}", e))?;
115
116 Ok(rows
117 .iter()
118 .map(|row| {
119 let document_type_str: String = row.get("document_type");
120 let document_type = match document_type_str.as_str() {
121 "meeting_minutes" => DocumentType::MeetingMinutes,
122 "financial_statement" => DocumentType::FinancialStatement,
123 "invoice" => DocumentType::Invoice,
124 "contract" => DocumentType::Contract,
125 "regulation" => DocumentType::Regulation,
126 "works_quote" => DocumentType::WorksQuote,
127 _ => DocumentType::Other,
128 };
129
130 Document {
131 id: row.get("id"),
132 organization_id: row.get("organization_id"),
133 building_id: row.get("building_id"),
134 document_type,
135 title: row.get("title"),
136 description: row.get("description"),
137 file_path: row.get("file_path"),
138 file_size: row.get("file_size"),
139 mime_type: row.get("mime_type"),
140 uploaded_by: row.get("uploaded_by"),
141 related_meeting_id: row.get("related_meeting_id"),
142 related_expense_id: row.get("related_expense_id"),
143 created_at: row.get("created_at"),
144 updated_at: row.get("updated_at"),
145 }
146 })
147 .collect())
148 }
149
150 async fn find_by_meeting(&self, meeting_id: Uuid) -> Result<Vec<Document>, String> {
151 let rows = sqlx::query(
152 r#"
153 SELECT id, organization_id, building_id, document_type::text AS document_type, title, description, file_path, file_size, mime_type, uploaded_by, related_meeting_id, related_expense_id, created_at, updated_at
154 FROM documents
155 WHERE related_meeting_id = $1
156 ORDER BY created_at DESC
157 "#,
158 )
159 .bind(meeting_id)
160 .fetch_all(&self.pool)
161 .await
162 .map_err(|e| format!("Database error: {}", e))?;
163
164 Ok(rows
165 .iter()
166 .map(|row| {
167 let document_type_str: String = row.get("document_type");
168 let document_type = match document_type_str.as_str() {
169 "meeting_minutes" => DocumentType::MeetingMinutes,
170 "financial_statement" => DocumentType::FinancialStatement,
171 "invoice" => DocumentType::Invoice,
172 "contract" => DocumentType::Contract,
173 "regulation" => DocumentType::Regulation,
174 "works_quote" => DocumentType::WorksQuote,
175 _ => DocumentType::Other,
176 };
177
178 Document {
179 id: row.get("id"),
180 organization_id: row.get("organization_id"),
181 building_id: row.get("building_id"),
182 document_type,
183 title: row.get("title"),
184 description: row.get("description"),
185 file_path: row.get("file_path"),
186 file_size: row.get("file_size"),
187 mime_type: row.get("mime_type"),
188 uploaded_by: row.get("uploaded_by"),
189 related_meeting_id: row.get("related_meeting_id"),
190 related_expense_id: row.get("related_expense_id"),
191 created_at: row.get("created_at"),
192 updated_at: row.get("updated_at"),
193 }
194 })
195 .collect())
196 }
197
198 async fn find_by_expense(&self, expense_id: Uuid) -> Result<Vec<Document>, String> {
199 let rows = sqlx::query(
200 r#"
201 SELECT id, organization_id, building_id, document_type::text AS document_type, title, description, file_path, file_size, mime_type, uploaded_by, related_meeting_id, related_expense_id, created_at, updated_at
202 FROM documents
203 WHERE related_expense_id = $1
204 ORDER BY created_at DESC
205 "#,
206 )
207 .bind(expense_id)
208 .fetch_all(&self.pool)
209 .await
210 .map_err(|e| format!("Database error: {}", e))?;
211
212 Ok(rows
213 .iter()
214 .map(|row| {
215 let document_type_str: String = row.get("document_type");
216 let document_type = match document_type_str.as_str() {
217 "meeting_minutes" => DocumentType::MeetingMinutes,
218 "financial_statement" => DocumentType::FinancialStatement,
219 "invoice" => DocumentType::Invoice,
220 "contract" => DocumentType::Contract,
221 "regulation" => DocumentType::Regulation,
222 "works_quote" => DocumentType::WorksQuote,
223 _ => DocumentType::Other,
224 };
225
226 Document {
227 id: row.get("id"),
228 organization_id: row.get("organization_id"),
229 building_id: row.get("building_id"),
230 document_type,
231 title: row.get("title"),
232 description: row.get("description"),
233 file_path: row.get("file_path"),
234 file_size: row.get("file_size"),
235 mime_type: row.get("mime_type"),
236 uploaded_by: row.get("uploaded_by"),
237 related_meeting_id: row.get("related_meeting_id"),
238 related_expense_id: row.get("related_expense_id"),
239 created_at: row.get("created_at"),
240 updated_at: row.get("updated_at"),
241 }
242 })
243 .collect())
244 }
245
246 async fn update(&self, document: &Document) -> Result<Document, String> {
247 sqlx::query(
248 r#"
249 UPDATE documents
250 SET related_meeting_id = $2, related_expense_id = $3, updated_at = $4
251 WHERE id = $1
252 "#,
253 )
254 .bind(document.id)
255 .bind(document.related_meeting_id)
256 .bind(document.related_expense_id)
257 .bind(document.updated_at)
258 .execute(&self.pool)
259 .await
260 .map_err(|e| format!("Database error: {}", e))?;
261
262 Ok(document.clone())
263 }
264
265 async fn delete(&self, id: Uuid) -> Result<bool, String> {
266 let result = sqlx::query("DELETE FROM documents WHERE id = $1")
267 .bind(id)
268 .execute(&self.pool)
269 .await
270 .map_err(|e| format!("Database error: {}", e))?;
271
272 Ok(result.rows_affected() > 0)
273 }
274
275 async fn find_all_paginated(
276 &self,
277 page_request: &crate::application::dto::PageRequest,
278 organization_id: Option<Uuid>,
279 ) -> Result<(Vec<Document>, i64), String> {
280 page_request.validate()?;
282
283 let where_clause = if let Some(org_id) = organization_id {
285 format!("WHERE organization_id = '{}'", org_id)
286 } else {
287 String::new()
288 };
289
290 let count_query = format!("SELECT COUNT(*) FROM documents {}", where_clause);
292 let total_items = sqlx::query_scalar::<_, i64>(&count_query)
293 .fetch_one(&self.pool)
294 .await
295 .map_err(|e| format!("Database error: {}", e))?;
296
297 let data_query = format!(
299 "SELECT id, organization_id, building_id, document_type, title, description, file_path, file_size, mime_type, uploaded_by, related_meeting_id, related_expense_id, created_at, updated_at \
300 FROM documents {} ORDER BY created_at DESC LIMIT {} OFFSET {}",
301 where_clause,
302 page_request.limit(),
303 page_request.offset()
304 );
305
306 let rows = sqlx::query(&data_query)
307 .fetch_all(&self.pool)
308 .await
309 .map_err(|e| format!("Database error: {}", e))?;
310
311 let documents: Vec<Document> = rows
312 .iter()
313 .map(|row| {
314 let document_type_str: String = row
315 .try_get("document_type")
316 .unwrap_or_else(|_| "other".to_string());
317 let document_type = match document_type_str.as_str() {
318 "meeting_minutes" => DocumentType::MeetingMinutes,
319 "financial_statement" => DocumentType::FinancialStatement,
320 "invoice" => DocumentType::Invoice,
321 "contract" => DocumentType::Contract,
322 "regulation" => DocumentType::Regulation,
323 "works_quote" => DocumentType::WorksQuote,
324 _ => DocumentType::Other,
325 };
326
327 Document {
328 id: row.get("id"),
329 organization_id: row.get("organization_id"),
330 building_id: row.get("building_id"),
331 document_type,
332 title: row.get("title"),
333 description: row.get("description"),
334 file_path: row.get("file_path"),
335 file_size: row.get("file_size"),
336 mime_type: row.get("mime_type"),
337 uploaded_by: row.get("uploaded_by"),
338 related_meeting_id: row.get("related_meeting_id"),
339 related_expense_id: row.get("related_expense_id"),
340 created_at: row.get("created_at"),
341 updated_at: row.get("updated_at"),
342 }
343 })
344 .collect();
345
346 Ok((documents, total_items))
347 }
348}