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)]
158 if let Some(building_id) = filters.building_id {
159 bind_count += 1;
160 where_clauses.push(format!("building_id = ${}", bind_count));
161 }
162
163 #[allow(unused_variables)]
164 if let Some(ref inspection_type) = filters.inspection_type {
165 bind_count += 1;
166 where_clauses.push(format!("inspection_type = ${}", bind_count));
167 }
168
169 #[allow(unused_variables)]
170 if let Some(ref status) = filters.status {
171 bind_count += 1;
172 where_clauses.push(format!("status = ${}", bind_count));
173 }
174
175 let where_clause = if where_clauses.is_empty() {
176 String::new()
177 } else {
178 format!("WHERE {}", where_clauses.join(" AND "))
179 };
180
181 let count_query = format!(
183 "SELECT COUNT(*) FROM technical_inspections {}",
184 where_clause
185 );
186 let mut count_query = sqlx::query_scalar::<_, i64>(&count_query);
187
188 if let Some(building_id) = filters.building_id {
189 count_query = count_query.bind(building_id);
190 }
191 if let Some(ref inspection_type) = filters.inspection_type {
192 count_query = count_query.bind(inspection_type);
193 }
194 if let Some(ref status) = filters.status {
195 count_query = count_query.bind(status);
196 }
197
198 let total = count_query
199 .fetch_one(&self.pool)
200 .await
201 .map_err(|e| format!("Database error counting inspections: {}", e))?;
202
203 let select_query = format!(
205 r#"
206 SELECT
207 id, organization_id, building_id, title, description, inspection_type,
208 inspector_name, inspector_company, inspector_certification,
209 inspection_date, next_due_date, status, result_summary, defects_found,
210 recommendations, compliant, compliance_certificate_number,
211 compliance_valid_until, cost, invoice_number, reports, photos,
212 certificates, notes, created_at, updated_at
213 FROM technical_inspections
214 {}
215 ORDER BY inspection_date DESC
216 LIMIT ${}
217 OFFSET ${}
218 "#,
219 where_clause,
220 bind_count + 1,
221 bind_count + 2
222 );
223
224 let mut select_query = sqlx::query(&select_query);
225
226 if let Some(building_id) = filters.building_id {
227 select_query = select_query.bind(building_id);
228 }
229 if let Some(ref inspection_type) = filters.inspection_type {
230 select_query = select_query.bind(inspection_type);
231 }
232 if let Some(ref status) = filters.status {
233 select_query = select_query.bind(status);
234 }
235
236 let rows = select_query
237 .bind(limit)
238 .bind(offset)
239 .fetch_all(&self.pool)
240 .await
241 .map_err(|e| format!("Database error fetching inspections: {}", e))?;
242
243 let inspections = rows.iter().map(map_row_to_technical_inspection).collect();
244
245 Ok((inspections, total))
246 }
247
248 async fn find_overdue(&self, building_id: Uuid) -> Result<Vec<TechnicalInspection>, String> {
249 let rows = sqlx::query(
250 r#"
251 SELECT
252 id, organization_id, building_id, title, description, inspection_type,
253 inspector_name, inspector_company, inspector_certification,
254 inspection_date, next_due_date, status, result_summary, defects_found,
255 recommendations, compliant, compliance_certificate_number,
256 compliance_valid_until, cost, invoice_number, reports, photos,
257 certificates, notes, created_at, updated_at
258 FROM technical_inspections
259 WHERE building_id = $1
260 AND next_due_date < NOW()
261 AND status = 'pending'
262 ORDER BY next_due_date ASC
263 "#,
264 )
265 .bind(building_id)
266 .fetch_all(&self.pool)
267 .await
268 .map_err(|e| format!("Database error finding overdue inspections: {}", e))?;
269
270 Ok(rows.iter().map(map_row_to_technical_inspection).collect())
271 }
272
273 async fn find_upcoming(
274 &self,
275 building_id: Uuid,
276 days: i32,
277 ) -> Result<Vec<TechnicalInspection>, String> {
278 let rows = sqlx::query(
279 r#"
280 SELECT
281 id, organization_id, building_id, title, description, inspection_type,
282 inspector_name, inspector_company, inspector_certification,
283 inspection_date, next_due_date, status, result_summary, defects_found,
284 recommendations, compliant, compliance_certificate_number,
285 compliance_valid_until, cost, invoice_number, reports, photos,
286 certificates, notes, created_at, updated_at
287 FROM technical_inspections
288 WHERE building_id = $1
289 AND next_due_date > NOW()
290 AND next_due_date <= NOW() + INTERVAL '1 day' * $2
291 AND status = 'pending'
292 ORDER BY next_due_date ASC
293 "#,
294 )
295 .bind(building_id)
296 .bind(days)
297 .fetch_all(&self.pool)
298 .await
299 .map_err(|e| format!("Database error finding upcoming inspections: {}", e))?;
300
301 Ok(rows.iter().map(map_row_to_technical_inspection).collect())
302 }
303
304 async fn find_by_type(
305 &self,
306 building_id: Uuid,
307 inspection_type: &str,
308 ) -> Result<Vec<TechnicalInspection>, String> {
309 let rows = sqlx::query(
310 r#"
311 SELECT
312 id, organization_id, building_id, title, description, inspection_type,
313 inspector_name, inspector_company, inspector_certification,
314 inspection_date, next_due_date, status, result_summary, defects_found,
315 recommendations, compliant, compliance_certificate_number,
316 compliance_valid_until, cost, invoice_number, reports, photos,
317 certificates, notes, created_at, updated_at
318 FROM technical_inspections
319 WHERE building_id = $1
320 AND inspection_type = $2
321 ORDER BY inspection_date DESC
322 "#,
323 )
324 .bind(building_id)
325 .bind(inspection_type)
326 .fetch_all(&self.pool)
327 .await
328 .map_err(|e| format!("Database error finding inspections by type: {}", e))?;
329
330 Ok(rows.iter().map(map_row_to_technical_inspection).collect())
331 }
332
333 async fn update(
334 &self,
335 inspection: &TechnicalInspection,
336 ) -> Result<TechnicalInspection, String> {
337 sqlx::query(
338 r#"
339 UPDATE technical_inspections
340 SET
341 building_id = $2,
342 title = $3,
343 description = $4,
344 inspection_type = $5,
345 inspector_name = $6,
346 inspector_company = $7,
347 inspector_certification = $8,
348 inspection_date = $9,
349 next_due_date = $10,
350 status = $11,
351 result_summary = $12,
352 defects_found = $13,
353 recommendations = $14,
354 compliant = $15,
355 compliance_certificate_number = $16,
356 compliance_valid_until = $17,
357 cost = $18,
358 invoice_number = $19,
359 reports = $20,
360 photos = $21,
361 certificates = $22,
362 notes = $23,
363 updated_at = $24
364 WHERE id = $1
365 "#,
366 )
367 .bind(inspection.id)
368 .bind(inspection.building_id)
369 .bind(&inspection.title)
370 .bind(&inspection.description)
371 .bind(inspection_type_to_sql(&inspection.inspection_type))
372 .bind(&inspection.inspector_name)
373 .bind(&inspection.inspector_company)
374 .bind(&inspection.inspector_certification)
375 .bind(inspection.inspection_date)
376 .bind(inspection.next_due_date)
377 .bind(inspection_status_to_sql(&inspection.status))
378 .bind(&inspection.result_summary)
379 .bind(&inspection.defects_found)
380 .bind(&inspection.recommendations)
381 .bind(inspection.compliant)
382 .bind(&inspection.compliance_certificate_number)
383 .bind(inspection.compliance_valid_until)
384 .bind(inspection.cost)
385 .bind(&inspection.invoice_number)
386 .bind(serde_json::to_value(&inspection.reports).unwrap_or(serde_json::json!([])))
387 .bind(serde_json::to_value(&inspection.photos).unwrap_or(serde_json::json!([])))
388 .bind(serde_json::to_value(&inspection.certificates).unwrap_or(serde_json::json!([])))
389 .bind(&inspection.notes)
390 .bind(inspection.updated_at)
391 .execute(&self.pool)
392 .await
393 .map_err(|e| format!("Database error updating technical inspection: {}", e))?;
394
395 Ok(inspection.clone())
396 }
397
398 async fn delete(&self, id: Uuid) -> Result<bool, String> {
399 let result = sqlx::query("DELETE FROM technical_inspections WHERE id = $1")
400 .bind(id)
401 .execute(&self.pool)
402 .await
403 .map_err(|e| format!("Database error deleting technical inspection: {}", e))?;
404
405 Ok(result.rows_affected() > 0)
406 }
407}
408
409fn map_row_to_technical_inspection(row: &sqlx::postgres::PgRow) -> TechnicalInspection {
411 let inspection_type_str: String = row.get("inspection_type");
412 let inspection_type = inspection_type_from_sql(&inspection_type_str);
413
414 let status_str: String = row.get("status");
415 let status = inspection_status_from_sql(&status_str);
416
417 let reports: Vec<String> = row
418 .get::<serde_json::Value, _>("reports")
419 .as_array()
420 .map(|arr| {
421 arr.iter()
422 .filter_map(|v| v.as_str().map(String::from))
423 .collect()
424 })
425 .unwrap_or_default();
426
427 let photos: Vec<String> = row
428 .get::<serde_json::Value, _>("photos")
429 .as_array()
430 .map(|arr| {
431 arr.iter()
432 .filter_map(|v| v.as_str().map(String::from))
433 .collect()
434 })
435 .unwrap_or_default();
436
437 let certificates: Vec<String> = row
438 .get::<serde_json::Value, _>("certificates")
439 .as_array()
440 .map(|arr| {
441 arr.iter()
442 .filter_map(|v| v.as_str().map(String::from))
443 .collect()
444 })
445 .unwrap_or_default();
446
447 TechnicalInspection {
448 id: row.get("id"),
449 organization_id: row.get("organization_id"),
450 building_id: row.get("building_id"),
451 inspection_type,
452 title: row.get("title"),
453 description: row.get("description"),
454 inspector_name: row.get("inspector_name"),
455 inspector_company: row.get("inspector_company"),
456 inspector_certification: row.get("inspector_certification"),
457 inspection_date: row.get("inspection_date"),
458 next_due_date: row.get("next_due_date"),
459 status,
460 result_summary: row.get("result_summary"),
461 defects_found: row.get("defects_found"),
462 recommendations: row.get("recommendations"),
463 compliant: row.get("compliant"),
464 compliance_certificate_number: row.get("compliance_certificate_number"),
465 compliance_valid_until: row.get("compliance_valid_until"),
466 cost: row.get("cost"),
467 invoice_number: row.get("invoice_number"),
468 reports,
469 photos,
470 certificates,
471 notes: row.get("notes"),
472 created_at: row.get("created_at"),
473 updated_at: row.get("updated_at"),
474 }
475}
476
477fn inspection_type_to_sql(inspection_type: &InspectionType) -> String {
479 match inspection_type {
480 InspectionType::Elevator => "elevator".to_string(),
481 InspectionType::Boiler => "boiler".to_string(),
482 InspectionType::Electrical => "electrical".to_string(),
483 InspectionType::FireExtinguisher => "fire_extinguisher".to_string(),
484 InspectionType::FireAlarm => "fire_alarm".to_string(),
485 InspectionType::GasInstallation => "gas_installation".to_string(),
486 InspectionType::RoofStructure => "roof".to_string(),
487 InspectionType::Facade => "facade".to_string(),
488 InspectionType::WaterQuality => "water_tank".to_string(),
489 InspectionType::Other { name: _ } => {
490 "other".to_string()
493 }
494 }
495}
496
497fn inspection_type_from_sql(s: &str) -> InspectionType {
499 match s {
500 "elevator" => InspectionType::Elevator,
501 "boiler" => InspectionType::Boiler,
502 "electrical" => InspectionType::Electrical,
503 "fire_extinguisher" => InspectionType::FireExtinguisher,
504 "fire_alarm" => InspectionType::FireAlarm,
505 "gas_installation" => InspectionType::GasInstallation,
506 "roof" => InspectionType::RoofStructure,
507 "facade" => InspectionType::Facade,
508 "water_tank" => InspectionType::WaterQuality,
509 "drainage" => InspectionType::Other {
510 name: "Drainage".to_string(),
511 },
512 "emergency_lighting" => InspectionType::Other {
513 name: "Emergency Lighting".to_string(),
514 },
515 "other" => InspectionType::Other {
516 name: "Other".to_string(),
517 },
518 _ => InspectionType::Other {
519 name: s.to_string(),
520 },
521 }
522}
523
524fn inspection_status_to_sql(status: &InspectionStatus) -> &'static str {
526 match status {
527 InspectionStatus::Scheduled => "pending",
528 InspectionStatus::InProgress => "pending", InspectionStatus::Completed => "completed",
530 InspectionStatus::Failed => "failed",
531 InspectionStatus::Overdue => "pending", InspectionStatus::Cancelled => "failed", }
534}
535
536fn inspection_status_from_sql(s: &str) -> InspectionStatus {
538 match s {
539 "pending" => InspectionStatus::Scheduled,
540 "completed" => InspectionStatus::Completed,
541 "failed" => InspectionStatus::Failed,
542 "passed_with_remarks" => InspectionStatus::Completed, _ => InspectionStatus::Scheduled,
544 }
545}