From 219a615cd9e54e0bceca766b31a07a7d337f0d20 Mon Sep 17 00:00:00 2001 From: hack_23031391_8ff9d8 <23031391@itcelaya.edu.mx> Date: Sat, 23 May 2026 02:57:55 +0000 Subject: [PATCH] =?UTF-8?q?A=C3=B1adir=2001-backend=5Farquitectura.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01-backend_arquitectura.md.-.md | 336 ++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 01-backend_arquitectura.md.-.md diff --git a/01-backend_arquitectura.md.-.md b/01-backend_arquitectura.md.-.md new file mode 100644 index 0000000..d806735 --- /dev/null +++ b/01-backend_arquitectura.md.-.md @@ -0,0 +1,336 @@ +# Arquitectura del Backend (Módulo B) + +## Clean Architecture aplicada + +``` +basura_backend/ +├── app/ +│ ├── domain/ # ← Reglas de negocio puras +│ │ ├── entities/ +│ │ │ └── ruta.py # Ruta, PuntoRuta, TruckStatus +│ │ └── interfaces/ +│ │ └── i_ruta_repository.py +│ │ +│ ├── data/ # ← Implementaciones concretas +│ │ └── repositories/ +│ │ └── ruta_repository.py # SQLite queries +│ │ +│ ├── use_cases/ # ← Lógica de aplicación +│ │ └── obtener_eta.py # Calcula ETA desde truck_status +│ │ +│ ├── services/ # ← Servicios de infraestructura +│ │ ├── simulador.py # APScheduler + lógica de avance +│ │ └── ws_manager.py # Singleton WebSocket broadcast +│ │ +│ ├── api/routes/ # ← Endpoints HTTP +│ │ └── eta_router.py +│ │ +│ ├── db/ +│ │ └── database.py # Esquema SQLite + seed +│ │ +│ └── core/ +│ └── config.py # Variables de entorno +│ +└── requirements.txt +``` + +--- + +## Capa de Dominio + +### Entidades (`domain/entities/ruta.py`) + +```python +@dataclass +class PuntoRuta: + id: int + ruta_id: str + orden: int + nombre: str + lat: float + lng: float + tiempo_estimado_min: int + +@dataclass +class Ruta: + id: str + nombre: str + turno: str + puntos: List[PuntoRuta] + +@dataclass +class TruckStatus: + route_id: str + current_position_id: int + last_update: datetime + status: str # EN_RUTA | AVERIADA | RETRASADA +``` + +**Sin dependencias de frameworks** — solo Python estándar. + +### Interfaz del Repositorio + +```python +class IRutaRepository(ABC): + @abstractmethod + async def obtener_ruta(self, ruta_id: str) -> Optional[Ruta]: + pass + + @abstractmethod + async def obtener_truck_status(self, ruta_id: str) -> Optional[TruckStatus]: + pass +``` + +--- + +## Capa de Datos + +### Repositorio SQLite (`data/repositories/ruta_repository.py`) + +- Lee de `rutas`, `puntos_ruta`, `truck_status` +- Implementa `IRutaRepository` +- Retorna entidades del dominio (no diccionarios crudos) + +**Ejemplo:** +```python +async def obtener_ruta(self, ruta_id: str) -> Optional[Ruta]: + cursor.execute("SELECT * FROM rutas WHERE id = ?", (ruta_id,)) + row = cursor.fetchone() + # ... mapea a entidad Ruta + return Ruta(id=row[0], nombre=row[1], ...) +``` + +--- + +## Capa de Casos de Uso + +### `obtener_eta.py` + +Recibe `address_id`, calcula ETA combinando: +1. Posición actual del camión (`truck_status.current_position_id`) +2. Posición del domicilio del usuario (`addresses.route_id`) +3. Distancia entre puntos + +**Retorna:** +```python +{ + "eta_minutos": 22, + "ventana": {"inicio": "7:20 pm", "fin": "7:35 pm"}, + "mensaje": "El camión está en camino..." +} +``` + +--- + +## Servicios de Infraestructura + +### Simulador (`services/simulador.py`) + +**Responsabilidades:** +- Avanza `truck_status.current_position_id` cada tick (10s default) +- Calcula ETA para cada domicilio en la ruta +- Si ETA ≤ umbral (10 min) → broadcast WebSocket +- Respeta `notification_preferences` del usuario +- Usa `notification_templates` para formatear mensajes + +**Ciclo de vida:** +```python +# Arranque manual vía endpoint +POST /admin/route/RUTA-01/start + → APScheduler.add_job(tick_simulador, interval=10) + +# Cada tick +def tick_simulador(): + avanzar_camion() + para cada domicilio en ruta: + calcular_eta() + si eta <= umbral: + consultar_preferencias(user_id) + si preferencia.notify_proximity: + broadcast_ws(address_id, payload) +``` + +**Eventos soportados:** +- `ruta_iniciada` → broadcast al arrancar +- `aproximandose` → cuando ETA ≤ 10 min +- `falla_mecanica` → POST /alerts/breakdown +- `ruta_tarde` → POST /admin/route/{id}/delay + +### WebSocket Manager (`services/ws_manager.py`) + +```python +class WSManager: + _instance = None # Singleton + connections: Dict[str, List[WebSocket]] = {} + + async def connect(self, address_id: str, ws: WebSocket): + # Indexa por address_id como zona_key + + async def broadcast_zona(self, address_id: str, payload: dict): + # Envía a todos los clientes conectados a ese address_id +``` + +--- + +## Capa de API + +### Router (`api/routes/eta_router.py`) + +```python +@router.get("/eta/{address_id}") +async def get_eta(address_id: int): + # Llama al use case + result = await obtener_eta_use_case.execute(address_id) + return result + +@router.websocket("/ws/{address_id}") +async def websocket_endpoint(address_id: int, ws: WebSocket): + await ws_manager.connect(address_id, ws) + # Mantiene conexión abierta +``` + +--- + +## Base de Datos + +### Tablas clave del Módulo B + +```sql +-- Estado actual del camión +CREATE TABLE truck_status ( + route_id TEXT PRIMARY KEY, + current_position_id INTEGER, -- FK a puntos_ruta.id + last_update TEXT, + status TEXT -- EN_RUTA | AVERIADA | RETRASADA +); + +-- Definición de rutas +CREATE TABLE rutas ( + id TEXT PRIMARY KEY, -- ej: "RUTA-01" + nombre TEXT, + turno TEXT -- "matutino" | "nocturno" +); + +-- Waypoints de cada ruta +CREATE TABLE puntos_ruta ( + id INTEGER PRIMARY KEY, + ruta_id TEXT, + orden INTEGER, -- 1, 2, 3... + nombre TEXT, + lat REAL, + lng REAL, + tiempo_estimado_min INTEGER +); + +-- Log de notificaciones enviadas +CREATE TABLE notificaciones ( + id INTEGER PRIMARY KEY, + tipo TEXT, -- "aproximandose" | "falla_mecanica" + ruta_id TEXT, + address_id INTEGER, + mensaje TEXT, + eta_minutos INTEGER, + creada_en TEXT +); +``` + +### Tablas compartidas con Persona A + +```sql +-- Domicilios validados (Persona A los crea) +CREATE TABLE addresses ( + id INTEGER PRIMARY KEY, + user_id INTEGER, + alias TEXT, + lat REAL, + lng REAL, + route_id TEXT, -- Persona B lo usa para filtrar + created_at TEXT +); + +-- Preferencias de notificaciones (Persona A las gestiona) +CREATE TABLE notification_preferences ( + user_id INTEGER PRIMARY KEY, + notify_proximity BOOLEAN, + notify_breakdown BOOLEAN, + notify_delay BOOLEAN, + notify_route_start BOOLEAN +); +``` + +--- + +## Flujo completo: desde arranque hasta notificación + +``` +1. POST /admin/route/RUTA-01/start + └─> simulador.arrancar() + └─> APScheduler inicia ticks cada 10s + +2. Cada tick (10s) + └─> truck_status.current_position_id += 1 + └─> para cada address en RUTA-01: + └─> calcular ETA + └─> si ETA <= 10 min: + └─> consultar notification_preferences + └─> si notify_proximity == True: + └─> formatear mensaje con template + └─> ws_manager.broadcast_zona(address_id, payload) + +3. Flutter escucha en WS /ws/{address_id} + └─> recibe JSON {"tipo": "aproximandose", "eta_minutos": 8} + └─> muestra alerta push local +``` + +--- + +## Variables de entorno (`.env`) + +```bash +# Simulador +SIM_TICK_SECONDS=10 # Cada cuántos segundos avanza el camión +SIM_ETA_ALERT_MINUTES=10 # Umbral para notificar proximidad +SIM_SPEED_MULTIPLIER=1.0 # Acelerar simulación (2.0 = doble velocidad) + +# Base de datos +DATABASE_PATH=./basura.db # Ruta del archivo SQLite + +# Servidor +HOST=0.0.0.0 +PORT=8000 +RELOAD=True +``` + +--- + +## Testing local + +```bash +# 1. Arrancar servidor +uvicorn app.main:app --reload + +# 2. Verificar health +curl http://localhost:8000/health + +# 3. Arrancar simulador +curl -X POST http://localhost:8000/admin/route/RUTA-01/start + +# 4. Verificar estado del camión +curl http://localhost:8000/admin/route/RUTA-01/status + +# 5. Consultar ETA de un domicilio +curl http://localhost:8000/eta/1 + +# 6. Forzar avería (para probar notificaciones) +curl -X POST http://localhost:8000/alerts/breakdown \ + -H "Content-Type: application/json" \ + -d '{"route_id": "RUTA-01"}' +``` + +--- + +## Siguiente paso + +Lee cómo Flutter consume estos endpoints: +- [Contratos de API](../api/01-endpoints.md) +- [Integración WebSocket](../api/02-websocket.md)