koprogo_api/infrastructure/database/repositories/
technical_inspection_repository_impl.rs

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        // Build WHERE clause based on filters
154        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        // Count total
182        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        // Fetch paginated results
204        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
409/// Helper function to map PostgreSQL row to TechnicalInspection entity
410fn 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
477/// Convert InspectionType to SQL string
478fn 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            // Store custom inspection types as "other" in the DB
491            // The name is stored in the title field
492            "other".to_string()
493        }
494    }
495}
496
497/// Convert SQL string to InspectionType
498fn 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
524/// Convert InspectionStatus to SQL string
525fn inspection_status_to_sql(status: &InspectionStatus) -> &'static str {
526    match status {
527        InspectionStatus::Scheduled => "pending",
528        InspectionStatus::InProgress => "pending", // Map to pending in DB
529        InspectionStatus::Completed => "completed",
530        InspectionStatus::Failed => "failed",
531        InspectionStatus::Overdue => "pending", // Map to pending in DB
532        InspectionStatus::Cancelled => "failed", // Map to failed in DB
533    }
534}
535
536/// Convert SQL string to InspectionStatus
537fn 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, // Map to completed
543        _ => InspectionStatus::Scheduled,
544    }
545}