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::application::use_cases::EnergyBillUploadUseCases;
9use crate::domain::entities::EnergyBillUpload;
10use crate::infrastructure::web::middleware::AuthenticatedUser;
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    uploads: web::Data<EnergyBillUploadUseCases>,
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 = uploads
64        .upload_bill(upload)
65        .await
66        .map_err(actix_web::error::ErrorInternalServerError)?;
67
68    // Note: Aggregation should be triggered asynchronously via message queue in production
69
70    Ok(HttpResponse::Created().json(EnergyBillUploadResponse::from(created)))
71}
72
73/// GET /api/v1/energy-bills/my-uploads
74/// Get my energy bill uploads
75#[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    // TODO: Get unit_id from unit_owners table based on user_id
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 for user's organization (filtered by repository)
87    let unit_id = uuid::Uuid::nil(); // Placeholder, should come from unit_owners table
88
89    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 /api/v1/energy-bills/{id}
103/// Get upload by ID
104#[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    // 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    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    // Get upload to check ownership
144    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    // Verify organization access
151    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 /api/v1/energy-bills/{id}/verify
175/// Verify upload (admin only)
176#[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    // TODO: Add admin role check
186    // if !user.is_admin() {
187    //     return Err(actix_web::error::ErrorForbidden("Admin access required"));
188    // }
189
190    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 /api/v1/energy-bills/{id}
199/// Delete upload (GDPR Art. 17 - Right to erasure)
200#[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    // Get upload to verify ownership
209    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    // Verify organization access
216    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 /api/v1/energy-bills/{id}/withdraw-consent
232/// Withdraw GDPR consent (Art. 7.3 - Immediate deletion)
233#[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    // Get upload to verify ownership
242    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    // Verify organization access
249    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 /api/v1/energy-campaigns/{campaign_id}/uploads
268/// Get all uploads for a campaign (admin)
269#[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    // Verify organization access
283    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}