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}