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