koprogo_api/domain/entities/
technical_inspection.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Technical Inspection - Inspection technique obligatoire
6///
7/// Tracks mandatory technical inspections for building equipment and systems.
8/// Belgian law requires regular inspections for safety-critical equipment.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct TechnicalInspection {
11    pub id: Uuid,
12    pub organization_id: Uuid,
13    pub building_id: Uuid,
14
15    // Inspection details
16    pub inspection_type: InspectionType,
17    pub title: String,
18    pub description: Option<String>,
19
20    // Inspector info
21    pub inspector_name: String,
22    pub inspector_company: Option<String>,
23    pub inspector_certification: Option<String>, // Certification number
24
25    // Dates
26    pub inspection_date: DateTime<Utc>,
27    pub next_due_date: DateTime<Utc>, // When next inspection is due
28
29    // Results
30    pub status: InspectionStatus,
31    pub result_summary: Option<String>,
32    pub defects_found: Option<String>,
33    pub recommendations: Option<String>,
34
35    // Compliance
36    pub compliant: Option<bool>,
37    pub compliance_certificate_number: Option<String>,
38    pub compliance_valid_until: Option<DateTime<Utc>>,
39
40    // Financial
41    pub cost: Option<f64>,
42    pub invoice_number: Option<String>,
43
44    // Documentation (JSON arrays of file paths)
45    pub reports: Vec<String>,
46    pub photos: Vec<String>,
47    pub certificates: Vec<String>,
48    pub notes: Option<String>,
49
50    // Metadata
51    pub created_at: DateTime<Utc>,
52    pub updated_at: DateTime<Utc>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56#[serde(rename_all = "snake_case")]
57pub enum InspectionType {
58    Elevator,               // Ascenseur (annuel)
59    Boiler,                 // Chaudière (annuel)
60    Electrical,             // Installation électrique (5 ans)
61    FireExtinguisher,       // Extincteurs (annuel)
62    FireAlarm,              // Système d'alarme incendie (annuel)
63    GasInstallation,        // Installation gaz (annuel)
64    RoofStructure,          // Structure toiture (5 ans)
65    Facade,                 // Façade (quinquennal)
66    WaterQuality,           // Qualité eau (annuel)
67    Other { name: String }, // Autre type d'inspection
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71#[serde(rename_all = "snake_case")]
72pub enum InspectionStatus {
73    Scheduled,  // Planifiée
74    InProgress, // En cours
75    Completed,  // Terminée
76    Failed,     // Échec (non conforme)
77    Overdue,    // En retard
78    Cancelled,  // Annulée
79}
80
81impl InspectionType {
82    /// Get the required inspection frequency in days
83    pub fn frequency_days(&self) -> i64 {
84        match self {
85            InspectionType::Elevator => 365,          // Annual
86            InspectionType::Boiler => 365,            // Annual
87            InspectionType::Electrical => 365 * 5,    // Every 5 years
88            InspectionType::FireExtinguisher => 365,  // Annual
89            InspectionType::FireAlarm => 365,         // Annual
90            InspectionType::GasInstallation => 365,   // Annual
91            InspectionType::RoofStructure => 365 * 5, // Every 5 years
92            InspectionType::Facade => 365 * 5,        // Every 5 years
93            InspectionType::WaterQuality => 365,      // Annual
94            InspectionType::Other { .. } => 365,      // Default annual
95        }
96    }
97
98    /// Get human-readable name
99    pub fn display_name(&self) -> String {
100        match self {
101            InspectionType::Elevator => "Ascenseur".to_string(),
102            InspectionType::Boiler => "Chaudière".to_string(),
103            InspectionType::Electrical => "Installation électrique".to_string(),
104            InspectionType::FireExtinguisher => "Extincteurs".to_string(),
105            InspectionType::FireAlarm => "Alarme incendie".to_string(),
106            InspectionType::GasInstallation => "Installation gaz".to_string(),
107            InspectionType::RoofStructure => "Structure toiture".to_string(),
108            InspectionType::Facade => "Façade".to_string(),
109            InspectionType::WaterQuality => "Qualité de l'eau".to_string(),
110            InspectionType::Other { name } => name.clone(),
111        }
112    }
113}
114
115impl TechnicalInspection {
116    #[allow(clippy::too_many_arguments)]
117    pub fn new(
118        organization_id: Uuid,
119        building_id: Uuid,
120        title: String,
121        description: Option<String>,
122        inspection_type: InspectionType,
123        inspector_name: String,
124        inspection_date: DateTime<Utc>,
125    ) -> Self {
126        let now = Utc::now();
127
128        // Calculate next due date based on inspection type
129        let next_due_date =
130            inspection_date + chrono::Duration::days(inspection_type.frequency_days());
131
132        Self {
133            id: Uuid::new_v4(),
134            organization_id,
135            building_id,
136            inspection_type,
137            title,
138            description,
139            inspector_name,
140            inspector_company: None,
141            inspector_certification: None,
142            inspection_date,
143            next_due_date,
144            status: InspectionStatus::Scheduled,
145            result_summary: None,
146            defects_found: None,
147            recommendations: None,
148            compliant: None,
149            compliance_certificate_number: None,
150            compliance_valid_until: None,
151            cost: None,
152            invoice_number: None,
153            reports: Vec::new(),
154            photos: Vec::new(),
155            certificates: Vec::new(),
156            notes: None,
157            created_at: now,
158            updated_at: now,
159        }
160    }
161
162    /// Calculate next due date based on inspection type
163    pub fn calculate_next_due_date(&self) -> DateTime<Utc> {
164        self.inspection_date + chrono::Duration::days(self.inspection_type.frequency_days())
165    }
166
167    /// Check if inspection is overdue
168    pub fn is_overdue(&self) -> bool {
169        Utc::now() > self.next_due_date
170    }
171
172    /// Get days until next inspection is due (negative if overdue)
173    pub fn days_until_due(&self) -> i64 {
174        (self.next_due_date - Utc::now()).num_days()
175    }
176
177    /// Mark as overdue
178    pub fn mark_overdue(&mut self) {
179        if self.is_overdue() && self.status == InspectionStatus::Scheduled {
180            self.status = InspectionStatus::Overdue;
181            self.updated_at = Utc::now();
182        }
183    }
184
185    /// Add report to inspection
186    pub fn add_report(&mut self, report_path: String) {
187        self.reports.push(report_path);
188        self.updated_at = Utc::now();
189    }
190
191    /// Add photo to inspection
192    pub fn add_photo(&mut self, photo_path: String) {
193        self.photos.push(photo_path);
194        self.updated_at = Utc::now();
195    }
196
197    /// Add certificate to inspection
198    pub fn add_certificate(&mut self, certificate_path: String) {
199        self.certificates.push(certificate_path);
200        self.updated_at = Utc::now();
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_inspection_creation() {
210        let inspection = TechnicalInspection::new(
211            Uuid::new_v4(),
212            Uuid::new_v4(),
213            "Inspection annuelle ascenseur".to_string(),
214            Some("Vérification complète".to_string()),
215            InspectionType::Elevator,
216            "Schindler Belgium".to_string(),
217            Utc::now(),
218        );
219
220        assert_eq!(inspection.title, "Inspection annuelle ascenseur");
221        assert_eq!(inspection.status, InspectionStatus::Scheduled);
222        assert!(!inspection.is_overdue());
223    }
224
225    #[test]
226    fn test_inspection_frequencies() {
227        assert_eq!(InspectionType::Elevator.frequency_days(), 365);
228        assert_eq!(InspectionType::Electrical.frequency_days(), 365 * 5);
229        assert_eq!(InspectionType::Facade.frequency_days(), 365 * 5);
230    }
231
232    #[test]
233    fn test_inspection_completion() {
234        let mut inspection = TechnicalInspection::new(
235            Uuid::new_v4(),
236            Uuid::new_v4(),
237            "Inspection chaudière".to_string(),
238            None,
239            InspectionType::Boiler,
240            "Test Inspector".to_string(),
241            Utc::now(),
242        );
243
244        inspection.status = InspectionStatus::Completed;
245        inspection.compliant = Some(true);
246        assert_eq!(inspection.status, InspectionStatus::Completed);
247        assert_eq!(inspection.compliant, Some(true));
248    }
249
250    #[test]
251    fn test_overdue_detection() {
252        let past_date = Utc::now() - chrono::Duration::days(400); // Over a year ago
253        let mut inspection = TechnicalInspection::new(
254            Uuid::new_v4(),
255            Uuid::new_v4(),
256            "Test".to_string(),
257            None,
258            InspectionType::FireExtinguisher,
259            "Test".to_string(),
260            past_date,
261        );
262
263        assert!(inspection.is_overdue());
264        assert!(inspection.days_until_due() < 0);
265
266        inspection.mark_overdue();
267        assert_eq!(inspection.status, InspectionStatus::Overdue);
268    }
269}