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
84 .organization_id
85 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
86
87 let unit_id = uuid::Uuid::nil(); let list = state
91 .energy_bill_upload_use_cases
92 .get_my_uploads(unit_id)
93 .await
94 .map_err(actix_web::error::ErrorInternalServerError)?;
95
96 let response: Vec<EnergyBillUploadResponse> = list
97 .into_iter()
98 .map(EnergyBillUploadResponse::from)
99 .collect();
100
101 Ok(HttpResponse::Ok().json(response))
102}
103
104#[get("/energy-bills/{id}")]
107pub async fn get_upload(
108 state: web::Data<AppState>,
109 path: web::Path<Uuid>,
110 user: AuthenticatedUser,
111) -> Result<HttpResponse, actix_web::Error> {
112 let id = path.into_inner();
113
114 let upload = state
115 .energy_bill_upload_use_cases
116 .get_upload(id)
117 .await
118 .map_err(actix_web::error::ErrorInternalServerError)?
119 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
120
121 if upload.organization_id
123 != user
124 .organization_id
125 .ok_or_else(|| actix_web::error::ErrorForbidden("Organization ID required"))?
126 {
127 return Err(actix_web::error::ErrorForbidden("Access denied"));
128 }
129
130 Ok(HttpResponse::Ok().json(EnergyBillUploadResponse::from(upload)))
131}
132
133#[get("/energy-bills/{id}/decrypt")]
136pub async fn decrypt_consumption(
137 state: web::Data<AppState>,
138 path: web::Path<Uuid>,
139 user: AuthenticatedUser,
140) -> Result<HttpResponse, actix_web::Error> {
141 let upload_id = path.into_inner();
142
143 let encryption_key =
144 get_encryption_key().map_err(actix_web::error::ErrorInternalServerError)?;
145
146 let upload = state
148 .energy_bill_upload_use_cases
149 .get_upload(upload_id)
150 .await
151 .map_err(actix_web::error::ErrorInternalServerError)?
152 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
153
154 let user_org = user
156 .organization_id
157 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
158 if upload.organization_id != user_org {
159 return Err(actix_web::error::ErrorForbidden("Access denied"));
160 }
161
162 let total_kwh = state
163 .energy_bill_upload_use_cases
164 .decrypt_consumption(upload_id, upload.unit_id, &encryption_key)
165 .await
166 .map_err(actix_web::error::ErrorForbidden)?;
167
168 let response = DecryptedConsumptionResponse {
169 upload_id,
170 total_kwh,
171 energy_type: upload.energy_type,
172 bill_period_start: upload.bill_period_start,
173 bill_period_end: upload.bill_period_end,
174 };
175
176 Ok(HttpResponse::Ok().json(response))
177}
178
179#[put("/energy-bills/{id}/verify")]
182pub async fn verify_upload(
183 state: web::Data<AppState>,
184 path: web::Path<Uuid>,
185 _request: web::Json<VerifyUploadRequest>,
186 user: AuthenticatedUser,
187) -> Result<HttpResponse, actix_web::Error> {
188 let upload_id = path.into_inner();
189
190 let updated = state
196 .energy_bill_upload_use_cases
197 .verify_upload(upload_id, user.user_id)
198 .await
199 .map_err(actix_web::error::ErrorInternalServerError)?;
200
201 Ok(HttpResponse::Ok().json(EnergyBillUploadResponse::from(updated)))
202}
203
204#[delete("/energy-bills/{id}")]
207pub async fn delete_upload(
208 state: web::Data<AppState>,
209 path: web::Path<Uuid>,
210 user: AuthenticatedUser,
211) -> Result<HttpResponse, actix_web::Error> {
212 let upload_id = path.into_inner();
213
214 let upload = state
216 .energy_bill_upload_use_cases
217 .get_upload(upload_id)
218 .await
219 .map_err(actix_web::error::ErrorInternalServerError)?
220 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
221
222 let user_org = user
224 .organization_id
225 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
226 if upload.organization_id != user_org {
227 return Err(actix_web::error::ErrorForbidden("Access denied"));
228 }
229
230 state
231 .energy_bill_upload_use_cases
232 .delete_upload(upload_id, upload.unit_id)
233 .await
234 .map_err(actix_web::error::ErrorForbidden)?;
235
236 Ok(HttpResponse::NoContent().finish())
237}
238
239#[post("/energy-bills/{id}/withdraw-consent")]
242pub async fn withdraw_consent(
243 state: web::Data<AppState>,
244 path: web::Path<Uuid>,
245 user: AuthenticatedUser,
246) -> Result<HttpResponse, actix_web::Error> {
247 let upload_id = path.into_inner();
248
249 let upload = state
251 .energy_bill_upload_use_cases
252 .get_upload(upload_id)
253 .await
254 .map_err(actix_web::error::ErrorInternalServerError)?
255 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
256
257 let user_org = user
259 .organization_id
260 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
261 if upload.organization_id != user_org {
262 return Err(actix_web::error::ErrorForbidden("Access denied"));
263 }
264
265 state
266 .energy_bill_upload_use_cases
267 .withdraw_consent(upload_id, upload.unit_id)
268 .await
269 .map_err(actix_web::error::ErrorForbidden)?;
270
271 Ok(HttpResponse::Ok().json(serde_json::json!({
272 "message": "Consent withdrawn and data deleted",
273 "gdpr_article": "7.3 - Right to withdraw consent"
274 })))
275}
276
277#[get("/energy-campaigns/{campaign_id}/uploads")]
280pub async fn get_campaign_uploads(
281 state: web::Data<AppState>,
282 path: web::Path<Uuid>,
283 user: AuthenticatedUser,
284) -> Result<HttpResponse, actix_web::Error> {
285 let campaign_id = path.into_inner();
286
287 let list = state
288 .energy_bill_upload_use_cases
289 .get_uploads_by_campaign(campaign_id)
290 .await
291 .map_err(actix_web::error::ErrorInternalServerError)?;
292
293 if !list.is_empty() {
295 let user_org = user
296 .organization_id
297 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
298 if list[0].organization_id != user_org {
299 return Err(actix_web::error::ErrorForbidden("Access denied"));
300 }
301 }
302
303 let response: Vec<EnergyBillUploadResponse> = list
304 .into_iter()
305 .map(EnergyBillUploadResponse::from)
306 .collect();
307
308 Ok(HttpResponse::Ok().json(response))
309}