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::application::use_cases::EnergyBillUploadUseCases;
9use crate::domain::entities::EnergyBillUpload;
10use crate::infrastructure::web::middleware::AuthenticatedUser;
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 uploads: web::Data<EnergyBillUploadUseCases>,
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 = uploads
64 .upload_bill(upload)
65 .await
66 .map_err(actix_web::error::ErrorInternalServerError)?;
67
68 Ok(HttpResponse::Created().json(EnergyBillUploadResponse::from(created)))
71}
72
73#[get("/energy-bills/my-uploads")]
76pub async fn get_my_uploads(
77 uploads: web::Data<EnergyBillUploadUseCases>,
78 user: AuthenticatedUser,
79) -> Result<HttpResponse, actix_web::Error> {
80 let _organization_id = user
83 .organization_id
84 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
85
86 let unit_id = uuid::Uuid::nil(); let list = uploads
90 .get_my_uploads(unit_id)
91 .await
92 .map_err(actix_web::error::ErrorInternalServerError)?;
93
94 let response: Vec<EnergyBillUploadResponse> = list
95 .into_iter()
96 .map(EnergyBillUploadResponse::from)
97 .collect();
98
99 Ok(HttpResponse::Ok().json(response))
100}
101
102#[get("/energy-bills/{id}")]
105pub async fn get_upload(
106 uploads: web::Data<EnergyBillUploadUseCases>,
107 path: web::Path<Uuid>,
108 user: AuthenticatedUser,
109) -> Result<HttpResponse, actix_web::Error> {
110 let id = path.into_inner();
111
112 let upload = uploads
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 uploads: web::Data<EnergyBillUploadUseCases>,
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 = uploads
145 .get_upload(upload_id)
146 .await
147 .map_err(actix_web::error::ErrorInternalServerError)?
148 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
149
150 let user_org = user
152 .organization_id
153 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
154 if upload.organization_id != user_org {
155 return Err(actix_web::error::ErrorForbidden("Access denied"));
156 }
157
158 let total_kwh = uploads
159 .decrypt_consumption(upload_id, upload.unit_id, &encryption_key)
160 .await
161 .map_err(actix_web::error::ErrorForbidden)?;
162
163 let response = DecryptedConsumptionResponse {
164 upload_id,
165 total_kwh,
166 energy_type: upload.energy_type,
167 bill_period_start: upload.bill_period_start,
168 bill_period_end: upload.bill_period_end,
169 };
170
171 Ok(HttpResponse::Ok().json(response))
172}
173
174#[put("/energy-bills/{id}/verify")]
177pub async fn verify_upload(
178 uploads: web::Data<EnergyBillUploadUseCases>,
179 path: web::Path<Uuid>,
180 _request: web::Json<VerifyUploadRequest>,
181 user: AuthenticatedUser,
182) -> Result<HttpResponse, actix_web::Error> {
183 let upload_id = path.into_inner();
184
185 let updated = uploads
191 .verify_upload(upload_id, user.user_id)
192 .await
193 .map_err(actix_web::error::ErrorInternalServerError)?;
194
195 Ok(HttpResponse::Ok().json(EnergyBillUploadResponse::from(updated)))
196}
197
198#[delete("/energy-bills/{id}")]
201pub async fn delete_upload(
202 uploads: web::Data<EnergyBillUploadUseCases>,
203 path: web::Path<Uuid>,
204 user: AuthenticatedUser,
205) -> Result<HttpResponse, actix_web::Error> {
206 let upload_id = path.into_inner();
207
208 let upload = uploads
210 .get_upload(upload_id)
211 .await
212 .map_err(actix_web::error::ErrorInternalServerError)?
213 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
214
215 let user_org = user
217 .organization_id
218 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
219 if upload.organization_id != user_org {
220 return Err(actix_web::error::ErrorForbidden("Access denied"));
221 }
222
223 uploads
224 .delete_upload(upload_id, upload.unit_id)
225 .await
226 .map_err(actix_web::error::ErrorForbidden)?;
227
228 Ok(HttpResponse::NoContent().finish())
229}
230
231#[post("/energy-bills/{id}/withdraw-consent")]
234pub async fn withdraw_consent(
235 uploads: web::Data<EnergyBillUploadUseCases>,
236 path: web::Path<Uuid>,
237 user: AuthenticatedUser,
238) -> Result<HttpResponse, actix_web::Error> {
239 let upload_id = path.into_inner();
240
241 let upload = uploads
243 .get_upload(upload_id)
244 .await
245 .map_err(actix_web::error::ErrorInternalServerError)?
246 .ok_or_else(|| actix_web::error::ErrorNotFound("Upload not found"))?;
247
248 let user_org = user
250 .organization_id
251 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
252 if upload.organization_id != user_org {
253 return Err(actix_web::error::ErrorForbidden("Access denied"));
254 }
255
256 uploads
257 .withdraw_consent(upload_id, upload.unit_id)
258 .await
259 .map_err(actix_web::error::ErrorForbidden)?;
260
261 Ok(HttpResponse::Ok().json(serde_json::json!({
262 "message": "Consent withdrawn and data deleted",
263 "gdpr_article": "7.3 - Right to withdraw consent"
264 })))
265}
266
267#[get("/energy-campaigns/{campaign_id}/uploads")]
270pub async fn get_campaign_uploads(
271 uploads: web::Data<EnergyBillUploadUseCases>,
272 path: web::Path<Uuid>,
273 user: AuthenticatedUser,
274) -> Result<HttpResponse, actix_web::Error> {
275 let campaign_id = path.into_inner();
276
277 let list = uploads
278 .get_uploads_by_campaign(campaign_id)
279 .await
280 .map_err(actix_web::error::ErrorInternalServerError)?;
281
282 if !list.is_empty() {
284 let user_org = user
285 .organization_id
286 .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
287 if list[0].organization_id != user_org {
288 return Err(actix_web::error::ErrorForbidden("Access denied"));
289 }
290 }
291
292 let response: Vec<EnergyBillUploadResponse> = list
293 .into_iter()
294 .map(EnergyBillUploadResponse::from)
295 .collect();
296
297 Ok(HttpResponse::Ok().json(response))
298}