koprogo_api/infrastructure/web/handlers/
energy_bill_upload_handlers.rs1use actix_web::{delete, get, post, put, web, HttpResponse};
2use uuid::Uuid;
3
4use crate::application::dto::{
5 DecryptedConsumptionResponse, EnergyBillUploadResponse, UploadEnergyBillRequest,
6 VerifyUploadRequest,
7};
8use crate::domain::entities::EnergyBillUpload;
9use crate::infrastructure::web::middleware::AuthenticatedUser;
10use crate::infrastructure::web::AppState;
11
12fn get_encryption_key() -> Result<[u8; 32], String> {
14 let key_hex = std::env::var("ENERGY_ENCRYPTION_MASTER_KEY")
15 .map_err(|_| "ENERGY_ENCRYPTION_MASTER_KEY not set".to_string())?;
16
17 if key_hex.len() != 64 {
18 return Err("Invalid encryption key length (expected 64 hex chars)".to_string());
19 }
20
21 let mut key = [0u8; 32];
22 hex::decode_to_slice(&key_hex, &mut key).map_err(|e| format!("Invalid hex key: {}", e))?;
23
24 Ok(key)
25}
26
27#[post("/energy-bills/upload")]
30pub async fn upload_bill(
31 state: web::Data<AppState>,
32 request: web::Json<UploadEnergyBillRequest>,
33 user: AuthenticatedUser,
34) -> Result<HttpResponse, actix_web::Error> {
35 if !request.consent.accepted {
37 return Err(actix_web::error::ErrorBadRequest("GDPR consent required"));
38 }
39
40 let encryption_key =
41 get_encryption_key().map_err(actix_web::error::ErrorInternalServerError)?;
42
43 let upload = EnergyBillUpload::new(
44 request.campaign_id,
45 request.unit_id,
46 request.building_id,
47 user.organization_id
48 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?,
49 request.bill_period_start,
50 request.bill_period_end,
51 request.total_kwh,
52 request.energy_type.clone(),
53 request.postal_code.clone(),
54 request.file_hash.clone(),
55 request.file_path.clone(), user.user_id,
57 request.consent.ip.clone(),
58 request.consent.user_agent.clone(),
59 &encryption_key,
60 )
61 .map_err(actix_web::error::ErrorBadRequest)?;
62
63 let created = state
64 .energy_bill_upload_use_cases
65 .upload_bill(upload)
66 .await
67 .map_err(actix_web::error::ErrorInternalServerError)?;
68
69 Ok(HttpResponse::Created().json(EnergyBillUploadResponse::from(created)))
72}
73
74#[get("/energy-bills/my-uploads")]
77pub async fn get_my_uploads(
78 state: web::Data<AppState>,
79 user: AuthenticatedUser,
80) -> Result<HttpResponse, actix_web::Error> {
81 let _organization_id = user
83 .organization_id
84 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
85
86 let list = state
88 .energy_bill_upload_use_cases
89 .get_my_uploads(user.user_id)
90 .await
91 .map_err(actix_web::error::ErrorInternalServerError)?;
92
93 let response: Vec<EnergyBillUploadResponse> = list
94 .into_iter()
95 .map(EnergyBillUploadResponse::from)
96 .collect();
97
98 Ok(HttpResponse::Ok().json(response))
99}
100
101#[get("/energy-bills/{id}")]
104pub async fn get_upload(
105 state: web::Data<AppState>,
106 path: web::Path<Uuid>,
107 user: AuthenticatedUser,
108) -> Result<HttpResponse, actix_web::Error> {
109 let id = path.into_inner();
110
111 let upload = state
112 .energy_bill_upload_use_cases
113 .get_upload(id)
114 .await
115 .map_err(actix_web::error::ErrorInternalServerError)?
116 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
117
118 if upload.organization_id
120 != user
121 .organization_id
122 .ok_or_else(|| actix_web::error::ErrorForbidden("Organization ID required"))?
123 {
124 return Err(actix_web::error::ErrorForbidden("Access denied"));
125 }
126
127 Ok(HttpResponse::Ok().json(EnergyBillUploadResponse::from(upload)))
128}
129
130#[get("/energy-bills/{id}/decrypt")]
133pub async fn decrypt_consumption(
134 state: web::Data<AppState>,
135 path: web::Path<Uuid>,
136 user: AuthenticatedUser,
137) -> Result<HttpResponse, actix_web::Error> {
138 let upload_id = path.into_inner();
139
140 let encryption_key =
141 get_encryption_key().map_err(actix_web::error::ErrorInternalServerError)?;
142
143 let upload = state
145 .energy_bill_upload_use_cases
146 .get_upload(upload_id)
147 .await
148 .map_err(actix_web::error::ErrorInternalServerError)?
149 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
150
151 let user_org = user
153 .organization_id
154 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
155 if upload.organization_id != user_org {
156 return Err(actix_web::error::ErrorForbidden("Access denied"));
157 }
158
159 let total_kwh = state
160 .energy_bill_upload_use_cases
161 .decrypt_consumption(upload_id, upload.unit_id, &encryption_key)
162 .await
163 .map_err(actix_web::error::ErrorForbidden)?;
164
165 let response = DecryptedConsumptionResponse {
166 upload_id,
167 total_kwh,
168 energy_type: upload.energy_type,
169 bill_period_start: upload.bill_period_start,
170 bill_period_end: upload.bill_period_end,
171 };
172
173 Ok(HttpResponse::Ok().json(response))
174}
175
176#[put("/energy-bills/{id}/verify")]
179pub async fn verify_upload(
180 state: web::Data<AppState>,
181 path: web::Path<Uuid>,
182 _request: web::Json<VerifyUploadRequest>,
183 user: AuthenticatedUser,
184) -> Result<HttpResponse, actix_web::Error> {
185 let upload_id = path.into_inner();
186
187 if !user.is_superadmin() && user.role != "syndic" {
200 return Err(actix_web::error::ErrorForbidden(
201 "Réservé au syndic et à l'administration de la plateforme",
202 ));
203 }
204
205 let upload = state
206 .energy_bill_upload_use_cases
207 .get_upload(upload_id)
208 .await
209 .map_err(actix_web::error::ErrorInternalServerError)?
210 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
211
212 if user.verify_org_access(upload.organization_id).is_err() {
213 return Err(actix_web::error::ErrorForbidden(
214 "Cette facture relève d'une autre organisation",
215 ));
216 }
217
218 let updated = state
219 .energy_bill_upload_use_cases
220 .verify_upload(upload_id, user.user_id)
221 .await
222 .map_err(actix_web::error::ErrorInternalServerError)?;
223
224 Ok(HttpResponse::Ok().json(EnergyBillUploadResponse::from(updated)))
225}
226
227#[delete("/energy-bills/{id}")]
230pub async fn delete_upload(
231 state: web::Data<AppState>,
232 path: web::Path<Uuid>,
233 user: AuthenticatedUser,
234) -> Result<HttpResponse, actix_web::Error> {
235 let upload_id = path.into_inner();
236
237 let upload = state
239 .energy_bill_upload_use_cases
240 .get_upload(upload_id)
241 .await
242 .map_err(actix_web::error::ErrorInternalServerError)?
243 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
244
245 let user_org = user
247 .organization_id
248 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
249 if upload.organization_id != user_org {
250 return Err(actix_web::error::ErrorForbidden("Access denied"));
251 }
252
253 state
254 .energy_bill_upload_use_cases
255 .delete_upload(upload_id, upload.unit_id)
256 .await
257 .map_err(actix_web::error::ErrorForbidden)?;
258
259 Ok(HttpResponse::NoContent().finish())
260}
261
262#[post("/energy-bills/{id}/withdraw-consent")]
265pub async fn withdraw_consent(
266 state: web::Data<AppState>,
267 path: web::Path<Uuid>,
268 user: AuthenticatedUser,
269) -> Result<HttpResponse, actix_web::Error> {
270 let upload_id = path.into_inner();
271
272 let upload = state
274 .energy_bill_upload_use_cases
275 .get_upload(upload_id)
276 .await
277 .map_err(actix_web::error::ErrorInternalServerError)?
278 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
279
280 let user_org = user
282 .organization_id
283 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
284 if upload.organization_id != user_org {
285 return Err(actix_web::error::ErrorForbidden("Access denied"));
286 }
287
288 state
289 .energy_bill_upload_use_cases
290 .withdraw_consent(upload_id, upload.unit_id)
291 .await
292 .map_err(actix_web::error::ErrorForbidden)?;
293
294 Ok(HttpResponse::Ok().json(serde_json::json!({
295 "message": "Consent withdrawn and data deleted",
296 "gdpr_article": "7.3 - Right to withdraw consent"
297 })))
298}
299
300#[get("/energy-campaigns/{campaign_id}/uploads")]
303pub async fn get_campaign_uploads(
304 state: web::Data<AppState>,
305 path: web::Path<Uuid>,
306 user: AuthenticatedUser,
307) -> Result<HttpResponse, actix_web::Error> {
308 let campaign_id = path.into_inner();
309
310 let list = state
311 .energy_bill_upload_use_cases
312 .get_uploads_by_campaign(campaign_id)
313 .await
314 .map_err(actix_web::error::ErrorInternalServerError)?;
315
316 if !list.is_empty() {
318 let user_org = user
319 .organization_id
320 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
321 if list[0].organization_id != user_org {
322 return Err(actix_web::error::ErrorForbidden("Access denied"));
323 }
324 }
325
326 let response: Vec<EnergyBillUploadResponse> = list
327 .into_iter()
328 .map(EnergyBillUploadResponse::from)
329 .collect();
330
331 Ok(HttpResponse::Ok().json(response))
332}