koprogo_api/infrastructure/web/handlers/
iot_handlers.rs1use crate::application::dto::{
2 ConfigureLinkyDeviceDto, CreateIoTReadingDto, QueryIoTReadingsDto, SyncLinkyDataDto,
3};
4use crate::application::use_cases::{IoTUseCases, LinkyUseCases};
5use crate::domain::entities::{DeviceType, MetricType};
6use crate::infrastructure::web::middleware::AuthenticatedUser;
7use actix_web::{error::ErrorBadRequest, web, HttpResponse, Result};
8use chrono::{DateTime, Utc};
9use std::sync::Arc;
10use uuid::Uuid;
11
12pub async fn create_iot_reading(
34 auth: AuthenticatedUser,
35 dto: web::Json<CreateIoTReadingDto>,
36 iot_use_cases: web::Data<Arc<IoTUseCases>>,
37) -> Result<HttpResponse> {
38 let organization_id = auth
39 .organization_id
40 .ok_or_else(|| ErrorBadRequest("Organization ID is required"))?;
41
42 match iot_use_cases
43 .create_reading(dto.into_inner(), auth.user_id, organization_id)
44 .await
45 {
46 Ok(reading) => Ok(HttpResponse::Created().json(reading)),
47 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
48 "error": e
49 }))),
50 }
51}
52
53pub async fn create_iot_readings_bulk(
74 auth: AuthenticatedUser,
75 dtos: web::Json<Vec<CreateIoTReadingDto>>,
76 iot_use_cases: web::Data<Arc<IoTUseCases>>,
77) -> Result<HttpResponse> {
78 let organization_id = auth
79 .organization_id
80 .ok_or_else(|| ErrorBadRequest("Organization ID is required"))?;
81
82 match iot_use_cases
83 .create_readings_bulk(dtos.into_inner(), auth.user_id, organization_id)
84 .await
85 {
86 Ok(count) => Ok(HttpResponse::Created().json(serde_json::json!({
87 "count": count,
88 "message": format!("{} IoT readings created", count)
89 }))),
90 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
91 "error": e
92 }))),
93 }
94}
95
96pub async fn query_iot_readings(
100 auth: AuthenticatedUser,
101 query: web::Query<QueryIoTReadingsDto>,
102 iot_use_cases: web::Data<Arc<IoTUseCases>>,
103) -> Result<HttpResponse> {
104 let _ = auth; match iot_use_cases.query_readings(query.into_inner()).await {
106 Ok(readings) => Ok(HttpResponse::Ok().json(readings)),
107 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
108 "error": e
109 }))),
110 }
111}
112
113pub async fn get_consumption_stats(
117 auth: AuthenticatedUser,
118 path: web::Path<Uuid>,
119 query: web::Query<serde_json::Value>,
120 iot_use_cases: web::Data<Arc<IoTUseCases>>,
121) -> Result<HttpResponse> {
122 let _ = auth; let building_id = path.into_inner();
124
125 let metric_type_str = query
126 .get("metric_type")
127 .and_then(|v| v.as_str())
128 .ok_or_else(|| ErrorBadRequest("metric_type query param required"))?;
129 let metric_type: MetricType = metric_type_str
130 .parse()
131 .map_err(|_| ErrorBadRequest("Invalid metric_type"))?;
132
133 let start_date_str = query
134 .get("start_date")
135 .and_then(|v| v.as_str())
136 .ok_or_else(|| ErrorBadRequest("start_date query param required"))?;
137 let start_date: DateTime<Utc> = start_date_str
138 .parse()
139 .map_err(|_| ErrorBadRequest("Invalid start_date format (use ISO 8601)"))?;
140
141 let end_date_str = query
142 .get("end_date")
143 .and_then(|v| v.as_str())
144 .ok_or_else(|| ErrorBadRequest("end_date query param required"))?;
145 let end_date: DateTime<Utc> = end_date_str
146 .parse()
147 .map_err(|_| ErrorBadRequest("Invalid end_date format (use ISO 8601)"))?;
148
149 match iot_use_cases
150 .get_consumption_stats(building_id, metric_type, start_date, end_date)
151 .await
152 {
153 Ok(stats) => Ok(HttpResponse::Ok().json(stats)),
154 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
155 "error": e
156 }))),
157 }
158}
159
160pub async fn get_daily_aggregates(
164 auth: AuthenticatedUser,
165 path: web::Path<Uuid>,
166 query: web::Query<serde_json::Value>,
167 iot_use_cases: web::Data<Arc<IoTUseCases>>,
168) -> Result<HttpResponse> {
169 let _ = auth; let building_id = path.into_inner();
171
172 let device_type_str = query
173 .get("device_type")
174 .and_then(|v| v.as_str())
175 .ok_or_else(|| ErrorBadRequest("device_type query param required"))?;
176 let device_type: DeviceType = device_type_str
177 .parse()
178 .map_err(|_| ErrorBadRequest("Invalid device_type"))?;
179
180 let metric_type_str = query
181 .get("metric_type")
182 .and_then(|v| v.as_str())
183 .ok_or_else(|| ErrorBadRequest("metric_type query param required"))?;
184 let metric_type: MetricType = metric_type_str
185 .parse()
186 .map_err(|_| ErrorBadRequest("Invalid metric_type"))?;
187
188 let start_date_str = query
189 .get("start_date")
190 .and_then(|v| v.as_str())
191 .ok_or_else(|| ErrorBadRequest("start_date query param required"))?;
192 let start_date: DateTime<Utc> = start_date_str
193 .parse()
194 .map_err(|_| ErrorBadRequest("Invalid start_date format (use ISO 8601)"))?;
195
196 let end_date_str = query
197 .get("end_date")
198 .and_then(|v| v.as_str())
199 .ok_or_else(|| ErrorBadRequest("end_date query param required"))?;
200 let end_date: DateTime<Utc> = end_date_str
201 .parse()
202 .map_err(|_| ErrorBadRequest("Invalid end_date format (use ISO 8601)"))?;
203
204 match iot_use_cases
205 .get_daily_aggregates(building_id, device_type, metric_type, start_date, end_date)
206 .await
207 {
208 Ok(aggregates) => Ok(HttpResponse::Ok().json(aggregates)),
209 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
210 "error": e
211 }))),
212 }
213}
214
215pub async fn get_monthly_aggregates(
219 auth: AuthenticatedUser,
220 path: web::Path<Uuid>,
221 query: web::Query<serde_json::Value>,
222 iot_use_cases: web::Data<Arc<IoTUseCases>>,
223) -> Result<HttpResponse> {
224 let _ = auth; let building_id = path.into_inner();
226
227 let device_type_str = query
228 .get("device_type")
229 .and_then(|v| v.as_str())
230 .ok_or_else(|| ErrorBadRequest("device_type query param required"))?;
231 let device_type: DeviceType = device_type_str
232 .parse()
233 .map_err(|_| ErrorBadRequest("Invalid device_type"))?;
234
235 let metric_type_str = query
236 .get("metric_type")
237 .and_then(|v| v.as_str())
238 .ok_or_else(|| ErrorBadRequest("metric_type query param required"))?;
239 let metric_type: MetricType = metric_type_str
240 .parse()
241 .map_err(|_| ErrorBadRequest("Invalid metric_type"))?;
242
243 let start_date_str = query
244 .get("start_date")
245 .and_then(|v| v.as_str())
246 .ok_or_else(|| ErrorBadRequest("start_date query param required"))?;
247 let start_date: DateTime<Utc> = start_date_str
248 .parse()
249 .map_err(|_| ErrorBadRequest("Invalid start_date format (use ISO 8601)"))?;
250
251 let end_date_str = query
252 .get("end_date")
253 .and_then(|v| v.as_str())
254 .ok_or_else(|| ErrorBadRequest("end_date query param required"))?;
255 let end_date: DateTime<Utc> = end_date_str
256 .parse()
257 .map_err(|_| ErrorBadRequest("Invalid end_date format (use ISO 8601)"))?;
258
259 match iot_use_cases
260 .get_monthly_aggregates(building_id, device_type, metric_type, start_date, end_date)
261 .await
262 {
263 Ok(aggregates) => Ok(HttpResponse::Ok().json(aggregates)),
264 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
265 "error": e
266 }))),
267 }
268}
269
270pub async fn detect_anomalies(
274 auth: AuthenticatedUser,
275 path: web::Path<Uuid>,
276 query: web::Query<serde_json::Value>,
277 iot_use_cases: web::Data<Arc<IoTUseCases>>,
278) -> Result<HttpResponse> {
279 let _ = auth; let building_id = path.into_inner();
281
282 let metric_type_str = query
283 .get("metric_type")
284 .and_then(|v| v.as_str())
285 .ok_or_else(|| ErrorBadRequest("metric_type query param required"))?;
286 let metric_type: MetricType = metric_type_str
287 .parse()
288 .map_err(|_| ErrorBadRequest("Invalid metric_type"))?;
289
290 let threshold_percentage = query
291 .get("threshold_percentage")
292 .and_then(|v| v.as_f64())
293 .unwrap_or(30.0);
294
295 let lookback_days = query
296 .get("lookback_days")
297 .and_then(|v| v.as_i64())
298 .unwrap_or(30);
299
300 match iot_use_cases
301 .detect_anomalies(
302 building_id,
303 metric_type,
304 threshold_percentage,
305 lookback_days,
306 )
307 .await
308 {
309 Ok(anomalies) => Ok(HttpResponse::Ok().json(anomalies)),
310 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
311 "error": e
312 }))),
313 }
314}
315
316pub async fn configure_linky_device(
334 auth: AuthenticatedUser,
335 dto: web::Json<ConfigureLinkyDeviceDto>,
336 linky_use_cases: web::Data<Arc<LinkyUseCases>>,
337) -> Result<HttpResponse> {
338 let organization_id = auth
339 .organization_id
340 .ok_or_else(|| ErrorBadRequest("Organization ID is required"))?;
341
342 match linky_use_cases
343 .configure_linky_device(dto.into_inner(), auth.user_id, organization_id)
344 .await
345 {
346 Ok(device) => Ok(HttpResponse::Created().json(device)),
347 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
348 "error": e
349 }))),
350 }
351}
352
353pub async fn get_linky_device(
357 auth: AuthenticatedUser,
358 path: web::Path<Uuid>,
359 linky_use_cases: web::Data<Arc<LinkyUseCases>>,
360) -> Result<HttpResponse> {
361 let _ = auth; let building_id = path.into_inner();
363
364 match linky_use_cases.get_linky_device(building_id).await {
365 Ok(device) => Ok(HttpResponse::Ok().json(device)),
366 Err(e) => Ok(HttpResponse::NotFound().json(serde_json::json!({
367 "error": e
368 }))),
369 }
370}
371
372pub async fn delete_linky_device(
376 auth: AuthenticatedUser,
377 path: web::Path<Uuid>,
378 linky_use_cases: web::Data<Arc<LinkyUseCases>>,
379) -> Result<HttpResponse> {
380 let building_id = path.into_inner();
381 let organization_id = auth
382 .organization_id
383 .ok_or_else(|| ErrorBadRequest("Organization ID is required"))?;
384
385 match linky_use_cases
386 .delete_linky_device(building_id, auth.user_id, organization_id)
387 .await
388 {
389 Ok(_) => Ok(HttpResponse::NoContent().finish()),
390 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
391 "error": e
392 }))),
393 }
394}
395
396pub async fn sync_linky_data(
409 auth: AuthenticatedUser,
410 path: web::Path<Uuid>,
411 dto: web::Json<SyncLinkyDataDto>,
412 linky_use_cases: web::Data<Arc<LinkyUseCases>>,
413) -> Result<HttpResponse> {
414 let _building_id = path.into_inner();
415 let organization_id = auth
416 .organization_id
417 .ok_or_else(|| ErrorBadRequest("Organization ID is required"))?;
418
419 match linky_use_cases
420 .sync_linky_data(dto.into_inner(), auth.user_id, organization_id)
421 .await
422 {
423 Ok(sync_result) => Ok(HttpResponse::Ok().json(sync_result)),
424 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
425 "error": e
426 }))),
427 }
428}
429
430pub async fn toggle_linky_sync(
441 auth: AuthenticatedUser,
442 path: web::Path<Uuid>,
443 body: web::Json<serde_json::Value>,
444 linky_use_cases: web::Data<Arc<LinkyUseCases>>,
445) -> Result<HttpResponse> {
446 let building_id = path.into_inner();
447 let enabled = body
448 .get("enabled")
449 .and_then(|v| v.as_bool())
450 .ok_or_else(|| ErrorBadRequest("enabled field required (boolean)"))?;
451 let organization_id = auth
452 .organization_id
453 .ok_or_else(|| ErrorBadRequest("Organization ID is required"))?;
454
455 match linky_use_cases
456 .toggle_sync(building_id, enabled, auth.user_id, organization_id)
457 .await
458 {
459 Ok(device) => Ok(HttpResponse::Ok().json(device)),
460 Err(e) => Ok(HttpResponse::BadRequest().json(serde_json::json!({
461 "error": e
462 }))),
463 }
464}
465
466pub async fn find_devices_needing_sync(
470 auth: AuthenticatedUser,
471 linky_use_cases: web::Data<Arc<LinkyUseCases>>,
472) -> Result<HttpResponse> {
473 let _ = auth; match linky_use_cases.find_devices_needing_sync().await {
476 Ok(devices) => Ok(HttpResponse::Ok().json(devices)),
477 Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
478 "error": e
479 }))),
480 }
481}
482
483pub async fn find_devices_with_expired_tokens(
487 auth: AuthenticatedUser,
488 linky_use_cases: web::Data<Arc<LinkyUseCases>>,
489) -> Result<HttpResponse> {
490 let _ = auth; match linky_use_cases.find_devices_with_expired_tokens().await {
493 Ok(devices) => Ok(HttpResponse::Ok().json(devices)),
494 Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
495 "error": e
496 }))),
497 }
498}
499
500pub fn configure_iot_routes(cfg: &mut web::ServiceConfig) {
502 cfg.service(
503 web::scope("/iot")
504 .route("/readings", web::post().to(create_iot_reading))
506 .route("/readings/bulk", web::post().to(create_iot_readings_bulk))
507 .route("/readings", web::get().to(query_iot_readings))
508 .route(
509 "/buildings/{building_id}/consumption/stats",
510 web::get().to(get_consumption_stats),
511 )
512 .route(
513 "/buildings/{building_id}/consumption/daily",
514 web::get().to(get_daily_aggregates),
515 )
516 .route(
517 "/buildings/{building_id}/consumption/monthly",
518 web::get().to(get_monthly_aggregates),
519 )
520 .route(
521 "/buildings/{building_id}/consumption/anomalies",
522 web::get().to(detect_anomalies),
523 )
524 .route("/linky/devices", web::post().to(configure_linky_device))
526 .route(
527 "/linky/buildings/{building_id}/device",
528 web::get().to(get_linky_device),
529 )
530 .route(
531 "/linky/buildings/{building_id}/device",
532 web::delete().to(delete_linky_device),
533 )
534 .route(
535 "/linky/buildings/{building_id}/sync",
536 web::post().to(sync_linky_data),
537 )
538 .route(
539 "/linky/buildings/{building_id}/sync/toggle",
540 web::put().to(toggle_linky_sync),
541 )
542 .route(
543 "/linky/devices/needing-sync",
544 web::get().to(find_devices_needing_sync),
545 )
546 .route(
547 "/linky/devices/expired-tokens",
548 web::get().to(find_devices_with_expired_tokens),
549 ),
550 );
551}