""" Capa de Datos — Supabase PostgreSQL con caché """ from datetime import datetime, timedelta from typing import Optional from app.db.database import get_db from app.domain.entities.ruta import ( Coordenada, EstadoCamion, ETAResult, NotificationPreferences, PuntoRuta, Ruta, TruckStatus, ) class SupabaseRutaRepository: def __init__(self): self.db = get_db() # ── Rutas y puntos ──────────────────────────────────────────────── def obtener_ruta(self, route_id: str) -> Optional[Ruta]: try: ruta_data = self.db.table("rutas").select("*").eq("id", route_id).execute() if not ruta_data.data: return None ruta_row = ruta_data.data[0] puntos_data = self.db.table("puntos_ruta").select("*").eq("ruta_id", route_id).order("orden").execute() puntos = [ PuntoRuta( orden=p["orden"], nombre=p["nombre"], coordenada=Coordenada(p["lat"], p["lng"]), tiempo_estimado_min=p["tiempo_estimado_min"], ) for p in puntos_data.data ] return Ruta( id=ruta_row["id"], nombre=ruta_row["nombre"], puntos=puntos, turno=ruta_row["turno"] ) except Exception as e: print(f"Error obtener_ruta: {e}") return None def obtener_ruta_por_address(self, address_id: int) -> Optional[Ruta]: try: addr_data = self.db.table("addresses").select("route_id").eq("id", address_id).execute() if not addr_data.data: return None return self.obtener_ruta(addr_data.data[0]["route_id"]) except Exception as e: print(f"Error obtener_ruta_por_address: {e}") return None # ── truck_status ────────────────────────────────────────── def obtener_truck_status(self, route_id: str) -> Optional[TruckStatus]: try: ts_data = self.db.table("truck_status").select("*").eq("route_id", route_id).execute() if not ts_data.data: return None row = ts_data.data[0] return TruckStatus( route_id=row["route_id"], current_position_id=row["current_position_id"], last_update=datetime.fromisoformat(row["last_update"].replace("Z", "+00:00")), status=EstadoCamion(row["status"]), ) except Exception as e: print(f"Error obtener_truck_status: {e}") return None def guardar_truck_status(self, ts: TruckStatus) -> None: try: self.db.table("truck_status").upsert({ "route_id": ts.route_id, "current_position_id": ts.current_position_id, "last_update": ts.last_update.isoformat(), "status": ts.status.value, }).execute() # Invalidar caché from app.core.cache import invalidate_route_cache import asyncio asyncio.create_task(invalidate_route_cache(ts.route_id)) except Exception as e: print(f"Error guardar_truck_status: {e}") # ── Preferencias de notificación ───────────────────────────────── def obtener_preferencias(self, user_id: str) -> NotificationPreferences: try: prefs_data = self.db.table("notification_preferences").select("*").eq("user_id", user_id).execute() if not prefs_data.data: return NotificationPreferences(user_id=user_id) row = prefs_data.data[0] return NotificationPreferences( user_id=user_id, notify_proximity=row["notify_proximity"], notify_breakdown=row["notify_breakdown"], notify_delay=row["notify_delay"], notify_route_start=row["notify_route_start"], ) except Exception as e: print(f"Error obtener_preferencias: {e}") return NotificationPreferences(user_id=user_id) def obtener_usuarios_por_ruta(self, route_id: str) -> list[dict]: try: users_data = self.db.table("addresses").select("id, user_id").eq("route_id", route_id).execute() return [{"address_id": u["id"], "user_id": u["user_id"]} for u in users_data.data] except Exception as e: print(f"Error obtener_usuarios_por_ruta: {e}") return [] # ── Templates de notificación ───────────────────────────────────── def obtener_template(self, trigger_event: str) -> Optional[dict]: try: template_data = self.db.table("notification_templates").select("*").eq("trigger_event", trigger_event).execute() if not template_data.data: return None return template_data.data[0] except Exception as e: print(f"Error obtener_template: {e}") return None # ── ETA calculado ──────────────────────────────────────────────── def calcular_eta(self, address_id: int) -> Optional[ETAResult]: ruta = self.obtener_ruta_por_address(address_id) if not ruta: return None ts = self.obtener_truck_status(ruta.id) if not ts: return ETAResult( address_id=address_id, route_id=ruta.id, status="SIN_INICIAR", eta_minutos=None, ventana_inicio=None, ventana_fin=None, mensaje="El camión aún no ha iniciado su ruta.", ) pos = ts.current_position_id puntos = {p.orden: p for p in ruta.puntos} ultimo = ruta.puntos[-1] actual = puntos.get(pos, ruta.puntos[0]) eta_min = max(0, ultimo.tiempo_estimado_min - actual.tiempo_estimado_min) ahora = datetime.now() llegada = ahora + timedelta(minutes=eta_min) v_ini = (llegada - timedelta(minutes=7)).strftime("%I:%M %p").lstrip("0") v_fin = (llegada + timedelta(minutes=7)).strftime("%I:%M %p").lstrip("0") if ts.status == EstadoCamion.APROXIMANDOSE: msg = f"El camión llegará a tu zona entre las {v_ini} y {v_fin}." elif ts.status in (EstadoCamion.AVERIADA, EstadoCamion.RETRASADA): msg = "El camión reportó una incidencia. Te notificaremos cuando se reanude." v_ini = v_fin = None else: msg = f"El camión está en camino. Llegada estimada: {v_ini} – {v_fin}." return ETAResult( address_id=address_id, route_id=ruta.id, status=ts.status.value, eta_minutos=eta_min, ventana_inicio=v_ini, ventana_fin=v_fin, mensaje=msg, ) def guardar_notificacion(self, tipo: str, route_id: str, address_id: int, mensaje: str, eta_minutos: Optional[int]) -> None: try: self.db.table("notificaciones").insert({ "tipo": tipo, "ruta_id": route_id, "address_id": address_id, "mensaje": mensaje, "eta_minutos": eta_minutos, "creada_en": datetime.utcnow().isoformat(), }).execute() except Exception as e: print(f"Error guardar_notificacion: {e}") # Alias para compatibilidad SQLiteRutaRepository = SupabaseRutaRepository