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, 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        // Validate page request
233        page_request.validate()?;
234
235        // Build WHERE clause
236        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        // Count total items
243        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        // Fetch paginated data
250        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}