1use crate::application::ports::OrganizationRepository;
2use crate::domain::entities::{Organization, SubscriptionPlan};
3use crate::infrastructure::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use uuid::Uuid;
7
8pub struct PostgresOrganizationRepository {
9 pool: DbPool,
10}
11
12impl PostgresOrganizationRepository {
13 pub fn new(pool: DbPool) -> Self {
14 Self { pool }
15 }
16}
17
18#[async_trait]
19impl OrganizationRepository for PostgresOrganizationRepository {
20 async fn create(&self, org: &Organization) -> Result<Organization, String> {
21 sqlx::query!(
22 r#"
23 INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at)
24 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
25 "#,
26 org.id,
27 org.name,
28 org.slug,
29 org.contact_email,
30 org.contact_phone,
31 org.subscription_plan.to_string(),
32 org.max_buildings,
33 org.max_users,
34 org.is_active,
35 org.created_at,
36 org.updated_at,
37 )
38 .execute(&self.pool)
39 .await
40 .map_err(|e| format!("Failed to create organization: {}", e))?;
41
42 Ok(org.clone())
43 }
44
45 async fn find_by_id(&self, id: Uuid) -> Result<Option<Organization>, String> {
46 let result = sqlx::query!(
47 r#"
48 SELECT id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at
49 FROM organizations
50 WHERE id = $1
51 "#,
52 id
53 )
54 .fetch_optional(&self.pool)
55 .await
56 .map_err(|e| format!("Failed to find organization: {}", e))?;
57
58 match result {
59 Some(row) => {
60 let subscription_plan = row
61 .subscription_plan
62 .parse::<SubscriptionPlan>()
63 .map_err(|e| format!("Invalid subscription plan: {}", e))?;
64
65 Ok(Some(Organization {
66 id: row.id,
67 name: row.name,
68 slug: row.slug,
69 contact_email: row.contact_email,
70 contact_phone: row.contact_phone,
71 subscription_plan,
72 max_buildings: row.max_buildings,
73 max_users: row.max_users,
74 is_active: row.is_active,
75 created_at: row.created_at,
76 updated_at: row.updated_at,
77 }))
78 }
79 None => Ok(None),
80 }
81 }
82
83 async fn find_by_slug(&self, slug: &str) -> Result<Option<Organization>, String> {
84 let result = sqlx::query!(
85 r#"
86 SELECT id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at
87 FROM organizations
88 WHERE slug = $1
89 "#,
90 slug
91 )
92 .fetch_optional(&self.pool)
93 .await
94 .map_err(|e| format!("Failed to find organization by slug: {}", e))?;
95
96 match result {
97 Some(row) => {
98 let subscription_plan = row
99 .subscription_plan
100 .parse::<SubscriptionPlan>()
101 .map_err(|e| format!("Invalid subscription plan: {}", e))?;
102
103 Ok(Some(Organization {
104 id: row.id,
105 name: row.name,
106 slug: row.slug,
107 contact_email: row.contact_email,
108 contact_phone: row.contact_phone,
109 subscription_plan,
110 max_buildings: row.max_buildings,
111 max_users: row.max_users,
112 is_active: row.is_active,
113 created_at: row.created_at,
114 updated_at: row.updated_at,
115 }))
116 }
117 None => Ok(None),
118 }
119 }
120
121 async fn find_page(
130 &self,
131 recherche: Option<String>,
132 limit: i64,
133 offset: i64,
134 ) -> Result<Vec<Organization>, String> {
135 let motif = recherche
148 .map(|r| r.trim().to_string())
149 .filter(|r| !r.is_empty())
150 .map(|r| format!("%{r}%"));
151
152 let rows = sqlx::query(
153 "SELECT id, name, slug, contact_email, contact_phone, subscription_plan, \
154 max_buildings, max_users, is_active, created_at, updated_at \
155 FROM organizations \
156 WHERE $1::text IS NULL OR name ILIKE $1 OR slug ILIKE $1 OR contact_email ILIKE $1 \
157 ORDER BY name ASC \
158 LIMIT $2 OFFSET $3",
159 )
160 .bind(motif.as_deref())
161 .bind(limit)
162 .bind(offset)
163 .fetch_all(&self.pool)
164 .await
165 .map_err(|e| format!("Failed to fetch organizations page: {e}"))?;
166
167 Ok(rows
168 .into_iter()
169 .filter_map(|row| {
170 let plan: String = row.get("subscription_plan");
171 let subscription_plan = plan.parse::<SubscriptionPlan>().ok()?;
172 Some(Organization {
173 id: row.get("id"),
174 name: row.get("name"),
175 slug: row.get("slug"),
176 contact_email: row.get("contact_email"),
177 contact_phone: row.get("contact_phone"),
178 subscription_plan,
179 max_buildings: row.get("max_buildings"),
180 max_users: row.get("max_users"),
181 is_active: row.get("is_active"),
182 created_at: row.get("created_at"),
183 updated_at: row.get("updated_at"),
184 })
185 })
186 .collect())
187 }
188
189 async fn count_matching(&self, recherche: Option<String>) -> Result<i64, String> {
190 let motif = recherche
191 .map(|r| r.trim().to_string())
192 .filter(|r| !r.is_empty())
193 .map(|r| format!("%{r}%"));
194
195 let total: i64 = sqlx::query_scalar(
196 "SELECT COUNT(*) FROM organizations \
197 WHERE $1::text IS NULL OR name ILIKE $1 OR slug ILIKE $1 OR contact_email ILIKE $1",
198 )
199 .bind(motif.as_deref())
200 .fetch_one(&self.pool)
201 .await
202 .map_err(|e| format!("Failed to count organizations: {e}"))?;
203
204 Ok(total)
205 }
206
207 async fn find_all(&self) -> Result<Vec<Organization>, String> {
208 let rows = sqlx::query!(
209 r#"
210 SELECT id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at
211 FROM organizations
212 ORDER BY created_at DESC
213 "#
214 )
215 .fetch_all(&self.pool)
216 .await
217 .map_err(|e| format!("Failed to fetch organizations: {}", e))?;
218
219 let orgs = rows
220 .into_iter()
221 .filter_map(|row| {
222 let subscription_plan = row.subscription_plan.parse::<SubscriptionPlan>().ok()?;
223 Some(Organization {
224 id: row.id,
225 name: row.name,
226 slug: row.slug,
227 contact_email: row.contact_email,
228 contact_phone: row.contact_phone,
229 subscription_plan,
230 max_buildings: row.max_buildings,
231 max_users: row.max_users,
232 is_active: row.is_active,
233 created_at: row.created_at,
234 updated_at: row.updated_at,
235 })
236 })
237 .collect();
238
239 Ok(orgs)
240 }
241
242 async fn update(&self, org: &Organization) -> Result<Organization, String> {
243 sqlx::query!(
244 r#"
245 UPDATE organizations
246 SET name = $2, slug = $3, contact_email = $4, contact_phone = $5,
247 subscription_plan = $6, max_buildings = $7, max_users = $8,
248 is_active = $9, updated_at = $10
249 WHERE id = $1
250 "#,
251 org.id,
252 org.name,
253 org.slug,
254 org.contact_email,
255 org.contact_phone,
256 org.subscription_plan.to_string(),
257 org.max_buildings,
258 org.max_users,
259 org.is_active,
260 org.updated_at,
261 )
262 .execute(&self.pool)
263 .await
264 .map_err(|e| format!("Failed to update organization: {}", e))?;
265
266 Ok(org.clone())
267 }
268
269 async fn delete(&self, id: Uuid) -> Result<bool, String> {
270 let result = sqlx::query!(
271 r#"
272 DELETE FROM organizations
273 WHERE id = $1
274 "#,
275 id
276 )
277 .execute(&self.pool)
278 .await
279 .map_err(|e| format!("Failed to delete organization: {}", e))?;
280
281 Ok(result.rows_affected() > 0)
282 }
283
284 async fn count_buildings(&self, org_id: Uuid) -> Result<i64, String> {
285 let result = sqlx::query!(
287 r#"
288 SELECT COUNT(*) as count
289 FROM buildings b
290 JOIN acps a ON a.id = b.acp_id
291 WHERE a.organization_id = $1
292 "#,
293 org_id
294 )
295 .fetch_one(&self.pool)
296 .await
297 .map_err(|e| format!("Failed to count buildings: {}", e))?;
298
299 Ok(result.count.unwrap_or(0))
300 }
301}