koprogo_api/infrastructure/database/repositories/
user_repository_impl.rs

1use crate::application::ports::UserRepository;
2use crate::domain::entities::{User, UserRole};
3use crate::infrastructure::pool::DbPool;
4use async_trait::async_trait;
5use uuid::Uuid;
6
7pub struct PostgresUserRepository {
8    pool: DbPool,
9}
10
11impl PostgresUserRepository {
12    pub fn new(pool: DbPool) -> Self {
13        Self { pool }
14    }
15}
16
17#[async_trait]
18impl UserRepository for PostgresUserRepository {
19    async fn create(&self, user: &User) -> Result<User, String> {
20        sqlx::query!(
21            r#"
22            INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
23            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
24            "#,
25            user.id,
26            user.email,
27            user.password_hash,
28            user.first_name,
29            user.last_name,
30            user.role.to_string(),
31            user.organization_id,
32            user.is_active,
33            user.created_at,
34            user.updated_at,
35        )
36        .execute(&self.pool)
37        .await
38        .map_err(|e| format!("Failed to create user: {}", e))?;
39
40        Ok(user.clone())
41    }
42
43    async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, String> {
44        let result = sqlx::query!(
45            r#"
46            SELECT id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at
47            FROM users
48            WHERE id = $1
49            "#,
50            id
51        )
52        .fetch_optional(&self.pool)
53        .await
54        .map_err(|e| format!("Failed to find user: {}", e))?;
55
56        match result {
57            Some(row) => {
58                let role = row
59                    .role
60                    .parse::<UserRole>()
61                    .map_err(|e| format!("Invalid role: {}", e))?;
62
63                Ok(Some(User {
64                    id: row.id,
65                    email: row.email,
66                    password_hash: row.password_hash,
67                    first_name: row.first_name,
68                    last_name: row.last_name,
69                    role,
70                    organization_id: row.organization_id,
71                    is_active: row.is_active,
72                    processing_restricted: false,
73                    processing_restricted_at: None,
74                    marketing_opt_out: false,
75                    marketing_opt_out_at: None,
76                    created_at: row.created_at,
77                    updated_at: row.updated_at,
78                }))
79            }
80            None => Ok(None),
81        }
82    }
83
84    async fn find_by_email(&self, email: &str) -> Result<Option<User>, String> {
85        let result = sqlx::query!(
86            r#"
87            SELECT id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at
88            FROM users
89            WHERE email = $1
90            "#,
91            email
92        )
93        .fetch_optional(&self.pool)
94        .await
95        .map_err(|e| format!("Failed to find user by email: {}", e))?;
96
97        match result {
98            Some(row) => {
99                let role = row
100                    .role
101                    .parse::<UserRole>()
102                    .map_err(|e| format!("Invalid role: {}", e))?;
103
104                Ok(Some(User {
105                    id: row.id,
106                    email: row.email,
107                    password_hash: row.password_hash,
108                    first_name: row.first_name,
109                    last_name: row.last_name,
110                    role,
111                    organization_id: row.organization_id,
112                    is_active: row.is_active,
113                    processing_restricted: false,
114                    processing_restricted_at: None,
115                    marketing_opt_out: false,
116                    marketing_opt_out_at: None,
117                    created_at: row.created_at,
118                    updated_at: row.updated_at,
119                }))
120            }
121            None => Ok(None),
122        }
123    }
124
125    async fn find_all(&self) -> Result<Vec<User>, String> {
126        let rows = sqlx::query!(
127            r#"
128            SELECT id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at
129            FROM users
130            ORDER BY created_at DESC
131            "#
132        )
133        .fetch_all(&self.pool)
134        .await
135        .map_err(|e| format!("Failed to fetch users: {}", e))?;
136
137        let users = rows
138            .into_iter()
139            .filter_map(|row| {
140                let role = row.role.parse::<UserRole>().ok()?;
141                Some(User {
142                    id: row.id,
143                    email: row.email,
144                    password_hash: row.password_hash,
145                    first_name: row.first_name,
146                    last_name: row.last_name,
147                    role,
148                    organization_id: row.organization_id,
149                    is_active: row.is_active,
150                    processing_restricted: false,
151                    processing_restricted_at: None,
152                    marketing_opt_out: false,
153                    marketing_opt_out_at: None,
154                    created_at: row.created_at,
155                    updated_at: row.updated_at,
156                })
157            })
158            .collect();
159
160        Ok(users)
161    }
162
163    async fn find_by_organization(&self, org_id: Uuid) -> Result<Vec<User>, String> {
164        let rows = sqlx::query!(
165            r#"
166            SELECT id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at
167            FROM users
168            WHERE organization_id = $1
169            ORDER BY created_at DESC
170            "#,
171            org_id
172        )
173        .fetch_all(&self.pool)
174        .await
175        .map_err(|e| format!("Failed to fetch users by organization: {}", e))?;
176
177        let users = rows
178            .into_iter()
179            .filter_map(|row| {
180                let role = row.role.parse::<UserRole>().ok()?;
181                Some(User {
182                    id: row.id,
183                    email: row.email,
184                    password_hash: row.password_hash,
185                    first_name: row.first_name,
186                    last_name: row.last_name,
187                    role,
188                    organization_id: row.organization_id,
189                    is_active: row.is_active,
190                    processing_restricted: false,
191                    processing_restricted_at: None,
192                    marketing_opt_out: false,
193                    marketing_opt_out_at: None,
194                    created_at: row.created_at,
195                    updated_at: row.updated_at,
196                })
197            })
198            .collect();
199
200        Ok(users)
201    }
202
203    async fn update(&self, user: &User) -> Result<User, String> {
204        sqlx::query!(
205            r#"
206            UPDATE users
207            SET email = $2, first_name = $3, last_name = $4, role = $5,
208                organization_id = $6, is_active = $7, updated_at = $8
209            WHERE id = $1
210            "#,
211            user.id,
212            user.email,
213            user.first_name,
214            user.last_name,
215            user.role.to_string(),
216            user.organization_id,
217            user.is_active,
218            user.updated_at,
219        )
220        .execute(&self.pool)
221        .await
222        .map_err(|e| format!("Failed to update user: {}", e))?;
223
224        Ok(user.clone())
225    }
226
227    async fn delete(&self, id: Uuid) -> Result<bool, String> {
228        let result = sqlx::query!(
229            r#"
230            DELETE FROM users
231            WHERE id = $1
232            "#,
233            id
234        )
235        .execute(&self.pool)
236        .await
237        .map_err(|e| format!("Failed to delete user: {}", e))?;
238
239        Ok(result.rows_affected() > 0)
240    }
241
242    async fn count_by_organization(&self, org_id: Uuid) -> Result<i64, String> {
243        let result = sqlx::query!(
244            r#"
245            SELECT COUNT(*) as count
246            FROM users
247            WHERE organization_id = $1
248            "#,
249            org_id
250        )
251        .fetch_one(&self.pool)
252        .await
253        .map_err(|e| format!("Failed to count users: {}", e))?;
254
255        Ok(result.count.unwrap_or(0))
256    }
257}