Files
marianesaldana 80dbd947e5 Initial commit
2026-05-23 08:59:34 -06:00

319 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Arquitectura técnica
> Documento técnico para evaluación. Decisiones de diseño, algoritmos clave y consideraciones de privacidad/seguridad.
---
## 1. Visión general
**Mi Ruta Limpia** es una arquitectura **clienteservidor REST** con simulación de eventos en el backend para emular telemetría de la flotilla, sin requerir GPS real.
```
[App móvil RN] ↔ HTTPS/REST + JWT ↔ [FastAPI] ↔ SQLite + routes.json (mock)
```
### Por qué esta arquitectura
| Decisión | Razón |
|----------|-------|
| **FastAPI** (Python) | Validación automática con Pydantic, OpenAPI docs gratis, async-ready, productivo en hackathon |
| **SQLite** (no PostgreSQL) | Zero-config, suficiente para hackathon, migración a PG es trivial (cambiar `DATABASE_URL`) |
| **JSON mock para rutas** | Cumple el requisito de "simular eventos sin telemetría real" sin complejidad innecesaria |
| **JWT** stateless | No requiere sesión en servidor, escalable horizontalmente |
| **React Native + Expo** | Multiplataforma (iOS, Android, web) con un solo código |
| **fetch sobre axios** | Axios rompe en RN nueva arquitectura por `URL.protocol` strict mode |
---
## 2. Backend — Estructura por capas
```
app/
├── config.py ← Settings con pydantic-settings (env vars)
├── database.py ← SQLAlchemy engine + sessionmaker
├── models/ ← ORM (capa de datos)
├── schemas/ ← Pydantic (capa de validación / serialización)
├── routers/ ← Endpoints HTTP (capa de presentación)
└── services/ ← Lógica de negocio (capa de dominio)
```
**Patrón:** los routers solo orquestan. Toda la lógica real vive en `services/`. Los modelos no contienen lógica.
### Inyección de dependencias (FastAPI `Depends`)
```python
# Cada endpoint declara qué necesita:
@router.get("/eta/address/{id}")
def get_eta(
id: int,
db: Session = Depends(get_db), BD sesión
user = Depends(get_current_user), JWT validado
):
...
```
Esto permite testear cada endpoint con mocks fácilmente.
---
## 3. Modelo de datos
```
users (id, email, phone, full_name, hashed_password, role, oauth_*, push_token, is_active)
├──< addresses (id, user_id, label, street, colony, lat, lng, route_id, is_default)
│ │
│ ├──< reports (id, user_id, address_id, folio, report_type, description, status, created_at)
│ │
│ └──< service_ratings (id, user_id, address_id, rating, comment, created_at)
└──< operational_reports (id, employee_id, folio, category, severity, route_id, truck_id, status, ...)
trucks (id, unit_number, plate, model, year, status, route_id, base, odometer_km, fuel_level_pct, ...)
```
### Roles (RBAC)
- **`CIUDADANO`** — default. Solo accede a `/auth`, `/addresses` (suyos), `/eta` (suyos), `/reports` (suyos)
- **`EMPLEADO`** — accede a `/staff/*` (su propio dashboard, sus reportes operativos). NO accede a `/admin/*`
- **`ADMIN`** — accede a todo, incluyendo `/admin/*` (gestión de reportes ciudadanos y usuarios)
### Migraciones
Por simplicidad de hackathon, las migraciones se hacen con `Base.metadata.create_all()` + `ALTER TABLE` inline en `seed.py`. En producción se debe usar **Alembic**.
---
## 4. Algoritmo ETA — núcleo del sistema
### Problema
Dado un domicilio (lat, lng) y la hora actual, calcular cuándo pasará el camión **sin exponer su ubicación real**.
### Datos de entrada
[`backend/app/data/routes.json`](../backend/app/data/routes.json) — 15 rutas, cada una con 8 waypoints, cada waypoint con:
- `lat`, `lng`
- `timestamp` (template, se reinterpreta cada día)
- `speed`
### Algoritmo (`app/services/eta_service.py`)
```python
def get_eta(route_id, user_lat, user_lng):
route = load_route(route_id)
now = datetime.now(CELAYA_TZ)
# 1. Construir horario del día con base en los timestamps templates
first_t = today_at(parse(route.positions[0].timestamp))
last_t = today_at(parse(route.positions[-1].timestamp))
# 2. Determinar fase
if now < first_t: return PROGRAMADO # no ha empezado
if now > last_t: return PASO # ya terminó
# 3. Encontrar entre qué dos waypoints está el camión ahora
current_idx = find_segment(route, now)
# 4. Encontrar el waypoint más cercano al domicilio del usuario
closest_idx = argmin(haversine(user, w) for w in route.positions)
# 5. Comparar
if current_idx > closest_idx: return PASO # ya pasó
if current_idx == closest_idx: return LLEGANDO # muy cerca
# 6. Calcular ventana horaria
user_time = today_at(parse(route.positions[closest_idx].timestamp))
eta_minutes = (user_time - now).minutes
return {
'status': 'EN_CAMINO',
'message': f'Entre las {user_time - 8min} y {user_time + 8min}',
'eta_minutes': eta_minutes,
'progress': elapsed_pct(now, first_t, last_t),
}
```
### Por qué este enfoque
- **No requiere telemetría real:** los timestamps son el "schedule" diario; la posición actual se infiere de la hora
- **Privacy by design built-in:** la API jamás devuelve `lat`/`lng` del camión
- **Predecible:** el ciudadano sabe el rango horario, no un punto exacto
- **Tolerante a tráfico:** las ventanas (`±8 minutos`) absorben la variabilidad
### Geo-routing automático
Cuando un ciudadano registra un domicilio sin `route_id`, `assign_route(lat, lng)` calcula el waypoint más cercano de todas las rutas y asigna la ruta correspondiente (algoritmo: distancia Haversine en O(n) con n=15·8=120 waypoints).
---
## 5. Seguridad
### Autenticación
- **bcrypt** con cost factor 12 para hashing
- **JWT** firmado con HS256, expira en 7 días
- El token incluye `sub` (user_id) y `role`
- Los endpoints sensibles validan el token con `HTTPBearer` + dependency
### Autorización (RBAC)
Tres dependencies en `routers/deps.py`:
```python
get_current_user cualquier rol autenticado
require_staff EMPLEADO o ADMIN (devuelve 403 si no)
require_admin solo ADMIN (devuelve 403 si no)
```
Probado con curl:
```bash
# Ciudadano → /admin/stats → HTTP 403 ✓
# Empleado → /admin/reports → HTTP 403 ✓
# Empleado → /staff/dashboard → HTTP 200 ✓
```
### Pasos adicionales para producción
- [ ] Refresh tokens
- [ ] Rate limiting (slowapi)
- [ ] HTTPS obligatorio
- [ ] CORS estricto (no `*`)
- [ ] Logs auditables (sin PII)
- [ ] Rotación de `SECRET_KEY`
- [ ] 2FA opcional
---
## 6. Privacidad por Diseño
| Requisito hackathon | Cómo se cumple |
|---------------------|----------------|
| "**PROHIBIDO el seguidor de ruta activo**" | El endpoint `/eta/address/:id` **nunca** devuelve `lat`/`lng` del camión. Solo: status, mensaje, ventana horaria, progreso% |
| "**PROHIBIDO el snooping**" | El backend valida `Address.user_id == current_user.id` en cada petición. Si intentas consultar `/eta/address/999` y no te pertenece → 404 |
| "**Visión de túnel: solo tu ruta**" | El cliente solo recibe `route_name` (texto), nunca el polyline de la ruta |
| "**Mensajería preventiva**" | Banner explícito en HomeScreen: *"Por tu seguridad, no persigas al camión. ¡Saca tu basura con tiempo!"* |
### Verificación
```bash
# Login como demo (id=1)
curl -X POST .../auth/login -d '{"email":"demo@celaya.gob.mx","password":"Celaya2026"}'
# Intentar acceder a un domicilio ajeno (id=999)
curl -H "Authorization: Bearer $TOKEN" .../eta/address/999
# → HTTP 404 "Domicilio no encontrado"
```
---
## 7. Simulación de eventos
Cumple el requisito *"Dado que no tendrán acceso a la telemetría real, deberán simular el avance de la ruta"*.
### Estrategia
1. **`routes.json`** contiene 15 rutas con 8 waypoints + timestamps cada una
2. Los timestamps son **templates relativos**: se reinterpretan cada día
3. **`eta_service.py`** los proyecta a la fecha actual y calcula la posición simulada por interpolación temporal
4. **No hay cron job ni websockets** — la simulación es **stateless y on-demand**
### Ventaja
Cualquier petición al backend, en cualquier momento del día, devuelve un ETA coherente con la "hora del camión" sin necesidad de procesos en background.
### Migración a telemetría real
Para producción con GPS real, solo cambiar `eta_service.get_eta()` para que:
1. Lea la posición actual del camión desde una tabla `truck_positions` (alimentada por GPS real)
2. Compare contra el waypoint del usuario
3. Devuelva la misma estructura de respuesta — **el cliente no cambia**
---
## 8. Frontend — Arquitectura
### Routing por rol
```
LoginScreen
↓ (login exitoso)
AuthContext.user.role
├── 'CIUDADANO' → CitizenTabs (5 pestañas)
├── 'EMPLEADO' → EmployeeTabs (3 pestañas)
└── 'ADMIN' → AdminTabs (4 pestañas)
```
### Estado
- **AuthContext** global (React Context) — guarda `user` + métodos `login/logout/register`
- **AsyncStorage** persistente — guarda el JWT entre sesiones
- **Estado local por pantalla** (`useState`) — listas, modales, formularios
- **No usamos Redux/Zustand** — overkill para este alcance
### Networking
- **`fetch`** nativo en lugar de axios (compatibilidad con RN nueva arq.)
- **Interceptor de auth**: `services/api.js` añade el `Authorization: Bearer` automáticamente y limpia el token en 401
- **No hay polling agresivo**: el cliente solo refresca al pull-to-refresh o navegar
### Polyfills críticos
- `react-native-url-polyfill/auto` — reemplaza `global.URL` con la implementación WHATWG completa **antes** de que Expo cargue. Solución al bug *"URL.protocol is not implemented"* en RN 0.74
---
## 9. Decisiones de UX
### Tipografía y color
- **Color guinda institucional** `#7B1A2E` (extraído del logo del Gobierno de Celaya)
- **Acento dorado** `#C8960C` para etiquetas de sección, evocando el slogan "ES GRANDE POR TI"
- **Encabezado guinda detrás de la dynamic island** en todas las pestañas — efecto de marca premium
- Tipografía: pesos 800-900 para títulos, 600 para subtítulos, 400 para cuerpo
### Patrón de pantalla repetido
Todas las pantallas post-login siguen:
```
AppHeader (guinda + "Mi Ruta Limpia")
Hero (gradient o imagen con título grande)
[Etiqueta dorada UPPERCASE] + [Título grande sección]
Cards / contenido
```
### Microinteracciones
- Pull-to-refresh en listas
- Confirmación "ya pasó" con opción **deshacer** (persistente por día/domicilio en AsyncStorage)
- Modales bottom-sheet para formularios largos
- Toast/Alert nativo para feedback
---
## 10. Limitaciones conocidas
| Limitación | Por qué | Mitigación |
|------------|---------|------------|
| Sin push notifications reales | Requiere Firebase Cloud Messaging y certificados | El esquema `push_token` en User está listo; solo falta integrar FCM |
| Sin OAuth real | Requiere SDK de Google/Facebook/Apple | El endpoint `/auth/oauth` está implementado; solo falta integrar el SDK del lado del cliente |
| Sin PDF de reportes | Requiere `expo-print` o servicio externo | Implementamos compartir como texto formateado con `Share` nativo |
| BD no escala a millones | SQLite no es para producción | Cambiar `DATABASE_URL` a PostgreSQL — toda la app sigue funcionando |
| Simulación, no GPS real | Cumple el requisito del hackathon | Endpoint listo para reemplazar con telemetría real |
---
## 11. Sugerencias del brief que SÍ cumplimos
| Sugerencia del reto | Estado |
|--------------------|:------:|
| Consumo inteligente de APIs geográficas (geocodificación solo en registro, cálculos internos) | ✅ |
| Cálculo interno de distancias (Haversine en Python) | ✅ |
| Notificaciones asíncronas listas para FCM | ✅ (esquema preparado) |
| Arquitectura por capas (lógica/datos separados) | ✅ |
| API REST limpia | ✅ |
| Multiplataforma (iOS, Android, Web) | ✅ |
| Manejo eficiente de estado | ✅ (sin polling, refresh manual) |
| Indexación geoespacial básica | ✅ (assign_route por proximidad) |
| Autenticación JWT | ✅ |
| RBAC | ✅ (3 niveles) |
---
## 12. Métricas del proyecto
| Métrica | Valor |
|---------|------:|
| Archivos backend | 27 |
| Archivos frontend (src/) | 22 |
| Líneas de código backend | ~1,800 |
| Líneas de código frontend | ~3,500 |
| Endpoints REST | 28 |
| Modelos de datos | 6 |
| Pantallas | 14 (5 ciudadano + 3 empleado + 4 admin + 2 auth) |
| Componentes reusables | 2 (AppHeader, GoogleGIcon) |
| Datos en BD (seed_massive) | 202 usuarios · 302 domicilios · 280 reportes · 150 op-reportes · 62 camiones · 220 calificaciones |