1use crate::application::dto::{PageRequest, TechnicalInspectionFilters};
2use crate::application::ports::TechnicalInspectionRepository;
3use crate::domain::entities::{InspectionStatus, InspectionType, TechnicalInspection};
4use crate::infrastructure::database::pool::DbPool;
5use async_trait::async_trait;
6use sqlx::Row;
7use uuid::Uuid;
8
9pub struct PostgresTechnicalInspectionRepository {
10 pool: DbPool,
11}
12
13impl PostgresTechnicalInspectionRepository {
14 pub fn new(pool: DbPool) -> Self {
15 Self { pool }
16 }
17}
18
19#[async_trait]
20impl TechnicalInspectionRepository for PostgresTechnicalInspectionRepository {
21 async fn create(
22 &self,
23 inspection: &TechnicalInspection,
24 ) -> Result<TechnicalInspection, String> {
25 sqlx::query(
26 r#"
27 INSERT INTO technical_inspections (
28 id, organization_id, building_id, title, description, inspection_type,
29 inspector_name, inspector_company, inspector_certification,
30 inspection_date, next_due_date, status, result_summary, defects_found,
31 recommendations, compliant, compliance_certificate_number,
32 compliance_valid_until, cost, invoice_number, reports, photos,
33 certificates, notes, created_at, updated_at
34 )
35 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)
36 "#,
37 )
38 .bind(inspection.id)
39 .bind(inspection.organization_id)
40 .bind(inspection.building_id)
41 .bind(&inspection.title)
42 .bind(&inspection.description)
43 .bind(inspection_type_to_sql(&inspection.inspection_type))
44 .bind(&inspection.inspector_name)
45 .bind(&inspection.inspector_company)
46 .bind(&inspection.inspector_certification)
47 .bind(inspection.inspection_date)
48 .bind(inspection.next_due_date)
49 .bind(inspection_status_to_sql(&inspection.status))
50 .bind(&inspection.result_summary)
51 .bind(&inspection.defects_found)
52 .bind(&inspection.recommendations)
53 .bind(inspection.compliant)
54 .bind(&inspection.compliance_certificate_number)
55 .bind(inspection.compliance_valid_until)
56 .bind(inspection.cost)
57 .bind(&inspection.invoice_number)
58 .bind(serde_json::to_value(&inspection.reports).unwrap_or(serde_json::json!([])))
59 .bind(serde_json::to_value(&inspection.photos).unwrap_or(serde_json::json!([])))
60 .bind(serde_json::to_value(&inspection.certificates).unwrap_or(serde_json::json!([])))
61 .bind(&inspection.notes)
62 .bind(inspection.created_at)
63 .bind(inspection.updated_at)
64 .execute(&self.pool)
65 .await
66 .map_err(|e| format!("Database error creating technical inspection: {}", e))?;
67
68 Ok(inspection.clone())
69 }
70
71 async fn find_by_id(&self, id: Uuid) -> Result<Option<TechnicalInspection>, String> {
72 let row = sqlx::query(
73 r#"
74 SELECT
75 id, organization_id, building_id, title, description, inspection_type,
76 inspector_name, inspector_company, inspector_certification,
77 inspection_date, next_due_date, status, result_summary, defects_found,
78 recommendations, compliant, compliance_certificate_number,
79 compliance_valid_until, cost, invoice_number, reports, photos,
80 certificates, notes, created_at, updated_at
81 FROM technical_inspections
82 WHERE id = $1
83 "#,
84 )
85 .bind(id)
86 .fetch_optional(&self.pool)
87 .await
88 .map_err(|e| format!("Database error finding technical inspection: {}", e))?;
89
90 Ok(row.map(|r| map_row_to_technical_inspection(&r)))
91 }
92
93 async fn find_by_building(
94 &self,
95 building_id: Uuid,
96 ) -> Result<Vec<TechnicalInspection>, String> {
97 let rows = sqlx::query(
98 r#"
99 SELECT
100 id, organization_id, building_id, title, description, inspection_type,
101 inspector_name, inspector_company, inspector_certification,
102 inspection_date, next_due_date, status, result_summary, defects_found,
103 recommendations, compliant, compliance_certificate_number,
104 compliance_valid_until, cost, invoice_number, reports, photos,
105 certificates, notes, created_at, updated_at
106 FROM technical_inspections
107 WHERE building_id = $1
108 ORDER BY inspection_date DESC
109 "#,
110 )
111 .bind(building_id)
112 .fetch_all(&self.pool)
113 .await
114 .map_err(|e| format!("Database error finding inspections by building: {}", e))?;
115
116 Ok(rows.iter().map(map_row_to_technical_inspection).collect())
117 }
118
119 async fn find_by_organization(
120 &self,
121 organization_id: Uuid,
122 ) -> Result<Vec<TechnicalInspection>, String> {
123 let rows = sqlx::query(
124 r#"
125 SELECT
126 id, organization_id, building_id, title, description, inspection_type,
127 inspector_name, inspector_company, inspector_certification,
128 inspection_date, next_due_date, status, result_summary, defects_found,
129 recommendations, compliant, compliance_certificate_number,
130 compliance_valid_until, cost, invoice_number, reports, photos,
131 certificates, notes, created_at, updated_at
132 FROM technical_inspections
133 WHERE organization_id = $1
134 ORDER BY inspection_date DESC
135 "#,
136 )
137 .bind(organization_id)
138 .fetch_all(&self.pool)
139 .await
140 .map_err(|e| format!("Database error finding inspections by organization: {}", e))?;
141
142 Ok(rows.iter().map(map_row_to_technical_inspection).collect())
143 }
144
145 async fn find_all_paginated(
146 &self,
147 page_request: &PageRequest,
148 filters: &TechnicalInspectionFilters,
149 ) -> Result<(Vec<TechnicalInspection>, i64), String> {
150 let offset = page_request.offset();
151 let limit = page_request.limit();
152
153 let mut where_clauses = vec![];
155 let mut bind_count = 0;
156
157 #[allow(unused_variables)]
162 if let Some(organization_id) = filters.organization_id {
163 bind_count += 1;
164 where_clauses.push(format!("organization_id = ${}", bind_count));
165 }
166
167 #[allow(unused_variables)]
168 if let Some(building_id) = filters.building_id {
169 bind_count += 1;
170 where_clauses.push(format!("building_id = ${}", bind_count));
171 }
172
173 #[allow(unused_variables)]
174 if let Some(ref inspection_type) = filters.inspection_type {
175 bind_count += 1;
176 where_clauses.push(format!("inspection_type = ${}", bind_count));
177 }
178
179 #[allow(unused_variables)]
180 if let Some(ref status) = filters.status {
181 bind_count += 1;
182 where_clauses.push(format!("status = ${}", bind_count));
183 }
184
185 let where_clause = if where_clauses.is_empty() {
186 String::new()
187 } else {
188 format!("WHERE {}", where_clauses.join(" AND "))
189 };
190
191 let count_query = format!(
193 "SELECT COUNT(*) FROM technical_inspections {}",
194 where_clause
195 );
196 let mut count_query = sqlx::query_scalar::<_, i64>(&count_query);
197
198 if let Some(organization_id) = filters.organization_id {
199 count_query = count_query.bind(organization_id);
200 }
201 if let Some(building_id) = filters.building_id {
202 count_query = count_query.bind(building_id);
203 }
204 if let Some(ref inspection_type) = filters.inspection_type {
205 count_query = count_query.bind(inspection_type);
206 }
207 if let Some(ref status) = filters.status {
208 count_query = count_query.bind(status);
209 }
210
211 let total = count_query
212 .fetch_one(&self.pool)
213 .await
214 .map_err(|e| format!("Database error counting inspections: {}", e))?;
215
216 let select_query = format!(
218 r#"
219 SELECT
220 id, organization_id, building_id, title, description, inspection_type,
221 inspector_name, inspector_company, inspector_certification,
222 inspection_date, next_due_date, status, result_summary, defects_found,
223 recommendations, compliant, compliance_certificate_number,
224 compliance_valid_until, cost, invoice_number, reports, photos,
225 certificates, notes, created_at, updated_at
226 FROM technical_inspections
227 {}
228 ORDER BY inspection_date DESC
229 LIMIT ${}
230 OFFSET ${}
231 "#,
232 where_clause,
233 bind_count + 1,
234 bind_count + 2
235 );
236
237 let mut select_query = sqlx::query(&select_query);
238
239 if let Some(organization_id) = filters.organization_id {
240 select_query = select_query.bind(organization_id);
241 }
242 if let Some(building_id) = filters.building_id {
243 select_query = select_query.bind(building_id);
244 }
245 if let Some(ref inspection_type) = filters.inspection_type {
246 select_query = select_query.bind(inspection_type);
247 }
248 if let Some(ref status) = filters.status {
249 select_query = select_query.bind(status);
250 }
251
252 let rows = select_query
253 .bind(limit)
254 .bind(offset)
255 .fetch_all(&self.pool)
256 .await
257 .map_err(|e| format!("Database error fetching inspections: {}", e))?;
258
259 let inspections = rows.iter().map(map_row_to_technical_inspection).collect();
260
261 Ok((inspections, total))
262 }
263
264 async fn find_overdue(&self, building_id: Uuid) -> Result<Vec<TechnicalInspection>, String> {
265 let rows = sqlx::query(
266 r#"
267 SELECT
268 id, organization_id, building_id, title, description, inspection_type,
269 inspector_name, inspector_company, inspector_certification,
270 inspection_date, next_due_date, status, result_summary, defects_found,
271 recommendations, compliant, compliance_certificate_number,
272 compliance_valid_until, cost, invoice_number, reports, photos,
273 certificates, notes, created_at, updated_at
274 FROM technical_inspections
275 WHERE building_id = $1
276 AND next_due_date < NOW()
277 AND status = 'pending'
278 ORDER BY next_due_date ASC
279 "#,
280 )
281 .bind(building_id)
282 .fetch_all(&self.pool)
283 .await
284 .map_err(|e| format!("Database error finding overdue inspections: {}", e))?;
285
286 Ok(rows.iter().map(map_row_to_technical_inspection).collect())
287 }
288
289 async fn find_upcoming(
290 &self,
291 building_id: Uuid,
292 days: i32,
293 ) -> Result<Vec<TechnicalInspection>, String> {
294 let rows = sqlx::query(
295 r#"
296 SELECT
297 id, organization_id, building_id, title, description, inspection_type,
298 inspector_name, inspector_company, inspector_certification,
299 inspection_date, next_due_date, status, result_summary, defects_found,
300 recommendations, compliant, compliance_certificate_number,
301 compliance_valid_until, cost, invoice_number, reports, photos,
302 certificates, notes, created_at, updated_at
303 FROM technical_inspections
304 WHERE building_id = $1
305 AND next_due_date > NOW()
306 AND next_due_date <= NOW() + INTERVAL '1 day' * $2
307 AND status = 'pending'
308 ORDER BY next_due_date ASC
309 "#,
310 )
311 .bind(building_id)
312 .bind(days)
313 .fetch_all(&self.pool)
314 .await
315 .map_err(|e| format!("Database error finding upcoming inspections: {}", e))?;
316
317 Ok(rows.iter().map(map_row_to_technical_inspection).collect())
318 }
319
320 async fn find_by_type(
321 &self,
322 building_id: Uuid,
323 inspection_type: &str,
324 ) -> Result<Vec<TechnicalInspection>, String> {
325 let rows = sqlx::query(
326 r#"
327 SELECT
328 id, organization_id, building_id, title, description, inspection_type,
329 inspector_name, inspector_company, inspector_certification,
330 inspection_date, next_due_date, status, result_summary, defects_found,
331 recommendations, compliant, compliance_certificate_number,
332 compliance_valid_until, cost, invoice_number, reports, photos,
333 certificates, notes, created_at, updated_at
334 FROM technical_inspections
335 WHERE building_id = $1
336 AND inspection_type = $2
337 ORDER BY inspection_date DESC
338 "#,
339 )
340 .bind(building_id)
341 .bind(inspection_type)
342 .fetch_all(&self.pool)
343 .await
344 .map_err(|e| format!("Database error finding inspections by type: {}", e))?;
345
346 Ok(rows.iter().map(map_row_to_technical_inspection).collect())
347 }
348
349 async fn update(
350 &self,
351 inspection: &TechnicalInspection,
352 ) -> Result<TechnicalInspection, String> {
353 sqlx::query(
354 r#"
355 UPDATE technical_inspections
356 SET
357 building_id = $2,
358 title = $3,
359 description = $4,
360 inspection_type = $5,
361 inspector_name = $6,
362 inspector_company = $7,
363 inspector_certification = $8,
364 inspection_date = $9,
365 next_due_date = $10,
366 status = $11,
367 result_summary = $12,
368 defects_found = $13,
369 recommendations = $14,
370 compliant = $15,
371 compliance_certificate_number = $16,
372 compliance_valid_until = $17,
373 cost = $18,
374 invoice_number = $19,
375 reports = $20,
376 photos = $21,
377 certificates = $22,
378 notes = $23,
379 updated_at = $24
380 WHERE id = $1
381 "#,
382 )
383 .bind(inspection.id)
384 .bind(inspection.building_id)
385 .bind(&inspection.title)
386 .bind(&inspection.description)
387 .bind(inspection_type_to_sql(&inspection.inspection_type))
388 .bind(&inspection.inspector_name)
389 .bind(&inspection.inspector_company)
390 .bind(&inspection.inspector_certification)
391 .bind(inspection.inspection_date)
392 .bind(inspection.next_due_date)
393 .bind(inspection_status_to_sql(&inspection.status))
394 .bind(&inspection.result_summary)
395 .bind(&inspection.defects_found)
396 .bind(&inspection.recommendations)
397 .bind(inspection.compliant)
398 .bind(&inspection.compliance_certificate_number)
399 .bind(inspection.compliance_valid_until)
400 .bind(inspection.cost)
401 .bind(&inspection.invoice_number)
402 .bind(serde_json::to_value(&inspection.reports).unwrap_or(serde_json::json!([])))
403 .bind(serde_json::to_value(&inspection.photos).unwrap_or(serde_json::json!([])))
404 .bind(serde_json::to_value(&inspection.certificates).unwrap_or(serde_json::json!([])))
405 .bind(&inspection.notes)
406 .bind(inspection.updated_at)
407 .execute(&self.pool)
408 .await
409 .map_err(|e| format!("Database error updating technical inspection: {}", e))?;
410
411 Ok(inspection.clone())
412 }
413
414 async fn delete(&self, id: Uuid) -> Result<bool, String> {
415 let result = sqlx::query("DELETE FROM technical_inspections WHERE id = $1")
416 .bind(id)
417 .execute(&self.pool)
418 .await
419 .map_err(|e| format!("Database error deleting technical inspection: {}", e))?;
420
421 Ok(result.rows_affected() > 0)
422 }
423}
424
425fn map_row_to_technical_inspection(row: &sqlx::postgres::PgRow) -> TechnicalInspection {
427 let inspection_type_str: String = row.get("inspection_type");
428 let inspection_type = inspection_type_from_sql(&inspection_type_str);
429
430 let status_str: String = row.get("status");
431 let status = inspection_status_from_sql(&status_str);
432
433 let reports: Vec<String> = row
434 .get::<serde_json::Value, _>("reports")
435 .as_array()
436 .map(|arr| {
437 arr.iter()
438 .filter_map(|v| v.as_str().map(String::from))
439 .collect()
440 })
441 .unwrap_or_default();
442
443 let photos: Vec<String> = row
444 .get::<serde_json::Value, _>("photos")
445 .as_array()
446 .map(|arr| {
447 arr.iter()
448 .filter_map(|v| v.as_str().map(String::from))
449 .collect()
450 })
451 .unwrap_or_default();
452
453 let certificates: Vec<String> = row
454 .get::<serde_json::Value, _>("certificates")
455 .as_array()
456 .map(|arr| {
457 arr.iter()
458 .filter_map(|v| v.as_str().map(String::from))
459 .collect()
460 })
461 .unwrap_or_default();
462
463 TechnicalInspection {
464 id: row.get("id"),
465 organization_id: row.get("organization_id"),
466 building_id: row.get("building_id"),
467 inspection_type,
468 title: row.get("title"),
469 description: row.get("description"),
470 inspector_name: row.get("inspector_name"),
471 inspector_company: row.get("inspector_company"),
472 inspector_certification: row.get("inspector_certification"),
473 inspection_date: row.get("inspection_date"),
474 next_due_date: row.get("next_due_date"),
475 status,
476 result_summary: row.get("result_summary"),
477 defects_found: row.get("defects_found"),
478 recommendations: row.get("recommendations"),
479 compliant: row.get("compliant"),
480 compliance_certificate_number: row.get("compliance_certificate_number"),
481 compliance_valid_until: row.get("compliance_valid_until"),
482 cost: row.get("cost"),
483 invoice_number: row.get("invoice_number"),
484 reports,
485 photos,
486 certificates,
487 notes: row.get("notes"),
488 created_at: row.get("created_at"),
489 updated_at: row.get("updated_at"),
490 }
491}
492
493fn inspection_type_to_sql(inspection_type: &InspectionType) -> String {
495 match inspection_type {
496 InspectionType::Elevator => "elevator".to_string(),
497 InspectionType::Boiler => "boiler".to_string(),
498 InspectionType::Electrical => "electrical".to_string(),
499 InspectionType::FireExtinguisher => "fire_extinguisher".to_string(),
500 InspectionType::FireAlarm => "fire_alarm".to_string(),
501 InspectionType::GasInstallation => "gas_installation".to_string(),
502 InspectionType::RoofStructure => "roof".to_string(),
503 InspectionType::Facade => "facade".to_string(),
504 InspectionType::WaterQuality => "water_tank".to_string(),
505 InspectionType::Other { name: _ } => {
506 "other".to_string()
509 }
510 }
511}
512
513fn inspection_type_from_sql(s: &str) -> InspectionType {
515 match s {
516 "elevator" => InspectionType::Elevator,
517 "boiler" => InspectionType::Boiler,
518 "electrical" => InspectionType::Electrical,
519 "fire_extinguisher" => InspectionType::FireExtinguisher,
520 "fire_alarm" => InspectionType::FireAlarm,
521 "gas_installation" => InspectionType::GasInstallation,
522 "roof" => InspectionType::RoofStructure,
523 "facade" => InspectionType::Facade,
524 "water_tank" => InspectionType::WaterQuality,
525 "drainage" => InspectionType::Other {
526 name: "Drainage".to_string(),
527 },
528 "emergency_lighting" => InspectionType::Other {
529 name: "Emergency Lighting".to_string(),
530 },
531 "other" => InspectionType::Other {
532 name: "Other".to_string(),
533 },
534 _ => InspectionType::Other {
535 name: s.to_string(),
536 },
537 }
538}
539
540fn inspection_status_to_sql(status: &InspectionStatus) -> &'static str {
542 match status {
543 InspectionStatus::Scheduled => "pending",
544 InspectionStatus::InProgress => "pending", InspectionStatus::Completed => "completed",
546 InspectionStatus::Failed => "failed",
547 InspectionStatus::Overdue => "pending", InspectionStatus::Cancelled => "failed", }
550}
551
552fn inspection_status_from_sql(s: &str) -> InspectionStatus {
554 match s {
555 "pending" => InspectionStatus::Scheduled,
556 "completed" => InspectionStatus::Completed,
557 "failed" => InspectionStatus::Failed,
558 "passed_with_remarks" => InspectionStatus::Completed, _ => InspectionStatus::Scheduled,
560 }
561}