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, 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, 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 update(&self, document: &Document) -> Result<Document, String> {
199 sqlx::query(
200 r#"
201 UPDATE documents
202 SET related_meeting_id = $2, related_expense_id = $3, updated_at = $4
203 WHERE id = $1
204 "#,
205 )
206 .bind(document.id)
207 .bind(document.related_meeting_id)
208 .bind(document.related_expense_id)
209 .bind(document.updated_at)
210 .execute(&self.pool)
211 .await
212 .map_err(|e| format!("Database error: {}", e))?;
213
214 Ok(document.clone())
215 }
216
217 async fn delete(&self, id: Uuid) -> Result<bool, String> {
218 let result = sqlx::query("DELETE FROM documents WHERE id = $1")
219 .bind(id)
220 .execute(&self.pool)
221 .await
222 .map_err(|e| format!("Database error: {}", e))?;
223
224 Ok(result.rows_affected() > 0)
225 }
226
227 async fn find_all_paginated(
228 &self,
229 page_request: &crate::application::dto::PageRequest,
230 organization_id: Option<Uuid>,
231 ) -> Result<(Vec<Document>, i64), String> {
232 page_request.validate()?;
234
235 let where_clause = if let Some(org_id) = organization_id {
237 format!("WHERE organization_id = '{}'", org_id)
238 } else {
239 String::new()
240 };
241
242 let count_query = format!("SELECT COUNT(*) FROM documents {}", where_clause);
244 let total_items = sqlx::query_scalar::<_, i64>(&count_query)
245 .fetch_one(&self.pool)
246 .await
247 .map_err(|e| format!("Database error: {}", e))?;
248
249 let data_query = format!(
251 "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 \
252 FROM documents {} ORDER BY created_at DESC LIMIT {} OFFSET {}",
253 where_clause,
254 page_request.limit(),
255 page_request.offset()
256 );
257
258 let rows = sqlx::query(&data_query)
259 .fetch_all(&self.pool)
260 .await
261 .map_err(|e| format!("Database error: {}", e))?;
262
263 let documents: Vec<Document> = rows
264 .iter()
265 .map(|row| {
266 let document_type_str: String = row
267 .try_get("document_type")
268 .unwrap_or_else(|_| "other".to_string());
269 let document_type = match document_type_str.as_str() {
270 "meeting_minutes" => DocumentType::MeetingMinutes,
271 "financial_statement" => DocumentType::FinancialStatement,
272 "invoice" => DocumentType::Invoice,
273 "contract" => DocumentType::Contract,
274 "regulation" => DocumentType::Regulation,
275 "works_quote" => DocumentType::WorksQuote,
276 _ => DocumentType::Other,
277 };
278
279 Document {
280 id: row.get("id"),
281 organization_id: row.get("organization_id"),
282 building_id: row.get("building_id"),
283 document_type,
284 title: row.get("title"),
285 description: row.get("description"),
286 file_path: row.get("file_path"),
287 file_size: row.get("file_size"),
288 mime_type: row.get("mime_type"),
289 uploaded_by: row.get("uploaded_by"),
290 related_meeting_id: row.get("related_meeting_id"),
291 related_expense_id: row.get("related_expense_id"),
292 created_at: row.get("created_at"),
293 updated_at: row.get("updated_at"),
294 }
295 })
296 .collect();
297
298 Ok((documents, total_items))
299 }
300}