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