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 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}