koprogo_api/infrastructure/database/repositories/
meeting_repository_impl.rs1use 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
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 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 is_second_convocation, 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 is_second_convocation, 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, $18)
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.is_second_convocation)
111 .bind(meeting.created_at)
112 .bind(meeting.updated_at)
113 .execute(&self.pool)
114 .await
115 .map_err(|e| format!("Database error: {}", e))?;
116
117 Ok(meeting.clone())
118 }
119
120 async fn find_by_id(&self, id: Uuid) -> Result<Option<Meeting>, String> {
121 let query = format!("SELECT {} FROM meetings WHERE id = $1", SELECT_COLUMNS);
122 let row = sqlx::query(&query)
123 .bind(id)
124 .fetch_optional(&self.pool)
125 .await
126 .map_err(|e| format!("Database error: {}", e))?;
127
128 Ok(row.as_ref().map(row_to_meeting))
129 }
130
131 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Meeting>, String> {
132 let query = format!(
133 "SELECT {} FROM meetings WHERE building_id = $1 ORDER BY scheduled_date DESC",
134 SELECT_COLUMNS
135 );
136 let rows = sqlx::query(&query)
137 .bind(building_id)
138 .fetch_all(&self.pool)
139 .await
140 .map_err(|e| format!("Database error: {}", e))?;
141
142 Ok(rows.iter().map(row_to_meeting).collect())
143 }
144
145 async fn update(&self, meeting: &Meeting) -> Result<Meeting, String> {
146 let status_str = match meeting.status {
147 MeetingStatus::Scheduled => "scheduled",
148 MeetingStatus::Completed => "completed",
149 MeetingStatus::Cancelled => "cancelled",
150 };
151
152 let agenda_json = serde_json::to_value(&meeting.agenda)
153 .map_err(|e| format!("JSON serialization error: {}", e))?;
154
155 sqlx::query(
156 r#"
157 UPDATE meetings
158 SET title = $2,
159 description = $3,
160 scheduled_date = $4,
161 location = $5,
162 status = CAST($6 AS meeting_status),
163 agenda = $7,
164 attendees_count = $8,
165 quorum_validated = $9,
166 quorum_percentage = $10,
167 total_quotas = $11,
168 present_quotas = $12,
169 is_second_convocation = $13,
170 updated_at = $14
171 WHERE id = $1
172 "#,
173 )
174 .bind(meeting.id)
175 .bind(&meeting.title)
176 .bind(&meeting.description)
177 .bind(meeting.scheduled_date)
178 .bind(&meeting.location)
179 .bind(status_str)
180 .bind(agenda_json)
181 .bind(meeting.attendees_count)
182 .bind(meeting.quorum_validated)
183 .bind(meeting.quorum_percentage)
184 .bind(meeting.total_quotas)
185 .bind(meeting.present_quotas)
186 .bind(meeting.is_second_convocation)
187 .bind(meeting.updated_at)
188 .execute(&self.pool)
189 .await
190 .map_err(|e| format!("Database error: {}", e))?;
191
192 Ok(meeting.clone())
193 }
194
195 async fn delete(&self, id: Uuid) -> Result<bool, String> {
196 let result = sqlx::query("DELETE FROM meetings WHERE id = $1")
197 .bind(id)
198 .execute(&self.pool)
199 .await
200 .map_err(|e| format!("Database error: {}", e))?;
201
202 Ok(result.rows_affected() > 0)
203 }
204
205 async fn find_all_paginated(
206 &self,
207 page_request: &crate::application::dto::PageRequest,
208 organization_id: Option<Uuid>,
209 ) -> Result<(Vec<Meeting>, i64), String> {
210 page_request.validate()?;
211
212 let where_clause = if let Some(org_id) = organization_id {
213 format!("WHERE organization_id = '{}'", org_id)
214 } else {
215 String::new()
216 };
217
218 let count_query = format!("SELECT COUNT(*) FROM meetings {}", where_clause);
219 let total_items = sqlx::query_scalar::<_, i64>(&count_query)
220 .fetch_one(&self.pool)
221 .await
222 .map_err(|e| format!("Database error: {}", e))?;
223
224 let data_query = format!(
225 "SELECT {} FROM meetings {} ORDER BY scheduled_date DESC LIMIT {} OFFSET {}",
226 SELECT_COLUMNS,
227 where_clause,
228 page_request.limit(),
229 page_request.offset()
230 );
231
232 let rows = sqlx::query(&data_query)
233 .fetch_all(&self.pool)
234 .await
235 .map_err(|e| format!("Database error: {}", e))?;
236
237 Ok((rows.iter().map(row_to_meeting).collect(), total_items))
238 }
239}