Añadir 01-backend_arquitectura.md
336
01-backend_arquitectura.md.-.md
Normal file
336
01-backend_arquitectura.md.-.md
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user