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    // TODO: Get unit_id from unit_owners table based on user_id
82    // Verify user has organization
83    let _organization_id = user
84        .organization_id
85        .ok_or_else(|| actix_web::error::ErrorUnauthorized("Organization required"))?;
86
87    // Get uploads for user's organization (filtered by repository)
88    let unit_id = uuid::Uuid::nil(); // Placeholder, should come from unit_owners table
89
90    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 /api/v1/energy-bills/{id}
105/// Get upload by ID
106#[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    // Verify access (owner or same organization)
122    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 /api/v1/energy-bills/{id}/decrypt
134/// Decrypt consumption data (owner only)
135#[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    // Get upload to check ownership
147    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    // Verify organization access
155    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 /api/v1/energy-bills/{id}/verify
180/// Verify upload (admin only)
181#[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    // TODO: Add admin role check
191    // if !user.is_admin() {
192    //     return Err(actix_web::error::ErrorForbidden("Admin access required"));
193    // }
194
195    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 /api/v1/energy-bills/{id}
205/// Delete upload (GDPR Art. 17 - Right to erasure)
206#[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    // Get upload to verify ownership
215    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    // Verify organization access
223    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 /api/v1/energy-bills/{id}/withdraw-consent
240/// Withdraw GDPR consent (Art. 7.3 - Immediate deletion)
241#[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    // Get upload to verify ownership
250    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    // Verify organization access
258    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 /api/v1/energy-campaigns/{campaign_id}/uploads
278/// Get all uploads for a campaign (admin)
279#[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    // Verify organization access
294    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}