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 let updated = state
193 .energy_bill_upload_use_cases
194 .verify_upload(upload_id, user.user_id)
195 .await
196 .map_err(actix_web::error::ErrorInternalServerError)?;
197
198 Ok(HttpResponse::Ok().json(EnergyBillUploadResponse::from(updated)))
199}
200
201#[delete("/energy-bills/{id}")]
204pub async fn delete_upload(
205 state: web::Data<AppState>,
206 path: web::Path<Uuid>,
207 user: AuthenticatedUser,
208) -> Result<HttpResponse, actix_web::Error> {
209 let upload_id = path.into_inner();
210
211 let upload = state
213 .energy_bill_upload_use_cases
214 .get_upload(upload_id)
215 .await
216 .map_err(actix_web::error::ErrorInternalServerError)?
217 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
218
219 let user_org = user
221 .organization_id
222 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
223 if upload.organization_id != user_org {
224 return Err(actix_web::error::ErrorForbidden("Access denied"));
225 }
226
227 state
228 .energy_bill_upload_use_cases
229 .delete_upload(upload_id, upload.unit_id)
230 .await
231 .map_err(actix_web::error::ErrorForbidden)?;
232
233 Ok(HttpResponse::NoContent().finish())
234}
235
236#[post("/energy-bills/{id}/withdraw-consent")]
239pub async fn withdraw_consent(
240 state: web::Data<AppState>,
241 path: web::Path<Uuid>,
242 user: AuthenticatedUser,
243) -> Result<HttpResponse, actix_web::Error> {
244 let upload_id = path.into_inner();
245
246 let upload = state
248 .energy_bill_upload_use_cases
249 .get_upload(upload_id)
250 .await
251 .map_err(actix_web::error::ErrorInternalServerError)?
252 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
253
254 let user_org = user
256 .organization_id
257 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
258 if upload.organization_id != user_org {
259 return Err(actix_web::error::ErrorForbidden("Access denied"));
260 }
261
262 state
263 .energy_bill_upload_use_cases
264 .withdraw_consent(upload_id, upload.unit_id)
265 .await
266 .map_err(actix_web::error::ErrorForbidden)?;
267
268 Ok(HttpResponse::Ok().json(serde_json::json!({
269 "message": "Consent withdrawn and data deleted",
270 "gdpr_article": "7.3 - Right to withdraw consent"
271 })))
272}
273
274#[get("/energy-campaigns/{campaign_id}/uploads")]
277pub async fn get_campaign_uploads(
278 state: web::Data<AppState>,
279 path: web::Path<Uuid>,
280 user: AuthenticatedUser,
281) -> Result<HttpResponse, actix_web::Error> {
282 let campaign_id = path.into_inner();
283
284 let list = state
285 .energy_bill_upload_use_cases
286 .get_uploads_by_campaign(campaign_id)
287 .await
288 .map_err(actix_web::error::ErrorInternalServerError)?;
289
290 if !list.is_empty() {
292 let user_org = user
293 .organization_id
294 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
295 if list[0].organization_id != user_org {
296 return Err(actix_web::error::ErrorForbidden("Access denied"));
297 }
298 }
299
300 let response: Vec<EnergyBillUploadResponse> = list
301 .into_iter()
302 .map(EnergyBillUploadResponse::from)
303 .collect();
304
305 Ok(HttpResponse::Ok().json(response))
306}