koprogo_api/application/use_cases/
technical_inspection_use_cases.rs

1use crate::application::dto::{
2    AddCertificateDto, AddInspectionPhotoDto, AddReportDto, CreateTechnicalInspectionDto,
3    InspectionStatusDto, PageRequest, TechnicalInspectionFilters,
4    TechnicalInspectionListResponseDto, TechnicalInspectionResponseDto,
5    UpdateTechnicalInspectionDto,
6};
7use crate::application::ports::TechnicalInspectionRepository;
8use crate::domain::entities::{InspectionStatus, TechnicalInspection};
9use chrono::DateTime;
10use std::sync::Arc;
11use uuid::Uuid;
12
13pub struct TechnicalInspectionUseCases {
14    repository: Arc<dyn TechnicalInspectionRepository>,
15}
16
17impl TechnicalInspectionUseCases {
18    pub fn new(repository: Arc<dyn TechnicalInspectionRepository>) -> Self {
19        Self { repository }
20    }
21
22    pub async fn create_technical_inspection(
23        &self,
24        dto: CreateTechnicalInspectionDto,
25    ) -> Result<TechnicalInspectionResponseDto, String> {
26        let organization_id = Uuid::parse_str(&dto.organization_id)
27            .map_err(|_| "Invalid organization_id format".to_string())?;
28        let building_id = Uuid::parse_str(&dto.building_id)
29            .map_err(|_| "Invalid building_id format".to_string())?;
30
31        let inspection_date = DateTime::parse_from_rfc3339(&dto.inspection_date)
32            .map_err(|_| "Invalid inspection_date format".to_string())?
33            .with_timezone(&chrono::Utc);
34
35        let compliance_valid_until = if let Some(ref date_str) = dto.compliance_valid_until {
36            Some(
37                DateTime::parse_from_rfc3339(date_str)
38                    .map_err(|_| "Invalid compliance_valid_until format".to_string())?
39                    .with_timezone(&chrono::Utc),
40            )
41        } else {
42            None
43        };
44
45        let inspection = TechnicalInspection::new(
46            organization_id,
47            building_id,
48            dto.title,
49            dto.description,
50            dto.inspection_type.clone(),
51            dto.inspector_name,
52            inspection_date,
53        );
54
55        let mut inspection = inspection;
56        inspection.inspector_company = dto.inspector_company;
57        inspection.inspector_certification = dto.inspector_certification;
58        inspection.result_summary = dto.result_summary;
59        inspection.defects_found = dto.defects_found;
60        inspection.recommendations = dto.recommendations;
61        inspection.compliant = dto.compliant;
62        inspection.compliance_certificate_number = dto.compliance_certificate_number;
63        inspection.compliance_valid_until = compliance_valid_until;
64        inspection.cost = dto.cost;
65        inspection.invoice_number = dto.invoice_number;
66        inspection.notes = dto.notes;
67
68        let created = self.repository.create(&inspection).await?;
69        Ok(self.to_response_dto(&created))
70    }
71
72    pub async fn get_technical_inspection(
73        &self,
74        id: Uuid,
75    ) -> Result<Option<TechnicalInspectionResponseDto>, String> {
76        let inspection = self.repository.find_by_id(id).await?;
77        Ok(inspection.map(|i| self.to_response_dto(&i)))
78    }
79
80    pub async fn list_technical_inspections_by_building(
81        &self,
82        building_id: Uuid,
83    ) -> Result<Vec<TechnicalInspectionResponseDto>, String> {
84        let inspections = self.repository.find_by_building(building_id).await?;
85        Ok(inspections
86            .iter()
87            .map(|i| self.to_response_dto(i))
88            .collect())
89    }
90
91    pub async fn list_technical_inspections_by_organization(
92        &self,
93        organization_id: Uuid,
94    ) -> Result<Vec<TechnicalInspectionResponseDto>, String> {
95        let inspections = self
96            .repository
97            .find_by_organization(organization_id)
98            .await?;
99        Ok(inspections
100            .iter()
101            .map(|i| self.to_response_dto(i))
102            .collect())
103    }
104
105    pub async fn list_technical_inspections_paginated(
106        &self,
107        page_request: &PageRequest,
108        filters: &TechnicalInspectionFilters,
109    ) -> Result<TechnicalInspectionListResponseDto, String> {
110        let (inspections, total) = self
111            .repository
112            .find_all_paginated(page_request, filters)
113            .await?;
114
115        let dtos = inspections
116            .iter()
117            .map(|i| self.to_response_dto(i))
118            .collect();
119
120        Ok(TechnicalInspectionListResponseDto {
121            inspections: dtos,
122            total,
123            page: page_request.page,
124            page_size: page_request.per_page,
125        })
126    }
127
128    pub async fn get_overdue_inspections(
129        &self,
130        building_id: Uuid,
131    ) -> Result<Vec<InspectionStatusDto>, String> {
132        let inspections = self.repository.find_overdue(building_id).await?;
133
134        Ok(inspections
135            .iter()
136            .map(|i| InspectionStatusDto {
137                inspection_id: i.id.to_string(),
138                title: i.title.clone(),
139                inspection_type: i.inspection_type.clone(),
140                next_due_date: i.next_due_date.to_rfc3339(),
141                status: i.status.clone(),
142                is_overdue: i.is_overdue(),
143                days_until_due: i.days_until_due(),
144            })
145            .collect())
146    }
147
148    pub async fn get_upcoming_inspections(
149        &self,
150        building_id: Uuid,
151        days: i32,
152    ) -> Result<Vec<InspectionStatusDto>, String> {
153        let inspections = self.repository.find_upcoming(building_id, days).await?;
154
155        Ok(inspections
156            .iter()
157            .map(|i| InspectionStatusDto {
158                inspection_id: i.id.to_string(),
159                title: i.title.clone(),
160                inspection_type: i.inspection_type.clone(),
161                next_due_date: i.next_due_date.to_rfc3339(),
162                status: i.status.clone(),
163                is_overdue: i.is_overdue(),
164                days_until_due: i.days_until_due(),
165            })
166            .collect())
167    }
168
169    pub async fn get_inspections_by_type(
170        &self,
171        building_id: Uuid,
172        inspection_type: &str,
173    ) -> Result<Vec<TechnicalInspectionResponseDto>, String> {
174        let inspections = self
175            .repository
176            .find_by_type(building_id, inspection_type)
177            .await?;
178
179        Ok(inspections
180            .iter()
181            .map(|i| self.to_response_dto(i))
182            .collect())
183    }
184
185    pub async fn update_technical_inspection(
186        &self,
187        id: Uuid,
188        dto: UpdateTechnicalInspectionDto,
189    ) -> Result<TechnicalInspectionResponseDto, String> {
190        let mut inspection = self
191            .repository
192            .find_by_id(id)
193            .await?
194            .ok_or_else(|| "Technical inspection not found".to_string())?;
195
196        if let Some(title) = dto.title {
197            inspection.title = title;
198        }
199        if let Some(description) = dto.description {
200            inspection.description = Some(description);
201        }
202        if let Some(inspection_type) = dto.inspection_type {
203            inspection.inspection_type = inspection_type;
204            // Recalculate next_due_date when type changes
205            inspection.next_due_date = inspection.calculate_next_due_date();
206        }
207        if let Some(inspector_name) = dto.inspector_name {
208            inspection.inspector_name = inspector_name;
209        }
210        if let Some(inspector_company) = dto.inspector_company {
211            inspection.inspector_company = Some(inspector_company);
212        }
213        if let Some(inspector_certification) = dto.inspector_certification {
214            inspection.inspector_certification = Some(inspector_certification);
215        }
216        if let Some(inspection_date_str) = dto.inspection_date {
217            let inspection_date = DateTime::parse_from_rfc3339(&inspection_date_str)
218                .map_err(|_| "Invalid inspection_date format".to_string())?
219                .with_timezone(&chrono::Utc);
220            inspection.inspection_date = inspection_date;
221            // Recalculate next_due_date when date changes
222            inspection.next_due_date = inspection.calculate_next_due_date();
223        }
224        if let Some(status) = dto.status {
225            inspection.status = status;
226        }
227        if let Some(result_summary) = dto.result_summary {
228            inspection.result_summary = Some(result_summary);
229        }
230        if let Some(defects_found) = dto.defects_found {
231            inspection.defects_found = Some(defects_found);
232        }
233        if let Some(recommendations) = dto.recommendations {
234            inspection.recommendations = Some(recommendations);
235        }
236        if let Some(compliant) = dto.compliant {
237            inspection.compliant = Some(compliant);
238        }
239        if let Some(compliance_certificate_number) = dto.compliance_certificate_number {
240            inspection.compliance_certificate_number = Some(compliance_certificate_number);
241        }
242        if let Some(compliance_valid_until_str) = dto.compliance_valid_until {
243            let compliance_valid_until = DateTime::parse_from_rfc3339(&compliance_valid_until_str)
244                .map_err(|_| "Invalid compliance_valid_until format".to_string())?
245                .with_timezone(&chrono::Utc);
246            inspection.compliance_valid_until = Some(compliance_valid_until);
247        }
248        if let Some(cost) = dto.cost {
249            inspection.cost = Some(cost);
250        }
251        if let Some(invoice_number) = dto.invoice_number {
252            inspection.invoice_number = Some(invoice_number);
253        }
254        if let Some(notes) = dto.notes {
255            inspection.notes = Some(notes);
256        }
257
258        inspection.updated_at = chrono::Utc::now();
259
260        let updated = self.repository.update(&inspection).await?;
261        Ok(self.to_response_dto(&updated))
262    }
263
264    pub async fn mark_as_completed(
265        &self,
266        id: Uuid,
267    ) -> Result<TechnicalInspectionResponseDto, String> {
268        let mut inspection = self
269            .repository
270            .find_by_id(id)
271            .await?
272            .ok_or_else(|| "Technical inspection not found".to_string())?;
273
274        inspection.status = InspectionStatus::Completed;
275        inspection.updated_at = chrono::Utc::now();
276
277        let updated = self.repository.update(&inspection).await?;
278        Ok(self.to_response_dto(&updated))
279    }
280
281    pub async fn add_report(
282        &self,
283        id: Uuid,
284        dto: AddReportDto,
285    ) -> Result<TechnicalInspectionResponseDto, String> {
286        let mut inspection = self
287            .repository
288            .find_by_id(id)
289            .await?
290            .ok_or_else(|| "Technical inspection not found".to_string())?;
291
292        inspection.add_report(dto.report_path);
293
294        let updated = self.repository.update(&inspection).await?;
295        Ok(self.to_response_dto(&updated))
296    }
297
298    pub async fn add_photo(
299        &self,
300        id: Uuid,
301        dto: AddInspectionPhotoDto,
302    ) -> Result<TechnicalInspectionResponseDto, String> {
303        let mut inspection = self
304            .repository
305            .find_by_id(id)
306            .await?
307            .ok_or_else(|| "Technical inspection not found".to_string())?;
308
309        inspection.add_photo(dto.photo_path);
310
311        let updated = self.repository.update(&inspection).await?;
312        Ok(self.to_response_dto(&updated))
313    }
314
315    pub async fn add_certificate(
316        &self,
317        id: Uuid,
318        dto: AddCertificateDto,
319    ) -> Result<TechnicalInspectionResponseDto, String> {
320        let mut inspection = self
321            .repository
322            .find_by_id(id)
323            .await?
324            .ok_or_else(|| "Technical inspection not found".to_string())?;
325
326        inspection.add_certificate(dto.certificate_path);
327
328        let updated = self.repository.update(&inspection).await?;
329        Ok(self.to_response_dto(&updated))
330    }
331
332    pub async fn delete_technical_inspection(&self, id: Uuid) -> Result<bool, String> {
333        self.repository.delete(id).await
334    }
335
336    fn to_response_dto(&self, inspection: &TechnicalInspection) -> TechnicalInspectionResponseDto {
337        TechnicalInspectionResponseDto {
338            id: inspection.id.to_string(),
339            organization_id: inspection.organization_id.to_string(),
340            building_id: inspection.building_id.to_string(),
341            title: inspection.title.clone(),
342            description: inspection.description.clone(),
343            inspection_type: inspection.inspection_type.clone(),
344            inspector_name: inspection.inspector_name.clone(),
345            inspector_company: inspection.inspector_company.clone(),
346            inspector_certification: inspection.inspector_certification.clone(),
347            inspection_date: inspection.inspection_date.to_rfc3339(),
348            next_due_date: inspection.next_due_date.to_rfc3339(),
349            status: inspection.status.clone(),
350            result_summary: inspection.result_summary.clone(),
351            defects_found: inspection.defects_found.clone(),
352            recommendations: inspection.recommendations.clone(),
353            compliant: inspection.compliant,
354            compliance_certificate_number: inspection.compliance_certificate_number.clone(),
355            compliance_valid_until: inspection
356                .compliance_valid_until
357                .as_ref()
358                .map(|d| d.to_rfc3339()),
359            cost: inspection.cost,
360            invoice_number: inspection.invoice_number.clone(),
361            reports: inspection.reports.clone(),
362            photos: inspection.photos.clone(),
363            certificates: inspection.certificates.clone(),
364            notes: inspection.notes.clone(),
365            is_overdue: inspection.is_overdue(),
366            days_until_due: inspection.days_until_due(),
367            created_at: inspection.created_at.to_rfc3339(),
368            updated_at: inspection.updated_at.to_rfc3339(),
369        }
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use crate::application::dto::{
377        AddInspectionPhotoDto, AddReportDto, CreateTechnicalInspectionDto, PageRequest,
378        TechnicalInspectionFilters,
379    };
380    use crate::application::ports::TechnicalInspectionRepository;
381    use crate::domain::entities::{InspectionStatus, InspectionType, TechnicalInspection};
382    use async_trait::async_trait;
383    use std::collections::HashMap;
384    use std::sync::Mutex;
385
386    // ========== Mock Repository ==========
387
388    struct MockTechnicalInspectionRepository {
389        inspections: Mutex<HashMap<Uuid, TechnicalInspection>>,
390    }
391
392    impl MockTechnicalInspectionRepository {
393        fn new() -> Self {
394            Self {
395                inspections: Mutex::new(HashMap::new()),
396            }
397        }
398    }
399
400    #[async_trait]
401    impl TechnicalInspectionRepository for MockTechnicalInspectionRepository {
402        async fn create(
403            &self,
404            inspection: &TechnicalInspection,
405        ) -> Result<TechnicalInspection, String> {
406            let mut inspections = self.inspections.lock().unwrap();
407            inspections.insert(inspection.id, inspection.clone());
408            Ok(inspection.clone())
409        }
410
411        async fn find_by_id(&self, id: Uuid) -> Result<Option<TechnicalInspection>, String> {
412            let inspections = self.inspections.lock().unwrap();
413            Ok(inspections.get(&id).cloned())
414        }
415
416        async fn find_by_building(
417            &self,
418            building_id: Uuid,
419        ) -> Result<Vec<TechnicalInspection>, String> {
420            let inspections = self.inspections.lock().unwrap();
421            Ok(inspections
422                .values()
423                .filter(|i| i.building_id == building_id)
424                .cloned()
425                .collect())
426        }
427
428        async fn find_by_organization(
429            &self,
430            organization_id: Uuid,
431        ) -> Result<Vec<TechnicalInspection>, String> {
432            let inspections = self.inspections.lock().unwrap();
433            Ok(inspections
434                .values()
435                .filter(|i| i.organization_id == organization_id)
436                .cloned()
437                .collect())
438        }
439
440        async fn find_all_paginated(
441            &self,
442            _page_request: &PageRequest,
443            _filters: &TechnicalInspectionFilters,
444        ) -> Result<(Vec<TechnicalInspection>, i64), String> {
445            let inspections = self.inspections.lock().unwrap();
446            let all: Vec<TechnicalInspection> = inspections.values().cloned().collect();
447            let count = all.len() as i64;
448            Ok((all, count))
449        }
450
451        async fn find_overdue(
452            &self,
453            building_id: Uuid,
454        ) -> Result<Vec<TechnicalInspection>, String> {
455            let inspections = self.inspections.lock().unwrap();
456            Ok(inspections
457                .values()
458                .filter(|i| i.building_id == building_id && i.is_overdue())
459                .cloned()
460                .collect())
461        }
462
463        async fn find_upcoming(
464            &self,
465            building_id: Uuid,
466            days: i32,
467        ) -> Result<Vec<TechnicalInspection>, String> {
468            let inspections = self.inspections.lock().unwrap();
469            Ok(inspections
470                .values()
471                .filter(|i| {
472                    i.building_id == building_id
473                        && i.days_until_due() >= 0
474                        && i.days_until_due() <= days as i64
475                })
476                .cloned()
477                .collect())
478        }
479
480        async fn find_by_type(
481            &self,
482            building_id: Uuid,
483            inspection_type: &str,
484        ) -> Result<Vec<TechnicalInspection>, String> {
485            let inspections = self.inspections.lock().unwrap();
486            Ok(inspections
487                .values()
488                .filter(|i| {
489                    i.building_id == building_id
490                        && format!("{:?}", i.inspection_type).to_lowercase()
491                            == inspection_type.to_lowercase()
492                })
493                .cloned()
494                .collect())
495        }
496
497        async fn update(
498            &self,
499            inspection: &TechnicalInspection,
500        ) -> Result<TechnicalInspection, String> {
501            let mut inspections = self.inspections.lock().unwrap();
502            inspections.insert(inspection.id, inspection.clone());
503            Ok(inspection.clone())
504        }
505
506        async fn delete(&self, id: Uuid) -> Result<bool, String> {
507            let mut inspections = self.inspections.lock().unwrap();
508            Ok(inspections.remove(&id).is_some())
509        }
510    }
511
512    // ========== Helpers ==========
513
514    fn make_use_cases(repo: MockTechnicalInspectionRepository) -> TechnicalInspectionUseCases {
515        TechnicalInspectionUseCases::new(Arc::new(repo))
516    }
517
518    fn valid_create_dto(org_id: Uuid, building_id: Uuid) -> CreateTechnicalInspectionDto {
519        CreateTechnicalInspectionDto {
520            organization_id: org_id.to_string(),
521            building_id: building_id.to_string(),
522            title: "Inspection annuelle ascenseur".to_string(),
523            description: Some("Vérification complète de l'ascenseur".to_string()),
524            inspection_type: InspectionType::Elevator,
525            inspector_name: "Schindler Belgium".to_string(),
526            inspector_company: Some("Schindler SA".to_string()),
527            inspector_certification: Some("CERT-2026-001".to_string()),
528            inspection_date: "2026-03-01T10:00:00Z".to_string(),
529            result_summary: None,
530            defects_found: None,
531            recommendations: None,
532            compliant: None,
533            compliance_certificate_number: None,
534            compliance_valid_until: None,
535            cost: Some(450.0),
536            invoice_number: Some("INV-2026-100".to_string()),
537            notes: None,
538        }
539    }
540
541    // ========== Tests ==========
542
543    #[tokio::test]
544    async fn test_create_technical_inspection_success() {
545        let repo = MockTechnicalInspectionRepository::new();
546        let uc = make_use_cases(repo);
547        let org_id = Uuid::new_v4();
548        let building_id = Uuid::new_v4();
549
550        let result = uc
551            .create_technical_inspection(valid_create_dto(org_id, building_id))
552            .await;
553
554        assert!(result.is_ok());
555        let dto = result.unwrap();
556        assert_eq!(dto.organization_id, org_id.to_string());
557        assert_eq!(dto.building_id, building_id.to_string());
558        assert_eq!(dto.title, "Inspection annuelle ascenseur");
559        assert_eq!(dto.inspector_name, "Schindler Belgium");
560        assert_eq!(dto.inspector_company, Some("Schindler SA".to_string()));
561        assert_eq!(dto.cost, Some(450.0));
562        assert_eq!(dto.status, InspectionStatus::Scheduled);
563        assert!(dto.reports.is_empty());
564        assert!(dto.photos.is_empty());
565        assert!(dto.certificates.is_empty());
566    }
567
568    #[tokio::test]
569    async fn test_create_technical_inspection_invalid_date_format() {
570        let repo = MockTechnicalInspectionRepository::new();
571        let uc = make_use_cases(repo);
572        let org_id = Uuid::new_v4();
573        let building_id = Uuid::new_v4();
574
575        let mut dto = valid_create_dto(org_id, building_id);
576        dto.inspection_date = "not-a-date".to_string();
577
578        let result = uc.create_technical_inspection(dto).await;
579        assert!(result.is_err());
580        assert_eq!(result.unwrap_err(), "Invalid inspection_date format");
581    }
582
583    #[tokio::test]
584    async fn test_create_technical_inspection_invalid_org_id() {
585        let repo = MockTechnicalInspectionRepository::new();
586        let uc = make_use_cases(repo);
587
588        let mut dto = valid_create_dto(Uuid::new_v4(), Uuid::new_v4());
589        dto.organization_id = "bad-uuid".to_string();
590
591        let result = uc.create_technical_inspection(dto).await;
592        assert!(result.is_err());
593        assert_eq!(result.unwrap_err(), "Invalid organization_id format");
594    }
595
596    #[tokio::test]
597    async fn test_get_technical_inspection_found() {
598        let repo = MockTechnicalInspectionRepository::new();
599        let uc = make_use_cases(repo);
600        let org_id = Uuid::new_v4();
601        let building_id = Uuid::new_v4();
602
603        let created = uc
604            .create_technical_inspection(valid_create_dto(org_id, building_id))
605            .await
606            .unwrap();
607        let inspection_id = Uuid::parse_str(&created.id).unwrap();
608
609        let result = uc.get_technical_inspection(inspection_id).await;
610        assert!(result.is_ok());
611        let found = result.unwrap();
612        assert!(found.is_some());
613        let found = found.unwrap();
614        assert_eq!(found.id, created.id);
615        assert_eq!(found.title, "Inspection annuelle ascenseur");
616    }
617
618    #[tokio::test]
619    async fn test_get_technical_inspection_not_found() {
620        let repo = MockTechnicalInspectionRepository::new();
621        let uc = make_use_cases(repo);
622
623        let result = uc.get_technical_inspection(Uuid::new_v4()).await;
624        assert!(result.is_ok());
625        assert!(result.unwrap().is_none());
626    }
627
628    #[tokio::test]
629    async fn test_list_technical_inspections_by_building() {
630        let repo = MockTechnicalInspectionRepository::new();
631        let uc = make_use_cases(repo);
632        let org_id = Uuid::new_v4();
633        let building_a = Uuid::new_v4();
634        let building_b = Uuid::new_v4();
635
636        // Create 2 inspections for building A
637        let mut dto_a1 = valid_create_dto(org_id, building_a);
638        dto_a1.title = "Elevator inspection".to_string();
639        uc.create_technical_inspection(dto_a1).await.unwrap();
640
641        let mut dto_a2 = valid_create_dto(org_id, building_a);
642        dto_a2.title = "Boiler inspection".to_string();
643        dto_a2.inspection_type = InspectionType::Boiler;
644        uc.create_technical_inspection(dto_a2).await.unwrap();
645
646        // Create 1 inspection for building B
647        let dto_b = valid_create_dto(org_id, building_b);
648        uc.create_technical_inspection(dto_b).await.unwrap();
649
650        let result = uc.list_technical_inspections_by_building(building_a).await;
651        assert!(result.is_ok());
652        let inspections = result.unwrap();
653        assert_eq!(inspections.len(), 2);
654        assert!(inspections
655            .iter()
656            .all(|i| i.building_id == building_a.to_string()));
657    }
658
659    #[tokio::test]
660    async fn test_mark_as_completed() {
661        let repo = MockTechnicalInspectionRepository::new();
662        let uc = make_use_cases(repo);
663        let org_id = Uuid::new_v4();
664        let building_id = Uuid::new_v4();
665
666        let created = uc
667            .create_technical_inspection(valid_create_dto(org_id, building_id))
668            .await
669            .unwrap();
670        let inspection_id = Uuid::parse_str(&created.id).unwrap();
671        assert_eq!(created.status, InspectionStatus::Scheduled);
672
673        let result = uc.mark_as_completed(inspection_id).await;
674        assert!(result.is_ok());
675        let completed = result.unwrap();
676        assert_eq!(completed.status, InspectionStatus::Completed);
677    }
678
679    #[tokio::test]
680    async fn test_mark_as_completed_not_found() {
681        let repo = MockTechnicalInspectionRepository::new();
682        let uc = make_use_cases(repo);
683
684        let result = uc.mark_as_completed(Uuid::new_v4()).await;
685        assert!(result.is_err());
686        assert_eq!(result.unwrap_err(), "Technical inspection not found");
687    }
688
689    #[tokio::test]
690    async fn test_add_report() {
691        let repo = MockTechnicalInspectionRepository::new();
692        let uc = make_use_cases(repo);
693        let org_id = Uuid::new_v4();
694        let building_id = Uuid::new_v4();
695
696        let created = uc
697            .create_technical_inspection(valid_create_dto(org_id, building_id))
698            .await
699            .unwrap();
700        let inspection_id = Uuid::parse_str(&created.id).unwrap();
701        assert!(created.reports.is_empty());
702
703        let result = uc
704            .add_report(
705                inspection_id,
706                AddReportDto {
707                    report_path: "/uploads/reports/elevator-2026-03.pdf".to_string(),
708                },
709            )
710            .await;
711
712        assert!(result.is_ok());
713        let updated = result.unwrap();
714        assert_eq!(updated.reports.len(), 1);
715        assert_eq!(updated.reports[0], "/uploads/reports/elevator-2026-03.pdf");
716    }
717
718    #[tokio::test]
719    async fn test_add_report_not_found() {
720        let repo = MockTechnicalInspectionRepository::new();
721        let uc = make_use_cases(repo);
722
723        let result = uc
724            .add_report(
725                Uuid::new_v4(),
726                AddReportDto {
727                    report_path: "/uploads/reports/test.pdf".to_string(),
728                },
729            )
730            .await;
731
732        assert!(result.is_err());
733        assert_eq!(result.unwrap_err(), "Technical inspection not found");
734    }
735
736    #[tokio::test]
737    async fn test_add_photo() {
738        let repo = MockTechnicalInspectionRepository::new();
739        let uc = make_use_cases(repo);
740        let org_id = Uuid::new_v4();
741        let building_id = Uuid::new_v4();
742
743        let created = uc
744            .create_technical_inspection(valid_create_dto(org_id, building_id))
745            .await
746            .unwrap();
747        let inspection_id = Uuid::parse_str(&created.id).unwrap();
748
749        let result = uc
750            .add_photo(
751                inspection_id,
752                AddInspectionPhotoDto {
753                    photo_path: "/uploads/photos/elevator-panel.jpg".to_string(),
754                },
755            )
756            .await;
757
758        assert!(result.is_ok());
759        let updated = result.unwrap();
760        assert_eq!(updated.photos.len(), 1);
761        assert_eq!(updated.photos[0], "/uploads/photos/elevator-panel.jpg");
762    }
763
764    #[tokio::test]
765    async fn test_add_photo_not_found() {
766        let repo = MockTechnicalInspectionRepository::new();
767        let uc = make_use_cases(repo);
768
769        let result = uc
770            .add_photo(
771                Uuid::new_v4(),
772                AddInspectionPhotoDto {
773                    photo_path: "/uploads/photos/test.jpg".to_string(),
774                },
775            )
776            .await;
777
778        assert!(result.is_err());
779        assert_eq!(result.unwrap_err(), "Technical inspection not found");
780    }
781
782    #[tokio::test]
783    async fn test_delete_technical_inspection() {
784        let repo = MockTechnicalInspectionRepository::new();
785        let uc = make_use_cases(repo);
786        let org_id = Uuid::new_v4();
787        let building_id = Uuid::new_v4();
788
789        let created = uc
790            .create_technical_inspection(valid_create_dto(org_id, building_id))
791            .await
792            .unwrap();
793        let inspection_id = Uuid::parse_str(&created.id).unwrap();
794
795        // Delete should succeed
796        let result = uc.delete_technical_inspection(inspection_id).await;
797        assert!(result.is_ok());
798        assert!(result.unwrap());
799
800        // Should no longer be found
801        let get_result = uc.get_technical_inspection(inspection_id).await;
802        assert!(get_result.is_ok());
803        assert!(get_result.unwrap().is_none());
804    }
805
806    #[tokio::test]
807    async fn test_delete_technical_inspection_not_found() {
808        let repo = MockTechnicalInspectionRepository::new();
809        let uc = make_use_cases(repo);
810
811        let result = uc.delete_technical_inspection(Uuid::new_v4()).await;
812        assert!(result.is_ok());
813        assert!(!result.unwrap());
814    }
815
816    #[tokio::test]
817    async fn test_add_multiple_reports_and_photos() {
818        let repo = MockTechnicalInspectionRepository::new();
819        let uc = make_use_cases(repo);
820        let org_id = Uuid::new_v4();
821        let building_id = Uuid::new_v4();
822
823        let created = uc
824            .create_technical_inspection(valid_create_dto(org_id, building_id))
825            .await
826            .unwrap();
827        let inspection_id = Uuid::parse_str(&created.id).unwrap();
828
829        // Add 2 reports
830        uc.add_report(
831            inspection_id,
832            AddReportDto {
833                report_path: "/reports/report1.pdf".to_string(),
834            },
835        )
836        .await
837        .unwrap();
838        uc.add_report(
839            inspection_id,
840            AddReportDto {
841                report_path: "/reports/report2.pdf".to_string(),
842            },
843        )
844        .await
845        .unwrap();
846
847        // Add 1 photo
848        uc.add_photo(
849            inspection_id,
850            AddInspectionPhotoDto {
851                photo_path: "/photos/photo1.jpg".to_string(),
852            },
853        )
854        .await
855        .unwrap();
856
857        let inspection = uc
858            .get_technical_inspection(inspection_id)
859            .await
860            .unwrap()
861            .unwrap();
862        assert_eq!(inspection.reports.len(), 2);
863        assert_eq!(inspection.photos.len(), 1);
864    }
865}