Añadir 04-integration.md

2026-05-23 02:39:15 +00:00
parent ebfe7829e1
commit d1dcb8254d

454
04-integration.md.-.md Normal file

@@ -0,0 +1,454 @@
# Integración entre Módulos
Guía de cómo conectar el trabajo de las 4 personas.
---
## Visión general
```
┌──────────────┐ ┌──────────────┐
│ Persona A │ │ Persona B │
│ (Auth) │◄────►│ (Simulador) │
└──────┬───────┘ └──────┬───────┘
│ │
│ Backend Python │
└──────────┬──────────┘
│ HTTP + WebSocket
┌──────────▼──────────┐
│ │
┌──────▼───────┐ ┌──────────▼─────┐
│ Persona C │ │ Persona D │
│ (ETA UI) │ │ (Guía) │
└──────────────┘ └────────────────┘
Frontend Flutter
```
---
## 1. Backend: Integración A + B
### Responsabilidades compartidas
| Persona A | Persona B | Compartido |
|-----------|-----------|------------|
| JWT tokens | Simulador | Esquema BD |
| RBAC | WebSocket | truck_status |
| Alta domicilios | ETA cálculo | addresses |
| Validar zona | Notificaciones | notification_preferences |
### Base de datos unificada
**Persona A crea:**
- `users`
- `addresses`
- `notification_preferences`
- `notification_templates`
**Persona B usa (read-only):**
- `addresses.route_id` — para filtrar domicilios por ruta
- `notification_preferences` — antes de enviar WebSocket
- `notification_templates` — para formatear mensajes
**Persona B crea y gestiona:**
- `truck_status`
- `rutas`
- `puntos_ruta`
- `notificaciones` (log)
### Endpoint compartido: `/alerts/breakdown`
**Persona A puede llamarlo** desde su módulo cuando detecta un reporte de usuario.
**Persona B lo implementa** y maneja:
1. Detener el simulador
2. Actualizar `truck_status` a `AVERIADA`
3. Broadcast WebSocket a todos los domicilios de la ruta
**Código de integración:**
```python
# app/api/routes/alerts_router.py (compartido)
from app.services.simulador import Simulador
from app.services.ws_manager import ws_manager
@router.post("/alerts/breakdown")
async def report_breakdown(payload: BreakdownRequest):
# Persona B: detener simulador
simulador = Simulador()
simulador.forzar_averia(payload.route_id)
# Persona B: broadcast WebSocket
addresses = obtener_addresses_de_ruta(payload.route_id)
for addr in addresses:
await ws_manager.broadcast_zona(
str(addr.id),
{
"tipo": "falla_mecanica",
"address_id": addr.id,
"mensaje": "...",
}
)
return {"message": "Alerta de avería enviada"}
```
### Middleware JWT (Persona A → usado por B)
**Persona A crea:**
```python
# app/core/auth.py
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer
security = HTTPBearer()
async def verify_token(token: str = Depends(security)):
try:
payload = jwt.decode(token, SECRET_KEY)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(401, "Token expirado")
```
**Persona B lo usa:**
```python
# app/api/routes/eta_router.py
from app.core.auth import verify_token
@router.get("/eta/{address_id}")
async def get_eta(
address_id: int,
user = Depends(verify_token) # ← agregado por Persona B
):
# Verificar que el address_id pertenece al user
if not address_belongs_to_user(address_id, user['sub']):
raise HTTPException(403, "No autorizado")
# ... lógica de ETA
```
---
## 2. Frontend: Integración C + D
### Estructura de carpetas unificada
```
basura_app/lib/
├── core/
│ ├── theme/
│ │ └── app_theme.dart # Persona D crea, Persona C usa
│ └── config/
│ ├── api_config.dart # Persona C
│ └── router.dart # Persona C
└── features/
├── auth/ # Persona C
├── eta/ # Persona C
└── recycling_guide/ # Persona D
```
### Tema compartido
**Persona D crea:**
```dart
// lib/core/theme/app_theme.dart
class AppTheme {
static const primaryColor = Color(0xFF1B5E20);
static ThemeData get lightTheme => ThemeData(...);
}
```
**Persona C usa en main.dart:**
```dart
import 'core/theme/app_theme.dart';
MaterialApp(
theme: AppTheme.lightTheme, // ← tema de Persona D
// ...
);
```
### Router (Persona C configura)
```dart
// lib/core/config/router.dart
import 'package:go_router/go_router.dart';
import '../../features/eta/presentation/screens/eta_home_screen.dart';
import '../../features/recycling_guide/presentation/screens/recycling_guide_screen.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (_, __) => const ETAHomeScreen(), // Persona C
),
GoRoute(
path: '/guia',
builder: (_, __) => const RecyclingGuideScreen(), // Persona D
),
],
);
```
### Navegación (desde ETA home a guía)
**Persona C agrega botón:**
```dart
// lib/features/eta/presentation/screens/eta_home_screen.dart
FloatingActionButton(
child: Icon(Icons.recycling),
onPressed: () => context.go('/guia'), // ← navega a módulo D
)
```
### Providers independientes (sin conflictos)
**Persona C:**
```dart
// lib/features/eta/presentation/providers/eta_provider.dart
final etaProvider = FutureProvider.family<ETAInfo, int>(...);
final wsProvider = StreamProvider.family<NotificationEvent, int>(...);
```
**Persona D:**
```dart
// lib/features/recycling_guide/presentation/providers/recycling_provider.dart
final recyclingCategoriesProvider = FutureProvider<List<RecyclingCategory>>(...);
final recyclingSearchProvider = StateNotifierProvider<...>(...);
```
**Cero conflictos** — cada uno tiene su namespace de providers.
---
## 3. Flujos completos end-to-end
### Flujo 1: Usuario se registra y consulta ETA
```
1. Usuario abre app → Flutter muestra LoginScreen (Persona C)
2. Usuario toca "Registrarse" → Flutter llama POST /register (Persona A)
Backend valida, crea usuario, retorna JWT
3. Usuario ingresa domicilio → Flutter llama POST /addresses (Persona A)
Backend asigna route_id según lat/lng
4. Flutter navega a ETAHomeScreen (Persona C)
→ GET /eta/{address_id} (Persona B)
→ Muestra ventana horaria
5. Flutter conecta WebSocket /ws/{address_id} (Persona B)
→ Recibe eventos en tiempo real
```
### Flujo 2: Usuario explora la guía offline
```
1. Usuario toca ícono de reciclaje → Flutter navega a /guia (Persona D)
2. RecyclingGuideScreen carga assets/recycling_guide.json (Persona D)
→ Muestra 4 categorías
3. Usuario escribe "pila" en buscador → RecyclingRepository.buscar() (Persona D)
→ Muestra "Especiales"
4. Usuario toca "Especiales" → CategoryDetailScreen (Persona D)
→ Muestra items ✓ y ✗
5. Todo funciona sin internet — cero llamadas al backend
```
### Flujo 3: Simulador notifica proximidad
```
1. Admin arranca simulador → POST /admin/route/RUTA-01/start (Persona B)
→ APScheduler inicia ticks cada 10s
2. Cada tick, Persona B:
→ Avanza truck_status.current_position_id
→ Calcula ETA para cada address en RUTA-01
→ Si ETA <= 10 min:
→ Consulta notification_preferences (Persona A)
→ Si notify_proximity == True:
→ Broadcast WebSocket (Persona B)
3. Flutter (Persona C) recibe evento "aproximandose"
→ Muestra SnackBar: "El camión llega en ~8 minutos"
→ Notificación push local
```
### Flujo 4: Usuario reporta avería
```
1. Usuario toca botón "Reportar problema" (Persona C)
→ POST /alerts/breakdown con route_id (endpoint compartido A+B)
2. Backend (Persona B):
→ Detiene simulador
→ Actualiza truck_status a AVERIADA
→ Broadcast WebSocket a todos los address de RUTA-01
3. Flutter (Persona C) recibe evento "falla_mecanica"
→ Actualiza badge a "Avería"
→ Muestra mensaje de disculpa
```
---
## 4. Pruebas de integración
### Test 1: Backend A + B
```bash
# 1. Registrar usuario (Persona A)
curl -X POST http://localhost:8000/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@ejemplo.com",
"password": "test123",
"phone": "+52 1234567890"
}'
# Guardar el token retornado
# 2. Dar de alta domicilio (Persona A)
curl -X POST http://localhost:8000/addresses \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"alias": "Test House",
"lat": 20.5200,
"lng": -100.8100
}'
# Guardar el address_id retornado
# 3. Consultar ETA (Persona B)
curl http://localhost:8000/eta/<ADDRESS_ID> \
-H "Authorization: Bearer <TOKEN>"
# Debería retornar ventana horaria
# 4. Arrancar simulador (Persona B)
curl -X POST http://localhost:8000/admin/route/RUTA-01/start
# 5. Conectar WebSocket (Persona B)
wscat -c ws://localhost:8000/ws/<ADDRESS_ID>
# Deberías recibir eventos cada 10s
```
### Test 2: Flutter C + D
```bash
# 1. Correr Flutter
fvm flutter run
# 2. Login (Persona C)
# → Ingresar credenciales en UI
# 3. Ver ETA home (Persona C)
# → Debería mostrar ventana horaria
# 4. Navegar a guía (Persona D)
# → Tocar ícono de reciclaje
# → Verificar que carga 4 categorías
# 5. Buscar "pila" (Persona D)
# → Verificar que aparece "Especiales"
# 6. Volver a home y esperar notificación (Persona C)
# → Debería recibir evento WebSocket cada 10s
```
---
## 5. Checklist de integración
### Backend (A + B)
- [ ] Esquema de BD unificado (database.py compartido)
- [ ] Persona A crea tablas de auth
- [ ] Persona B crea tablas de simulador
- [ ] Endpoint `/alerts/breakdown` implementado por B
- [ ] Middleware JWT de A agregado a rutas de B
- [ ] CORS habilitado para desarrollo
### Frontend (C + D)
- [ ] `app_theme.dart` de D copiado a `core/theme/`
- [ ] `recycling_guide/` de D copiado a `features/`
- [ ] `assets/recycling_guide.json` agregado y declarado en pubspec
- [ ] Router de C incluye ruta `/guia` → RecyclingGuideScreen
- [ ] main.dart usa `AppTheme.lightTheme`
- [ ] Navegación de home a guía funciona
### Contratos API
- [ ] Backend corre en puerto 8000
- [ ] Flutter usa URL correcta (10.0.2.2 para Android emulator)
- [ ] JWT token se pasa en headers cuando sea necesario
- [ ] WebSocket conecta con address_id válido
---
## 6. Troubleshooting de integración
### "401 Unauthorized" en GET /eta
**Causa:** Persona C olvidó agregar el token JWT.
**Solución:**
```dart
final dio = Dio(BaseOptions(
headers: {'Authorization': 'Bearer $token'},
));
```
---
### "404 Not Found" en POST /addresses
**Causa:** Persona A no implementó ese endpoint aún.
**Solución temporal:**
```dart
// Mock el address_id en Persona C
final mockAddressId = 1; // del seed de Persona B
```
---
### WebSocket no recibe eventos
**Causa:** El simulador no está corriendo.
**Solución:**
```bash
curl -X POST http://localhost:8000/admin/route/RUTA-01/start
```
---
### La guía no carga categorías
**Causa:** `recycling_guide.json` no está en `pubspec.yaml`.
**Solución:**
```yaml
flutter:
assets:
- assets/recycling_guide.json
```
---
## Siguiente paso
- [Errores comunes](../troubleshooting/01-errores-comunes.md)
- [Problemas de versiones](../troubleshooting/02-versiones.md)