1
04-integration.md
hack_23031391_8ff9d8 edited this page 2026-05-23 02:39:15 +00:00

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:

# 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:

# 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:

# 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:

// lib/core/theme/app_theme.dart
class AppTheme {
  static const primaryColor = Color(0xFF1B5E20);
  static ThemeData get lightTheme => ThemeData(...);
}

Persona C usa en main.dart:

import 'core/theme/app_theme.dart';

MaterialApp(
  theme: AppTheme.lightTheme,  // ← tema de Persona D
  // ...
);

Router (Persona C configura)

// 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:

// 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:

// lib/features/eta/presentation/providers/eta_provider.dart
final etaProvider = FutureProvider.family<ETAInfo, int>(...);
final wsProvider = StreamProvider.family<NotificationEvent, int>(...);

Persona D:

// 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

# 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

# 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:

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:

// 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:

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:

flutter:
  assets:
    - assets/recycling_guide.json

Siguiente paso