Initial commit

This commit is contained in:
marianesaldana
2026-05-23 08:59:34 -06:00
commit 80dbd947e5
36446 changed files with 3729147 additions and 0 deletions

318
docs/ARQUITECTURA.md Normal file
View File

@@ -0,0 +1,318 @@
# 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 |