koprogo_api/domain/entities/
energy_bill_upload.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use super::energy_campaign::EnergyType;
6
7/// Upload de facture énergie (données chiffrées)
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9pub struct EnergyBillUpload {
10    pub id: Uuid,
11    pub campaign_id: Uuid,
12    pub unit_id: Uuid,
13    pub building_id: Uuid,
14    pub organization_id: Uuid,
15
16    // Données extraites (chiffrées)
17    pub bill_period_start: DateTime<Utc>,
18    pub bill_period_end: DateTime<Utc>,
19    pub total_kwh_encrypted: Vec<u8>, // AES-256-GCM
20    pub energy_type: EnergyType,
21    pub provider: Option<String>,
22    pub postal_code: String, // Pour tarifs régionaux CREG
23
24    // Authentification facture
25    pub file_hash: String,           // SHA-256
26    pub file_path_encrypted: String, // S3 path chiffré
27    pub ocr_confidence: f64,         // 0-100
28    pub manually_verified: bool,
29
30    // Upload metadata
31    pub uploaded_by: Uuid,
32    pub uploaded_at: DateTime<Utc>,
33    pub verified_at: Option<DateTime<Utc>>,
34    pub verified_by: Option<Uuid>,
35
36    // GDPR Consent
37    pub consent_timestamp: DateTime<Utc>,
38    pub consent_ip: String,
39    pub consent_user_agent: String,
40    pub consent_signature_hash: String,
41
42    // Privacy & Retention
43    pub anonymized: bool,
44    pub retention_until: DateTime<Utc>, // Auto-delete
45    pub deleted_at: Option<DateTime<Utc>>,
46
47    pub created_at: DateTime<Utc>,
48    pub updated_at: DateTime<Utc>,
49}
50
51impl EnergyBillUpload {
52    /// Créer nouvel upload avec consentement
53    #[allow(clippy::too_many_arguments)]
54    pub fn new(
55        campaign_id: Uuid,
56        unit_id: Uuid,
57        building_id: Uuid,
58        organization_id: Uuid,
59        bill_period_start: DateTime<Utc>,
60        bill_period_end: DateTime<Utc>,
61        total_kwh: f64,
62        energy_type: EnergyType,
63        postal_code: String,
64        file_hash: String,
65        file_path_encrypted: String,
66        uploaded_by: Uuid,
67        consent_ip: String,
68        consent_user_agent: String,
69        encryption_key: &[u8; 32],
70    ) -> Result<Self, String> {
71        // Validation
72        if total_kwh <= 0.0 {
73            return Err("Consumption must be positive".to_string());
74        }
75
76        if postal_code.len() != 4 {
77            return Err("Invalid Belgian postal code".to_string());
78        }
79
80        if bill_period_start >= bill_period_end {
81            return Err("Bill period start must be before end".to_string());
82        }
83
84        // Chiffrement AES-256-GCM de la consommation
85        let total_kwh_encrypted = Self::encrypt_kwh(total_kwh, encryption_key)?;
86
87        // Signature consentement
88        let consent_data = format!("{}|{}|{}|{}", unit_id, total_kwh, consent_ip, Utc::now());
89        let consent_signature_hash = format!("{:x}", md5::compute(consent_data.as_bytes()));
90
91        // Rétention: 90 jours après fin campagne (GDPR)
92        let retention_until = Utc::now() + chrono::Duration::days(90);
93
94        Ok(Self {
95            id: Uuid::new_v4(),
96            campaign_id,
97            unit_id,
98            building_id,
99            organization_id,
100            bill_period_start,
101            bill_period_end,
102            total_kwh_encrypted,
103            energy_type,
104            provider: None,
105            postal_code,
106            file_hash,
107            file_path_encrypted,
108            ocr_confidence: 0.0,
109            manually_verified: false,
110            uploaded_by,
111            uploaded_at: Utc::now(),
112            verified_at: None,
113            verified_by: None,
114            consent_timestamp: Utc::now(),
115            consent_ip,
116            consent_user_agent,
117            consent_signature_hash,
118            anonymized: false,
119            retention_until,
120            deleted_at: None,
121            created_at: Utc::now(),
122            updated_at: Utc::now(),
123        })
124    }
125
126    /// Chiffrer consommation kWh avec AES-256-GCM
127    fn encrypt_kwh(kwh: f64, key: &[u8; 32]) -> Result<Vec<u8>, String> {
128        use aes_gcm::{
129            aead::{Aead, KeyInit},
130            Aes256Gcm, Nonce,
131        };
132
133        let cipher = Aes256Gcm::new(key.into());
134
135        // Générer nonce aléatoire (12 bytes pour GCM)
136        let nonce_bytes = Self::generate_nonce();
137        let nonce = Nonce::from(nonce_bytes);
138
139        let plaintext = kwh.to_string().as_bytes().to_vec();
140
141        let ciphertext = cipher
142            .encrypt(&nonce, plaintext.as_ref())
143            .map_err(|e| format!("Encryption failed: {}", e))?;
144
145        // Préfixer le ciphertext avec le nonce pour pouvoir déchiffrer plus tard
146        let mut result = nonce_bytes.to_vec();
147        result.extend(ciphertext);
148        Ok(result)
149    }
150
151    /// Déchiffrer consommation kWh (requiert consentement actif)
152    pub fn decrypt_kwh(&self, key: &[u8; 32]) -> Result<f64, String> {
153        use aes_gcm::{
154            aead::{Aead, KeyInit},
155            Aes256Gcm, Nonce,
156        };
157
158        if self.deleted_at.is_some() {
159            return Err("Bill has been deleted (GDPR)".to_string());
160        }
161
162        if self.total_kwh_encrypted.len() < 12 {
163            return Err("Invalid encrypted data".to_string());
164        }
165
166        let cipher = Aes256Gcm::new(key.into());
167
168        // Extraire nonce (12 premiers bytes)
169        let nonce_array: [u8; 12] = self.total_kwh_encrypted[..12]
170            .try_into()
171            .map_err(|_| "Invalid nonce length".to_string())?;
172        let nonce = Nonce::from(nonce_array);
173
174        // Extraire ciphertext (reste des bytes)
175        let ciphertext = &self.total_kwh_encrypted[12..];
176
177        let plaintext = cipher
178            .decrypt(&nonce, ciphertext)
179            .map_err(|e| format!("Decryption failed: {}", e))?;
180
181        let kwh_str = String::from_utf8(plaintext).map_err(|e| format!("UTF-8 error: {}", e))?;
182
183        kwh_str
184            .parse::<f64>()
185            .map_err(|e| format!("Parse error: {}", e))
186    }
187
188    /// Générer nonce aléatoire de 12 bytes
189    fn generate_nonce() -> [u8; 12] {
190        use rand::Rng;
191        let mut rng = rand::rng();
192        let mut nonce = [0u8; 12];
193        rng.fill(&mut nonce);
194        nonce
195    }
196
197    /// Marquer comme vérifié (après OCR ou validation manuelle)
198    pub fn mark_verified(&mut self, verified_by: Uuid) -> Result<(), String> {
199        if self.verified_at.is_some() {
200            return Err("Already verified".to_string());
201        }
202
203        self.manually_verified = true;
204        self.verified_at = Some(Utc::now());
205        self.verified_by = Some(verified_by);
206        self.updated_at = Utc::now();
207        Ok(())
208    }
209
210    /// Anonymiser (agréger au building)
211    pub fn anonymize(&mut self) -> Result<(), String> {
212        if self.anonymized {
213            return Err("Already anonymized".to_string());
214        }
215
216        if !self.manually_verified && self.ocr_confidence < 95.0 {
217            return Err("Must be verified before anonymization".to_string());
218        }
219
220        self.anonymized = true;
221        self.updated_at = Utc::now();
222        Ok(())
223    }
224
225    /// Supprimer données (GDPR Art. 17 - Droit à l'effacement)
226    pub fn delete(&mut self) -> Result<(), String> {
227        if self.deleted_at.is_some() {
228            return Err("Already deleted".to_string());
229        }
230
231        self.deleted_at = Some(Utc::now());
232        self.updated_at = Utc::now();
233        Ok(())
234    }
235
236    /// Vérifier si peut être supprimé automatiquement (rétention expirée)
237    pub fn should_auto_delete(&self) -> bool {
238        self.deleted_at.is_none() && self.retention_until < Utc::now()
239    }
240
241    /// Retirer consentement (GDPR Art. 7.3)
242    pub fn withdraw_consent(&mut self) -> Result<(), String> {
243        if self.deleted_at.is_some() {
244            return Err("Already deleted".to_string());
245        }
246
247        // Supprimer immédiatement si consentement retiré
248        self.delete()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    fn get_test_encryption_key() -> [u8; 32] {
257        // 32 bytes exactly for AES-256
258        *b"test_master_key_for_32bytes!##!!"
259    }
260
261    #[test]
262    fn test_create_bill_upload_success() {
263        let key = get_test_encryption_key();
264        let bill = EnergyBillUpload::new(
265            Uuid::new_v4(),
266            Uuid::new_v4(),
267            Uuid::new_v4(),
268            Uuid::new_v4(),
269            Utc::now() - chrono::Duration::days(365),
270            Utc::now(),
271            2400.0,
272            EnergyType::Electricity,
273            "1050".to_string(),
274            "abc123".to_string(),
275            "/encrypted/path".to_string(),
276            Uuid::new_v4(),
277            "192.168.1.1".to_string(),
278            "Mozilla/5.0".to_string(),
279            &key,
280        );
281
282        assert!(bill.is_ok());
283        let bill = bill.unwrap();
284        assert_eq!(bill.energy_type, EnergyType::Electricity);
285        assert!(!bill.anonymized);
286        assert!(!bill.manually_verified);
287    }
288
289    #[test]
290    fn test_create_bill_upload_invalid_postal_code() {
291        let key = get_test_encryption_key();
292        let result = EnergyBillUpload::new(
293            Uuid::new_v4(),
294            Uuid::new_v4(),
295            Uuid::new_v4(),
296            Uuid::new_v4(),
297            Utc::now() - chrono::Duration::days(365),
298            Utc::now(),
299            2400.0,
300            EnergyType::Electricity,
301            "123".to_string(), // Invalid: 3 digits instead of 4
302            "abc123".to_string(),
303            "/encrypted/path".to_string(),
304            Uuid::new_v4(),
305            "192.168.1.1".to_string(),
306            "Mozilla/5.0".to_string(),
307            &key,
308        );
309
310        assert!(result.is_err());
311        assert_eq!(result.unwrap_err(), "Invalid Belgian postal code");
312    }
313
314    #[test]
315    fn test_create_bill_upload_negative_consumption() {
316        let key = get_test_encryption_key();
317        let result = EnergyBillUpload::new(
318            Uuid::new_v4(),
319            Uuid::new_v4(),
320            Uuid::new_v4(),
321            Uuid::new_v4(),
322            Utc::now() - chrono::Duration::days(365),
323            Utc::now(),
324            -100.0, // Invalid: negative
325            EnergyType::Electricity,
326            "1050".to_string(),
327            "abc123".to_string(),
328            "/encrypted/path".to_string(),
329            Uuid::new_v4(),
330            "192.168.1.1".to_string(),
331            "Mozilla/5.0".to_string(),
332            &key,
333        );
334
335        assert!(result.is_err());
336        assert_eq!(result.unwrap_err(), "Consumption must be positive");
337    }
338
339    #[test]
340    fn test_encrypt_decrypt_kwh() {
341        let key = get_test_encryption_key();
342        let original_kwh = 2400.5;
343
344        let bill = EnergyBillUpload::new(
345            Uuid::new_v4(),
346            Uuid::new_v4(),
347            Uuid::new_v4(),
348            Uuid::new_v4(),
349            Utc::now() - chrono::Duration::days(365),
350            Utc::now(),
351            original_kwh,
352            EnergyType::Electricity,
353            "1050".to_string(),
354            "abc123".to_string(),
355            "/encrypted/path".to_string(),
356            Uuid::new_v4(),
357            "192.168.1.1".to_string(),
358            "Mozilla/5.0".to_string(),
359            &key,
360        )
361        .unwrap();
362
363        // Déchiffrer
364        let decrypted = bill.decrypt_kwh(&key);
365        assert!(decrypted.is_ok());
366        assert_eq!(decrypted.unwrap(), original_kwh);
367    }
368
369    #[test]
370    fn test_decrypt_with_wrong_key() {
371        let key = get_test_encryption_key();
372        let wrong_key = *b"wrong_key_for_decryption_test!#!";
373
374        let bill = EnergyBillUpload::new(
375            Uuid::new_v4(),
376            Uuid::new_v4(),
377            Uuid::new_v4(),
378            Uuid::new_v4(),
379            Utc::now() - chrono::Duration::days(365),
380            Utc::now(),
381            2400.0,
382            EnergyType::Electricity,
383            "1050".to_string(),
384            "abc123".to_string(),
385            "/encrypted/path".to_string(),
386            Uuid::new_v4(),
387            "192.168.1.1".to_string(),
388            "Mozilla/5.0".to_string(),
389            &key,
390        )
391        .unwrap();
392
393        // Déchiffrer avec mauvaise clé
394        let result = bill.decrypt_kwh(&wrong_key);
395        assert!(result.is_err());
396    }
397
398    #[test]
399    fn test_mark_verified() {
400        let key = get_test_encryption_key();
401        let mut bill = EnergyBillUpload::new(
402            Uuid::new_v4(),
403            Uuid::new_v4(),
404            Uuid::new_v4(),
405            Uuid::new_v4(),
406            Utc::now() - chrono::Duration::days(365),
407            Utc::now(),
408            2400.0,
409            EnergyType::Electricity,
410            "1050".to_string(),
411            "abc123".to_string(),
412            "/encrypted/path".to_string(),
413            Uuid::new_v4(),
414            "192.168.1.1".to_string(),
415            "Mozilla/5.0".to_string(),
416            &key,
417        )
418        .unwrap();
419
420        let verifier_id = Uuid::new_v4();
421        assert!(bill.mark_verified(verifier_id).is_ok());
422        assert!(bill.manually_verified);
423        assert_eq!(bill.verified_by, Some(verifier_id));
424
425        // Cannot verify twice
426        assert!(bill.mark_verified(verifier_id).is_err());
427    }
428
429    #[test]
430    fn test_anonymize() {
431        let key = get_test_encryption_key();
432        let mut bill = EnergyBillUpload::new(
433            Uuid::new_v4(),
434            Uuid::new_v4(),
435            Uuid::new_v4(),
436            Uuid::new_v4(),
437            Utc::now() - chrono::Duration::days(365),
438            Utc::now(),
439            2400.0,
440            EnergyType::Electricity,
441            "1050".to_string(),
442            "abc123".to_string(),
443            "/encrypted/path".to_string(),
444            Uuid::new_v4(),
445            "192.168.1.1".to_string(),
446            "Mozilla/5.0".to_string(),
447            &key,
448        )
449        .unwrap();
450
451        // Cannot anonymize without verification
452        bill.ocr_confidence = 90.0;
453        assert!(bill.anonymize().is_err());
454
455        // Mark as verified
456        bill.mark_verified(Uuid::new_v4()).unwrap();
457
458        // Now can anonymize
459        assert!(bill.anonymize().is_ok());
460        assert!(bill.anonymized);
461
462        // Cannot anonymize twice
463        assert!(bill.anonymize().is_err());
464    }
465
466    #[test]
467    fn test_anonymize_high_ocr_confidence() {
468        let key = get_test_encryption_key();
469        let mut bill = EnergyBillUpload::new(
470            Uuid::new_v4(),
471            Uuid::new_v4(),
472            Uuid::new_v4(),
473            Uuid::new_v4(),
474            Utc::now() - chrono::Duration::days(365),
475            Utc::now(),
476            2400.0,
477            EnergyType::Electricity,
478            "1050".to_string(),
479            "abc123".to_string(),
480            "/encrypted/path".to_string(),
481            Uuid::new_v4(),
482            "192.168.1.1".to_string(),
483            "Mozilla/5.0".to_string(),
484            &key,
485        )
486        .unwrap();
487
488        // High OCR confidence (≥95%) allows anonymization without manual verification
489        bill.ocr_confidence = 98.0;
490        assert!(bill.anonymize().is_ok());
491        assert!(bill.anonymized);
492    }
493
494    #[test]
495    fn test_delete() {
496        let key = get_test_encryption_key();
497        let mut bill = EnergyBillUpload::new(
498            Uuid::new_v4(),
499            Uuid::new_v4(),
500            Uuid::new_v4(),
501            Uuid::new_v4(),
502            Utc::now() - chrono::Duration::days(365),
503            Utc::now(),
504            2400.0,
505            EnergyType::Electricity,
506            "1050".to_string(),
507            "abc123".to_string(),
508            "/encrypted/path".to_string(),
509            Uuid::new_v4(),
510            "192.168.1.1".to_string(),
511            "Mozilla/5.0".to_string(),
512            &key,
513        )
514        .unwrap();
515
516        assert!(bill.delete().is_ok());
517        assert!(bill.deleted_at.is_some());
518
519        // Cannot delete twice
520        assert!(bill.delete().is_err());
521
522        // Cannot decrypt deleted bill
523        let result = bill.decrypt_kwh(&key);
524        assert!(result.is_err());
525        assert_eq!(result.unwrap_err(), "Bill has been deleted (GDPR)");
526    }
527
528    #[test]
529    fn test_should_auto_delete() {
530        let key = get_test_encryption_key();
531        let mut bill = EnergyBillUpload::new(
532            Uuid::new_v4(),
533            Uuid::new_v4(),
534            Uuid::new_v4(),
535            Uuid::new_v4(),
536            Utc::now() - chrono::Duration::days(365),
537            Utc::now(),
538            2400.0,
539            EnergyType::Electricity,
540            "1050".to_string(),
541            "abc123".to_string(),
542            "/encrypted/path".to_string(),
543            Uuid::new_v4(),
544            "192.168.1.1".to_string(),
545            "Mozilla/5.0".to_string(),
546            &key,
547        )
548        .unwrap();
549
550        // Fresh bill should not be auto-deleted
551        assert!(!bill.should_auto_delete());
552
553        // Set retention in the past
554        bill.retention_until = Utc::now() - chrono::Duration::days(1);
555        assert!(bill.should_auto_delete());
556
557        // Already deleted bill should not be auto-deleted again
558        bill.delete().unwrap();
559        assert!(!bill.should_auto_delete());
560    }
561
562    #[test]
563    fn test_withdraw_consent() {
564        let key = get_test_encryption_key();
565        let mut bill = EnergyBillUpload::new(
566            Uuid::new_v4(),
567            Uuid::new_v4(),
568            Uuid::new_v4(),
569            Uuid::new_v4(),
570            Utc::now() - chrono::Duration::days(365),
571            Utc::now(),
572            2400.0,
573            EnergyType::Electricity,
574            "1050".to_string(),
575            "abc123".to_string(),
576            "/encrypted/path".to_string(),
577            Uuid::new_v4(),
578            "192.168.1.1".to_string(),
579            "Mozilla/5.0".to_string(),
580            &key,
581        )
582        .unwrap();
583
584        assert!(bill.withdraw_consent().is_ok());
585        assert!(bill.deleted_at.is_some());
586    }
587}