koprogo_api/infrastructure/database/repositories/
document_repository_impl.rs

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        // Validate page request
281        page_request.validate()?;
282
283        // Build WHERE clause
284        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        // Count total items
291        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        // Fetch paginated data
298        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}