koprogo_api/infrastructure/web/handlers/
energy_bill_upload_handlers.rs

1use 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
12// Helper: Get encryption key from environment
13fn 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 /api/v1/energy-bills/upload
28/// Upload energy bill with GDPR consent
29#[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    // Verify GDPR consent
36    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(), // Will be encrypted
56        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    // Note: Aggregation should be triggered asynchronously via message queue in production
70
71    Ok(HttpResponse::Created().json(EnergyBillUploadResponse::from(created)))
72}
73
74/// GET /api/v1/energy-bills/my-uploads
75/// Get my energy bill uploads
76#[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    // Verify user has organization
82    let _organization_id = user
83        .organization_id
84        .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
85
86    // Get uploads by the authenticated user
87    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 /api/v1/energy-bills/{id}
102/// Get upload by ID
103#[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    // Verify access (owner or same organization)
119    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 /api/v1/energy-bills/{id}/decrypt
131/// Decrypt consumption data (owner only)
132#[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    // Get upload to check ownership
144    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    // Verify organization access
152    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 /api/v1/energy-bills/{id}/verify
177/// Verify upload (admin only)
178#[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    // TODO: Add admin role check
188    // if !user.is_admin() {
189    //     return Err(actix_web::error::ErrorForbidden("Admin access required"));
190    // }
191
192    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 /api/v1/energy-bills/{id}
202/// Delete upload (GDPR Art. 17 - Right to erasure)
203#[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    // Get upload to verify ownership
212    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    // Verify organization access
220    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 /api/v1/energy-bills/{id}/withdraw-consent
237/// Withdraw GDPR consent (Art. 7.3 - Immediate deletion)
238#[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    // Get upload to verify ownership
247    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    // Verify organization access
255    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 /api/v1/energy-campaigns/{campaign_id}/uploads
275/// Get all uploads for a campaign (admin)
276#[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    // Verify organization access
291    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}