1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use super::energy_campaign::EnergyType;
6
7#[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 pub bill_period_start: DateTime<Utc>,
18 pub bill_period_end: DateTime<Utc>,
19 pub total_kwh_encrypted: Vec<u8>, pub energy_type: EnergyType,
21 pub provider: Option<String>,
22 pub postal_code: String, pub file_hash: String, pub file_path_encrypted: String, pub ocr_confidence: f64, pub manually_verified: bool,
29
30 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 pub consent_timestamp: DateTime<Utc>,
38 pub consent_ip: String,
39 pub consent_user_agent: String,
40 pub consent_signature_hash: String,
41
42 pub anonymized: bool,
44 pub retention_until: DateTime<Utc>, pub deleted_at: Option<DateTime<Utc>>,
46
47 pub created_at: DateTime<Utc>,
48 pub updated_at: DateTime<Utc>,
49}
50
51impl EnergyBillUpload {
52 #[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 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 let total_kwh_encrypted = Self::encrypt_kwh(total_kwh, encryption_key)?;
86
87 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 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 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 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 let mut result = nonce_bytes.to_vec();
147 result.extend(ciphertext);
148 Ok(result)
149 }
150
151 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 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 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 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 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 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 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 pub fn should_auto_delete(&self) -> bool {
238 self.deleted_at.is_none() && self.retention_until < Utc::now()
239 }
240
241 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 self.delete()
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 fn get_test_encryption_key() -> [u8; 32] {
257 *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(), "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, 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 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 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 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 bill.ocr_confidence = 90.0;
453 assert!(bill.anonymize().is_err());
454
455 bill.mark_verified(Uuid::new_v4()).unwrap();
457
458 assert!(bill.anonymize().is_ok());
460 assert!(bill.anonymized);
461
462 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 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 assert!(bill.delete().is_err());
521
522 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 assert!(!bill.should_auto_delete());
552
553 bill.retention_until = Utc::now() - chrono::Duration::days(1);
555 assert!(bill.should_auto_delete());
556
557 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}