koprogo_api/infrastructure/database/repositories/
contract_evaluation_repository_impl.rs

1use crate::application::ports::contract_evaluation_repository::ContractEvaluationRepository;
2use crate::domain::entities::contract_evaluation::ContractEvaluation;
3use crate::infrastructure::database::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use std::collections::HashMap;
7use uuid::Uuid;
8
9pub struct PostgresContractEvaluationRepository {
10    pool: DbPool,
11}
12
13impl PostgresContractEvaluationRepository {
14    pub fn new(pool: DbPool) -> Self {
15        Self { pool }
16    }
17}
18
19#[async_trait]
20impl ContractEvaluationRepository for PostgresContractEvaluationRepository {
21    async fn create(&self, evaluation: &ContractEvaluation) -> Result<ContractEvaluation, String> {
22        sqlx::query(
23            r#"
24            INSERT INTO contract_evaluations (
25                id, organization_id, service_provider_id, quote_id, ticket_id,
26                evaluator_id, building_id, criteria, global_score, comments,
27                would_recommend, is_legal_evaluation, is_anonymous, created_at
28            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
29            "#,
30        )
31        .bind(evaluation.id)
32        .bind(evaluation.organization_id)
33        .bind(evaluation.service_provider_id)
34        .bind(evaluation.quote_id)
35        .bind(evaluation.ticket_id)
36        .bind(evaluation.evaluator_id)
37        .bind(evaluation.building_id)
38        .bind(
39            serde_json::to_value(&evaluation.criteria)
40                .map_err(|e| format!("Failed to serialize criteria: {}", e))?,
41        )
42        .bind(evaluation.global_score)
43        .bind(&evaluation.comments)
44        .bind(evaluation.would_recommend)
45        .bind(evaluation.is_legal_evaluation)
46        .bind(evaluation.is_anonymous)
47        .bind(evaluation.created_at)
48        .execute(&self.pool)
49        .await
50        .map_err(|e| format!("Database error creating contract evaluation: {}", e))?;
51
52        Ok(evaluation.clone())
53    }
54
55    async fn find_by_id(&self, id: Uuid) -> Result<Option<ContractEvaluation>, String> {
56        let row = sqlx::query(
57            r#"
58            SELECT id, organization_id, service_provider_id, quote_id, ticket_id,
59                   evaluator_id, building_id, criteria, global_score, comments,
60                   would_recommend, is_legal_evaluation, is_anonymous, created_at
61            FROM contract_evaluations
62            WHERE id = $1
63            "#,
64        )
65        .bind(id)
66        .fetch_optional(&self.pool)
67        .await
68        .map_err(|e| format!("Database error: {}", e))?;
69
70        Ok(row.map(|row| ContractEvaluation {
71            id: row.get("id"),
72            organization_id: row.get("organization_id"),
73            service_provider_id: row.get("service_provider_id"),
74            quote_id: row.get("quote_id"),
75            ticket_id: row.get("ticket_id"),
76            evaluator_id: row.get("evaluator_id"),
77            building_id: row.get("building_id"),
78            criteria: row
79                .get::<serde_json::Value, _>("criteria")
80                .as_object()
81                .map(|obj| {
82                    obj.iter()
83                        .map(|(k, v)| (k.clone(), v.as_u64().unwrap_or(0) as u8))
84                        .collect::<HashMap<String, u8>>()
85                })
86                .unwrap_or_default(),
87            global_score: row.get("global_score"),
88            comments: row.get("comments"),
89            would_recommend: row.get("would_recommend"),
90            is_legal_evaluation: row.get("is_legal_evaluation"),
91            is_anonymous: row.get("is_anonymous"),
92            created_at: row.get("created_at"),
93        }))
94    }
95
96    async fn find_by_service_provider(
97        &self,
98        provider_id: Uuid,
99        page: i64,
100        per_page: i64,
101    ) -> Result<Vec<ContractEvaluation>, String> {
102        if page < 1 || per_page < 1 {
103            return Err("Page and per_page must be >= 1".to_string());
104        }
105
106        let offset = (page - 1) * per_page;
107
108        let rows = sqlx::query(
109            r#"
110            SELECT id, organization_id, service_provider_id, quote_id, ticket_id,
111                   evaluator_id, building_id, criteria, global_score, comments,
112                   would_recommend, is_legal_evaluation, is_anonymous, created_at
113            FROM contract_evaluations
114            WHERE service_provider_id = $1
115            ORDER BY created_at DESC
116            LIMIT $2 OFFSET $3
117            "#,
118        )
119        .bind(provider_id)
120        .bind(per_page)
121        .bind(offset)
122        .fetch_all(&self.pool)
123        .await
124        .map_err(|e| format!("Database error: {}", e))?;
125
126        Ok(rows
127            .iter()
128            .map(|row| ContractEvaluation {
129                id: row.get("id"),
130                organization_id: row.get("organization_id"),
131                service_provider_id: row.get("service_provider_id"),
132                quote_id: row.get("quote_id"),
133                ticket_id: row.get("ticket_id"),
134                evaluator_id: row.get("evaluator_id"),
135                building_id: row.get("building_id"),
136                criteria: row
137                    .get::<serde_json::Value, _>("criteria")
138                    .as_object()
139                    .map(|obj| {
140                        obj.iter()
141                            .map(|(k, v)| (k.clone(), v.as_u64().unwrap_or(0) as u8))
142                            .collect::<HashMap<String, u8>>()
143                    })
144                    .unwrap_or_default(),
145                global_score: row.get("global_score"),
146                comments: row.get("comments"),
147                would_recommend: row.get("would_recommend"),
148                is_legal_evaluation: row.get("is_legal_evaluation"),
149                is_anonymous: row.get("is_anonymous"),
150                created_at: row.get("created_at"),
151            })
152            .collect())
153    }
154
155    async fn find_by_quote(&self, quote_id: Uuid) -> Result<Vec<ContractEvaluation>, String> {
156        let rows = sqlx::query(
157            r#"
158            SELECT id, organization_id, service_provider_id, quote_id, ticket_id,
159                   evaluator_id, building_id, criteria, global_score, comments,
160                   would_recommend, is_legal_evaluation, is_anonymous, created_at
161            FROM contract_evaluations
162            WHERE quote_id = $1
163            ORDER BY created_at DESC
164            "#,
165        )
166        .bind(quote_id)
167        .fetch_all(&self.pool)
168        .await
169        .map_err(|e| format!("Database error: {}", e))?;
170
171        Ok(rows
172            .iter()
173            .map(|row| ContractEvaluation {
174                id: row.get("id"),
175                organization_id: row.get("organization_id"),
176                service_provider_id: row.get("service_provider_id"),
177                quote_id: row.get("quote_id"),
178                ticket_id: row.get("ticket_id"),
179                evaluator_id: row.get("evaluator_id"),
180                building_id: row.get("building_id"),
181                criteria: row
182                    .get::<serde_json::Value, _>("criteria")
183                    .as_object()
184                    .map(|obj| {
185                        obj.iter()
186                            .map(|(k, v)| (k.clone(), v.as_u64().unwrap_or(0) as u8))
187                            .collect::<HashMap<String, u8>>()
188                    })
189                    .unwrap_or_default(),
190                global_score: row.get("global_score"),
191                comments: row.get("comments"),
192                would_recommend: row.get("would_recommend"),
193                is_legal_evaluation: row.get("is_legal_evaluation"),
194                is_anonymous: row.get("is_anonymous"),
195                created_at: row.get("created_at"),
196            })
197            .collect())
198    }
199
200    async fn find_by_ticket(&self, ticket_id: Uuid) -> Result<Vec<ContractEvaluation>, String> {
201        let rows = sqlx::query(
202            r#"
203            SELECT id, organization_id, service_provider_id, quote_id, ticket_id,
204                   evaluator_id, building_id, criteria, global_score, comments,
205                   would_recommend, is_legal_evaluation, is_anonymous, created_at
206            FROM contract_evaluations
207            WHERE ticket_id = $1
208            ORDER BY created_at DESC
209            "#,
210        )
211        .bind(ticket_id)
212        .fetch_all(&self.pool)
213        .await
214        .map_err(|e| format!("Database error: {}", e))?;
215
216        Ok(rows
217            .iter()
218            .map(|row| ContractEvaluation {
219                id: row.get("id"),
220                organization_id: row.get("organization_id"),
221                service_provider_id: row.get("service_provider_id"),
222                quote_id: row.get("quote_id"),
223                ticket_id: row.get("ticket_id"),
224                evaluator_id: row.get("evaluator_id"),
225                building_id: row.get("building_id"),
226                criteria: row
227                    .get::<serde_json::Value, _>("criteria")
228                    .as_object()
229                    .map(|obj| {
230                        obj.iter()
231                            .map(|(k, v)| (k.clone(), v.as_u64().unwrap_or(0) as u8))
232                            .collect::<HashMap<String, u8>>()
233                    })
234                    .unwrap_or_default(),
235                global_score: row.get("global_score"),
236                comments: row.get("comments"),
237                would_recommend: row.get("would_recommend"),
238                is_legal_evaluation: row.get("is_legal_evaluation"),
239                is_anonymous: row.get("is_anonymous"),
240                created_at: row.get("created_at"),
241            })
242            .collect())
243    }
244
245    async fn find_by_building(
246        &self,
247        building_id: Uuid,
248        page: i64,
249        per_page: i64,
250    ) -> Result<Vec<ContractEvaluation>, String> {
251        if page < 1 || per_page < 1 {
252            return Err("Page and per_page must be >= 1".to_string());
253        }
254
255        let offset = (page - 1) * per_page;
256
257        let rows = sqlx::query(
258            r#"
259            SELECT id, organization_id, service_provider_id, quote_id, ticket_id,
260                   evaluator_id, building_id, criteria, global_score, comments,
261                   would_recommend, is_legal_evaluation, is_anonymous, created_at
262            FROM contract_evaluations
263            WHERE building_id = $1
264            ORDER BY created_at DESC
265            LIMIT $2 OFFSET $3
266            "#,
267        )
268        .bind(building_id)
269        .bind(per_page)
270        .bind(offset)
271        .fetch_all(&self.pool)
272        .await
273        .map_err(|e| format!("Database error: {}", e))?;
274
275        Ok(rows
276            .iter()
277            .map(|row| ContractEvaluation {
278                id: row.get("id"),
279                organization_id: row.get("organization_id"),
280                service_provider_id: row.get("service_provider_id"),
281                quote_id: row.get("quote_id"),
282                ticket_id: row.get("ticket_id"),
283                evaluator_id: row.get("evaluator_id"),
284                building_id: row.get("building_id"),
285                criteria: row
286                    .get::<serde_json::Value, _>("criteria")
287                    .as_object()
288                    .map(|obj| {
289                        obj.iter()
290                            .map(|(k, v)| (k.clone(), v.as_u64().unwrap_or(0) as u8))
291                            .collect::<HashMap<String, u8>>()
292                    })
293                    .unwrap_or_default(),
294                global_score: row.get("global_score"),
295                comments: row.get("comments"),
296                would_recommend: row.get("would_recommend"),
297                is_legal_evaluation: row.get("is_legal_evaluation"),
298                is_anonymous: row.get("is_anonymous"),
299                created_at: row.get("created_at"),
300            })
301            .collect())
302    }
303
304    async fn find_legal_evaluations(
305        &self,
306        building_id: Uuid,
307        page: i64,
308        per_page: i64,
309    ) -> Result<Vec<ContractEvaluation>, String> {
310        if page < 1 || per_page < 1 {
311            return Err("Page and per_page must be >= 1".to_string());
312        }
313
314        let offset = (page - 1) * per_page;
315
316        let rows = sqlx::query(
317            r#"
318            SELECT id, organization_id, service_provider_id, quote_id, ticket_id,
319                   evaluator_id, building_id, criteria, global_score, comments,
320                   would_recommend, is_legal_evaluation, is_anonymous, created_at
321            FROM contract_evaluations
322            WHERE building_id = $1 AND is_legal_evaluation = TRUE
323            ORDER BY created_at DESC
324            LIMIT $2 OFFSET $3
325            "#,
326        )
327        .bind(building_id)
328        .bind(per_page)
329        .bind(offset)
330        .fetch_all(&self.pool)
331        .await
332        .map_err(|e| format!("Database error: {}", e))?;
333
334        Ok(rows
335            .iter()
336            .map(|row| ContractEvaluation {
337                id: row.get("id"),
338                organization_id: row.get("organization_id"),
339                service_provider_id: row.get("service_provider_id"),
340                quote_id: row.get("quote_id"),
341                ticket_id: row.get("ticket_id"),
342                evaluator_id: row.get("evaluator_id"),
343                building_id: row.get("building_id"),
344                criteria: row
345                    .get::<serde_json::Value, _>("criteria")
346                    .as_object()
347                    .map(|obj| {
348                        obj.iter()
349                            .map(|(k, v)| (k.clone(), v.as_u64().unwrap_or(0) as u8))
350                            .collect::<HashMap<String, u8>>()
351                    })
352                    .unwrap_or_default(),
353                global_score: row.get("global_score"),
354                comments: row.get("comments"),
355                would_recommend: row.get("would_recommend"),
356                is_legal_evaluation: row.get("is_legal_evaluation"),
357                is_anonymous: row.get("is_anonymous"),
358                created_at: row.get("created_at"),
359            })
360            .collect())
361    }
362
363    async fn update(&self, evaluation: &ContractEvaluation) -> Result<ContractEvaluation, String> {
364        sqlx::query(
365            r#"
366            UPDATE contract_evaluations
367            SET criteria = $1,
368                global_score = $2,
369                comments = $3,
370                would_recommend = $4,
371                is_legal_evaluation = $5,
372                is_anonymous = $6,
373                quote_id = $7,
374                ticket_id = $8
375            WHERE id = $9
376            "#,
377        )
378        .bind(
379            serde_json::to_value(&evaluation.criteria)
380                .map_err(|e| format!("Failed to serialize criteria: {}", e))?,
381        )
382        .bind(evaluation.global_score)
383        .bind(&evaluation.comments)
384        .bind(evaluation.would_recommend)
385        .bind(evaluation.is_legal_evaluation)
386        .bind(evaluation.is_anonymous)
387        .bind(evaluation.quote_id)
388        .bind(evaluation.ticket_id)
389        .bind(evaluation.id)
390        .execute(&self.pool)
391        .await
392        .map_err(|e| format!("Database error updating contract evaluation: {}", e))?;
393
394        Ok(evaluation.clone())
395    }
396
397    async fn delete(&self, id: Uuid) -> Result<(), String> {
398        sqlx::query("DELETE FROM contract_evaluations WHERE id = $1")
399            .bind(id)
400            .execute(&self.pool)
401            .await
402            .map_err(|e| format!("Database error deleting contract evaluation: {}", e))?;
403
404        Ok(())
405    }
406
407    async fn count_by_service_provider(&self, provider_id: Uuid) -> Result<i64, String> {
408        let row = sqlx::query(
409            "SELECT COUNT(*) as count FROM contract_evaluations WHERE service_provider_id = $1",
410        )
411        .bind(provider_id)
412        .fetch_one(&self.pool)
413        .await
414        .map_err(|e| format!("Database error: {}", e))?;
415
416        Ok(row.get::<i64, _>("count"))
417    }
418
419    async fn average_score_by_provider(&self, provider_id: Uuid) -> Result<Option<f64>, String> {
420        let row = sqlx::query("SELECT AVG(global_score) as avg_score FROM contract_evaluations WHERE service_provider_id = $1")
421            .bind(provider_id)
422            .fetch_one(&self.pool)
423            .await
424            .map_err(|e| format!("Database error: {}", e))?;
425
426        Ok(row.get::<Option<f64>, _>("avg_score"))
427    }
428}