koprogo_api/infrastructure/database/repositories/
meeting_repository_impl.rs

1use crate::application::ports::MeetingRepository;
2use crate::domain::entities::{Meeting, MeetingStatus, MeetingType};
3use crate::infrastructure::database::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use uuid::Uuid;
7
8pub struct PostgresMeetingRepository {
9    pool: DbPool,
10}
11
12impl PostgresMeetingRepository {
13    pub fn new(pool: DbPool) -> Self {
14        Self { pool }
15    }
16}
17
18/// Helper: map a DB row to a Meeting entity (includes quorum fields)
19fn row_to_meeting(row: &sqlx::postgres::PgRow) -> Meeting {
20    let meeting_type_str: String = row.get("meeting_type");
21    let meeting_type = match meeting_type_str.as_str() {
22        "extraordinary" => MeetingType::Extraordinary,
23        _ => MeetingType::Ordinary,
24    };
25
26    let status_str: String = row.get("status");
27    let status = match status_str.as_str() {
28        "completed" => MeetingStatus::Completed,
29        "cancelled" => MeetingStatus::Cancelled,
30        _ => MeetingStatus::Scheduled,
31    };
32
33    let agenda_json: serde_json::Value = row.get("agenda");
34    let agenda: Vec<String> = serde_json::from_value(agenda_json).unwrap_or_default();
35
36    Meeting {
37        id: row.get("id"),
38        organization_id: row.get("organization_id"),
39        building_id: row.get("building_id"),
40        meeting_type,
41        title: row.get("title"),
42        description: row.get("description"),
43        scheduled_date: row.get("scheduled_date"),
44        location: row.get("location"),
45        status,
46        agenda,
47        attendees_count: row.get("attendees_count"),
48        quorum_validated: row.try_get("quorum_validated").unwrap_or(false),
49        quorum_percentage: row.try_get("quorum_percentage").unwrap_or(None),
50        total_quotas: row.try_get("total_quotas").unwrap_or(None),
51        present_quotas: row.try_get("present_quotas").unwrap_or(None),
52        created_at: row.get("created_at"),
53        updated_at: row.get("updated_at"),
54        is_second_convocation: row.try_get("is_second_convocation").unwrap_or(false),
55        minutes_document_id: row.try_get("minutes_document_id").unwrap_or(None),
56        minutes_sent_at: row.try_get("minutes_sent_at").unwrap_or(None),
57    }
58}
59
60const SELECT_COLUMNS: &str = "id, organization_id, building_id, \
61    meeting_type::text AS meeting_type, title, description, scheduled_date, \
62    location, status::text AS status, agenda, attendees_count, \
63    quorum_validated, quorum_percentage, total_quotas, present_quotas, \
64    created_at, updated_at";
65
66#[async_trait]
67impl MeetingRepository for PostgresMeetingRepository {
68    async fn create(&self, meeting: &Meeting) -> Result<Meeting, String> {
69        let meeting_type_str = match meeting.meeting_type {
70            MeetingType::Ordinary => "ordinary",
71            MeetingType::Extraordinary => "extraordinary",
72        };
73
74        let status_str = match meeting.status {
75            MeetingStatus::Scheduled => "scheduled",
76            MeetingStatus::Completed => "completed",
77            MeetingStatus::Cancelled => "cancelled",
78        };
79
80        let agenda_json = serde_json::to_value(&meeting.agenda)
81            .map_err(|e| format!("JSON serialization error: {}", e))?;
82
83        sqlx::query(
84            r#"
85            INSERT INTO meetings (
86                id, organization_id, building_id, meeting_type, title, description,
87                scheduled_date, location, status, agenda, attendees_count,
88                quorum_validated, quorum_percentage, total_quotas, present_quotas,
89                created_at, updated_at
90            )
91            VALUES ($1, $2, $3, CAST($4 AS meeting_type), $5, $6, $7, $8, CAST($9 AS meeting_status),
92                    $10, $11, $12, $13, $14, $15, $16, $17)
93            "#,
94        )
95        .bind(meeting.id)
96        .bind(meeting.organization_id)
97        .bind(meeting.building_id)
98        .bind(meeting_type_str)
99        .bind(&meeting.title)
100        .bind(&meeting.description)
101        .bind(meeting.scheduled_date)
102        .bind(&meeting.location)
103        .bind(status_str)
104        .bind(agenda_json)
105        .bind(meeting.attendees_count)
106        .bind(meeting.quorum_validated)
107        .bind(meeting.quorum_percentage)
108        .bind(meeting.total_quotas)
109        .bind(meeting.present_quotas)
110        .bind(meeting.created_at)
111        .bind(meeting.updated_at)
112        .execute(&self.pool)
113        .await
114        .map_err(|e| format!("Database error: {}", e))?;
115
116        Ok(meeting.clone())
117    }
118
119    async fn find_by_id(&self, id: Uuid) -> Result<Option<Meeting>, String> {
120        let query = format!("SELECT {} FROM meetings WHERE id = $1", SELECT_COLUMNS);
121        let row = sqlx::query(&query)
122            .bind(id)
123            .fetch_optional(&self.pool)
124            .await
125            .map_err(|e| format!("Database error: {}", e))?;
126
127        Ok(row.as_ref().map(row_to_meeting))
128    }
129
130    async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Meeting>, String> {
131        let query = format!(
132            "SELECT {} FROM meetings WHERE building_id = $1 ORDER BY scheduled_date DESC",
133            SELECT_COLUMNS
134        );
135        let rows = sqlx::query(&query)
136            .bind(building_id)
137            .fetch_all(&self.pool)
138            .await
139            .map_err(|e| format!("Database error: {}", e))?;
140
141        Ok(rows.iter().map(row_to_meeting).collect())
142    }
143
144    async fn update(&self, meeting: &Meeting) -> Result<Meeting, String> {
145        let status_str = match meeting.status {
146            MeetingStatus::Scheduled => "scheduled",
147            MeetingStatus::Completed => "completed",
148            MeetingStatus::Cancelled => "cancelled",
149        };
150
151        let agenda_json = serde_json::to_value(&meeting.agenda)
152            .map_err(|e| format!("JSON serialization error: {}", e))?;
153
154        sqlx::query(
155            r#"
156            UPDATE meetings
157            SET title = $2,
158                description = $3,
159                scheduled_date = $4,
160                location = $5,
161                status = CAST($6 AS meeting_status),
162                agenda = $7,
163                attendees_count = $8,
164                quorum_validated = $9,
165                quorum_percentage = $10,
166                total_quotas = $11,
167                present_quotas = $12,
168                updated_at = $13
169            WHERE id = $1
170            "#,
171        )
172        .bind(meeting.id)
173        .bind(&meeting.title)
174        .bind(&meeting.description)
175        .bind(meeting.scheduled_date)
176        .bind(&meeting.location)
177        .bind(status_str)
178        .bind(agenda_json)
179        .bind(meeting.attendees_count)
180        .bind(meeting.quorum_validated)
181        .bind(meeting.quorum_percentage)
182        .bind(meeting.total_quotas)
183        .bind(meeting.present_quotas)
184        .bind(meeting.updated_at)
185        .execute(&self.pool)
186        .await
187        .map_err(|e| format!("Database error: {}", e))?;
188
189        Ok(meeting.clone())
190    }
191
192    async fn delete(&self, id: Uuid) -> Result<bool, String> {
193        let result = sqlx::query("DELETE FROM meetings WHERE id = $1")
194            .bind(id)
195            .execute(&self.pool)
196            .await
197            .map_err(|e| format!("Database error: {}", e))?;
198
199        Ok(result.rows_affected() > 0)
200    }
201
202    async fn find_all_paginated(
203        &self,
204        page_request: &crate::application::dto::PageRequest,
205        organization_id: Option<Uuid>,
206    ) -> Result<(Vec<Meeting>, i64), String> {
207        page_request.validate()?;
208
209        let where_clause = if let Some(org_id) = organization_id {
210            format!("WHERE organization_id = '{}'", org_id)
211        } else {
212            String::new()
213        };
214
215        let count_query = format!("SELECT COUNT(*) FROM meetings {}", where_clause);
216        let total_items = sqlx::query_scalar::<_, i64>(&count_query)
217            .fetch_one(&self.pool)
218            .await
219            .map_err(|e| format!("Database error: {}", e))?;
220
221        let data_query = format!(
222            "SELECT {} FROM meetings {} ORDER BY scheduled_date DESC LIMIT {} OFFSET {}",
223            SELECT_COLUMNS,
224            where_clause,
225            page_request.limit(),
226            page_request.offset()
227        );
228
229        let rows = sqlx::query(&data_query)
230            .fetch_all(&self.pool)
231            .await
232            .map_err(|e| format!("Database error: {}", e))?;
233
234        Ok((rows.iter().map(row_to_meeting).collect(), total_items))
235    }
236}