Añadir 04-integration.md
454
04-integration.md.-.md
Normal file
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)
|
||||||
Reference in New Issue
Block a user