diff --git a/.gitignore b/.gitignore
index 8a0ab32..c18a242 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@
.env.production
.phpactor.json
.phpunit.result.cache
+CLAUDE.md
Homestead.json
Homestead.yaml
auth.json
diff --git a/docker-compose.yml b/docker-compose.yml
index 4f917c5..83c6c5c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -58,19 +58,6 @@ services:
timeout: 15s
retries: 10
- phpmyadmin:
- image: phpmyadmin/phpmyadmin
- environment:
- PMA_HOST: mysql
- PMA_PORT: 3306
- ports:
- - '${PMA_PORT}:80'
- depends_on:
- - mysql
- networks:
- - arcos-network
- mem_limit: 400m
-
redis:
image: redis:alpine
ports:
@@ -85,18 +72,6 @@ services:
timeout: 5s
retries: 5
- redis-commander:
- image: rediscommander/redis-commander:latest
- environment:
- - REDIS_HOSTS=local:redis:6379
- ports:
- - "${REDIS_COMMANDER_PORT}:8081"
- networks:
- - arcos-network
- mem_limit: 256m
- depends_on:
- - redis
-
volumes:
nginx_data:
driver: local
diff --git a/docs/DOCUMENTACION_TECNICA.md b/docs/DOCUMENTACION_TECNICA.md
new file mode 100644
index 0000000..4d3983b
--- /dev/null
+++ b/docs/DOCUMENTACION_TECNICA.md
@@ -0,0 +1,485 @@
+# Documentación Técnica — Arcos Backend
+
+## Índice
+
+1. [Visión General](#visión-general)
+2. [Flujo de Detección RFID](#flujo-de-detección-rfid)
+3. [Gestión de Vehículos Robados](#gestión-de-vehículos-robados)
+4. [Integraciones REPUVE](#integraciones-repuve)
+5. [Gestión de Arcos (Lectores RFID)](#gestión-de-arcos-lectores-rfid)
+6. [Sistema de Alertas](#sistema-de-alertas)
+7. [Registro de Detecciones](#registro-de-detecciones)
+8. [Transmisión en Tiempo Real](#transmisión-en-tiempo-real)
+9. [Esquema de Base de Datos](#esquema-de-base-de-datos)
+10. [Comandos y Tareas Programadas](#comandos-y-tareas-programadas)
+
+---
+
+## Visión General
+
+**Arcos** es una API REST construida en Laravel 12 para la detección de vehículos robados mediante lectores RFID ("arcos"). El sistema conecta lectores de hardware en campo con registros federales y estatales de robo vehicular (REPUVE), registra cada detección en base de datos y emite alertas en tiempo real vía WebSocket cuando un vehículo reportado como robado es detectado.
+
+### Stack tecnológico relevante
+
+| Componente | Tecnología |
+|---|---|
+| Framework | Laravel 12 / PHP 8.3 |
+| Base de datos principal | MySQL 8.0 |
+| Almacenamiento en caliente | Redis |
+| Cola de trabajos | Laravel Queue (driver: database) |
+| WebSockets | Laravel Reverb |
+| Autenticación hardware | Token AES-256-CBC propio |
+| Autenticación usuarios | Laravel Passport (OAuth 2.0) |
+
+---
+
+## Flujo de Detección RFID
+
+Este es el flujo central del sistema. Ocurre cada vez que un lector RFID detecta una etiqueta vehicular.
+
+```
+Lector RFID (hardware)
+ │
+ ▼
+POST /api/vehicles/buscar
+Authorization: Bearer {arco_token}
+{ fast_id, antena?, timestamp? }
+ │
+ ▼
+ArcoTokenMiddleware
+ ├─ Desencripta token del encabezado y lo compara contra arcos.api_token
+ ├─ Verifica que el arco esté activo (activo = true)
+ └─ Inyecta el objeto Arco en $request->get('arco_autenticado')
+ │
+ ▼
+VehicleController::buscarPorTag()
+ └─ Despacha ProcesarDeteccionVehiculo al queue
+ │ (responde 200 OK inmediato al hardware)
+ │
+ ▼ (proceso asíncrono)
+Job: ProcesarDeteccionVehiculo
+ Reintentos: 3 | Backoff: 10 s | Timeout: 120 s
+ │
+ ▼
+VehicleService::procesarDeteccion($fastId, $arcoId, $antena)
+ │
+ ├─ ¿fast_id existe en Redis como robado?
+ │ │
+ │ ├─ SÍ ──► verificarVehiculoRobado()
+ │ │ ├─ Incrementa contador en Redis
+ │ │ ├─ Crea registro en alertas_robos
+ │ │ ├─ Emite evento WebSocket VehiculoRobadoDetectado
+ │ │ └─ Registra en detections + daily_detections (estado: ROBADO)
+ │ │
+ │ └─ NO ──► consultarNuevoVehiculo()
+ │ ├─ Consulta ConsultaRepuveConstancia (API REST)
+ │ │ └─ Si responde → datos del vehículo
+ │ ├─ Si no encontrado → Consulta ReporteRoboService (SOAP federal)
+ │ └─ Registra en detections + daily_detections (estado: LIBRE / DESCONOCIDO)
+```
+
+### Autenticación del hardware (ArcoTokenMiddleware)
+
+Cada lector RFID tiene un `api_token` único almacenado encriptado en la tabla `arcos`. El middleware:
+
+1. Extrae el Bearer token del encabezado `Authorization`
+2. Itera los arcos activos comparando el token desencriptado con `EncryptionHelper::encryptWithCustomKey()`
+3. Rechaza con `401` si el token no corresponde a ningún arco
+4. Rechaza con `403` si el arco está inactivo (`activo = false`)
+
+La clave de encriptación proviene de `ARCO_TOKEN_ENCRYPTION_KEY` en `.env`.
+
+---
+
+## Gestión de Vehículos Robados
+
+El registro activo de vehículos robados vive en **Redis**, no en MySQL. Esto permite búsquedas en tiempo O(1) durante cada detección.
+
+### Registrar vehículo como robado
+
+**`POST /api/vehicles/consultar`** — requiere `auth:api`
+
+```json
+{
+ "placa": "ABC1234",
+ "vin": "1G1FB1RX1234567890",
+ "fecha_robo": "2025-02-01",
+ "autoridad": "PROCURADURÍA",
+ "acta": "ACT-2025-001",
+ "denunciante": "Nombre Denunciante",
+ "fecha_acta": "2025-02-01"
+}
+```
+
+El proceso interno:
+
+1. Consulta el `fast_id` (tag RFID) del vehículo via `VehicleService::consultarVehiculoPorTag()` usando placa o VIN como llave de búsqueda en REPUVE Constancia
+2. Verifica que no esté ya registrado en Redis: `Redis::exists("vehiculo:robado:{fastId}")`
+3. Almacena la entrada en Redis con la clave `vehiculo:robado:{fastId}`:
+
+```php
+[
+ 'fast_id' => string,
+ 'vin' => string,
+ 'placa' => string,
+ 'marca' => string,
+ 'modelo' => string,
+ 'color' => string,
+ 'fecha_robo' => string,
+ 'autoridad' => string,
+ 'acta' => string,
+ 'denunciante' => string,
+ 'fecha_acta' => string,
+ 'primera_deteccion' => ISO8601,
+ 'ultima_deteccion' => ISO8601,
+ 'detecciones' => int // contador incremental
+]
+```
+
+### Recuperar vehículo
+
+**`POST /api/vehicles/recuperar`** — requiere `auth:api`
+
+1. Busca el vehículo en Redis por `placa` o `vin` iterando claves `vehiculo:robado:*`
+2. Crea un registro permanente en MySQL (`vehicles`) con los datos de robo y la fecha de recuperación
+3. Elimina la clave de Redis: `Redis::del("vehiculo:robado:{fastId}")`
+
+### Consultar estado de un vehículo
+
+**`GET /api/vehicles/detectar?placa=...`** o **`GET /api/vehicles/robado?vin=...`**
+
+Busca primero en Redis y luego consulta las APIs REPUVE según sea necesario.
+
+### Listar vehículos robados activos
+
+**`GET /api/vehicles/robados?placa=...&vin=...`**
+
+Lee todas las claves `vehiculo:robado:*` de Redis. Soporta filtro por `placa` y `vin`.
+
+### Listar vehículos recuperados
+
+**`GET /api/vehicles`**
+
+Lee la tabla `vehicles` en MySQL (archivo histórico de recuperaciones).
+
+---
+
+## Integraciones REPUVE
+
+El sistema consume tres APIs externas de registros vehiculares. Las credenciales se configuran en `.env` y se exponen en `config/services.php`.
+
+### 1. REPUVE Constancia (`ConsultaRepuveConstancia`)
+
+API REST con autenticación JWT. Es la fuente principal de datos vehiculares.
+
+**Variables de entorno:**
+```
+REPUVE_CONSTANCIA_BASE_URL
+REPUVE_CONSTANCIA_LOGIN_ENDPOINT
+REPUVE_CONSTANCIA_CONSULTA_ENDPOINT
+REPUVE_CONSTANCIA_EMAIL
+REPUVE_CONSTANCIA_PASSWORD
+REPUVE_CONSTANCIA_TOKEN_TTL # TTL del token en caché (segundos)
+```
+
+**Flujo de autenticación:** El servicio obtiene un JWT via email/contraseña y lo almacena en caché por `TOKEN_TTL` segundos. Se renueva automáticamente al expirar.
+
+**Búsqueda de vehículo** — `consultarVehiculoPorTag()`:
+
+Intenta en orden: `tag_number` → `placa` → `vin`
+
+**Respuesta normalizada:**
+```php
+[
+ 'tag_number' => string, // fast_id / folio RFID
+ 'folio_tag' => string,
+ 'vin' => string,
+ 'placa' => string,
+ 'marca' => string,
+ 'modelo' => string,
+ 'linea' => string,
+ 'sublinea' => string,
+ 'color' => string,
+ 'numero_motor' => string,
+ 'clase_veh' => string,
+ 'tipo_servicio' => string,
+ 'reporte_robo' => bool, // indicador de robo en REPUVE
+ 'propietario' => [
+ 'id', 'nombre_completo', 'rfc', 'curp', 'telefono', 'direccion'
+ ],
+ 'tag_status' => string // ACTIVO / INACTIVO
+]
+```
+
+---
+
+### 2. REPUVE Federal — Reporte de Robo (`ReporteRoboService`)
+
+Servicio SOAP sobre cURL. Consulta el padrón federal de vehículos robados.
+
+**Variables de entorno:**
+```
+REPUVE_FED_BASE_URL
+REPUVE_FED_USERNAME
+REPUVE_FED_PASSWORD
+```
+
+**Método `consultarRobado($vin, $placa)`**
+
+Protocolo SOAP; el cuerpo de la solicitud:
+```xml
+
+ {username}
+ {password}
+ {vin}|{placa}|||||
+
+```
+
+Respuesta parseada desde ``: `OK:indicador|fecha|placa|vin|autoridad|acta|denunciante|fecha_acta`
+
+```php
+[
+ 'tiene_reporte' => bool, // true si indicador == '1'
+ 'datos' => [
+ 'indicador' => '1', // 1 = robado, 0 = no robado
+ 'fecha_robo' => 'YYYY-MM-DD',
+ 'placa' => string,
+ 'vin' => string,
+ 'autoridad' => string,
+ 'acta' => string,
+ 'denunciante' => string,
+ 'fecha_acta' => 'YYYY-MM-DD',
+ ]
+]
+```
+
+**Método `consultarVehiculo($vin, $placa)`**
+
+Consulta el padrón nacional para obtener datos del vehículo (sin estado de robo).
+
+---
+
+### 3. Padrón Estatal (`ConsultaEstatalService`)
+
+Servicio SOAP que responde en JSON. Consulta el registro estatal de vehículos.
+
+**Variable de entorno:** `REPUVE_EST_URL`
+
+---
+
+### Jerarquía de consulta durante una detección
+
+```
+fast_id detectado
+ │
+ ├─ 1° Redis (robados en caliente) → respuesta inmediata
+ ├─ 2° REPUVE Constancia (datos + robo) → API REST / JWT
+ └─ 3° REPUVE Federal (backup) → SOAP
+```
+
+---
+
+## Gestión de Arcos (Lectores RFID)
+
+Un **arco** representa un lector RFID físico instalado en campo.
+
+### Modelo `Arco`
+
+Campos relevantes de la tabla `arcos`:
+
+| Campo | Tipo | Descripción |
+|---|---|---|
+| `nombre` | string | Nombre descriptivo del arco |
+| `ip_address` | string (unique) | IP del lector en la red |
+| `ubicacion` | string | Descripción de la ubicación física |
+| `activo` | boolean | Habilita/deshabilita el arco |
+| `antena_1..4` | string | Etiquetas para cada antena |
+| `api_token` | string | Token encriptado AES-256-CBC |
+
+**Generación de token:** Al crear un arco se genera automáticamente un token de 64 caracteres aleatorios, encriptado con `EncryptionHelper::encryptWithCustomKey()` antes de persistir en BD.
+
+### Endpoints
+
+| Método | Ruta | Acción |
+|---|---|---|
+| `GET` | `/api/arcos` | Listar arcos (filtros: `activo`, `ip`) |
+| `POST` | `/api/arcos` | Crear arco |
+| `GET` | `/api/arcos/{id}` | Detalle de un arco |
+| `PUT/PATCH` | `/api/arcos/{id}` | Actualizar arco |
+| `DELETE` | `/api/arcos/{id}` | Eliminar arco |
+| `PATCH` | `/api/arcos/{id}/toggle-estado` | Activar / desactivar |
+| `GET` | `/api/arcos/{id}/detecciones/dia` | Detecciones del día para ese arco |
+
+---
+
+## Sistema de Alertas
+
+Una alerta se genera automáticamente cuando el job de detección identifica que el `fast_id` detectado corresponde a un vehículo registrado como robado en Redis.
+
+### Generación (`VehicleService::crearAlertaRobo()`)
+
+1. Inserta registro en `alertas_robos`
+2. Emite evento WebSocket `VehiculoRobadoDetectado` al canal `alertas-robos`
+
+### Confirmar alerta
+
+**`PUT /api/alertas/{id}/confirmar`** — requiere `auth:api`
+
+- Si ya fue confirmada (`visto = true`), devuelve `400` con los datos del usuario que la confirmó
+- Si no, actualiza: `visto = true`, `usuario_id = auth()->id()`, `fecha_confirmacion = now()`
+
+### Endpoints de consulta
+
+| Endpoint | Descripción |
+|---|---|
+| `GET /api/alertas/pendientes` | Sin confirmar, ordenadas por más recientes |
+| `GET /api/alertas` | Todas, filtrable por `visto`, `arco_id`, `placa`, `vin`, rango de fechas |
+| `GET /api/alertas/{id}` | Detalle de una alerta |
+
+---
+
+## Registro de Detecciones
+
+Cada detección se persiste en **dos tablas** con propósitos distintos.
+
+### `detections` — Historial completo
+
+Registro individual e inmutable de cada evento RFID.
+
+| Campo | Descripción |
+|---|---|
+| `arco_id` | FK al arco que detectó |
+| `fast_id` | Tag RFID leído |
+| `vin`, `placa`, `marca`, `modelo`, `color` | Datos del vehículo al momento de detección |
+| `antena` | Número de antena del arco |
+| `fecha_deteccion` | Timestamp exacto |
+
+### `daily_detections` — Resumen diario
+
+Incluye información adicional de estado, útil para dashboards y reportes.
+
+| Campo | Descripción |
+|---|---|
+| `arco_nombre` | Nombre del arco (desnormalizado) |
+| `estado` | `LIBRE` \| `ROBADO` \| `DESCONOCIDO` |
+| `tiene_reporte_robo` | Boolean — si REPUVE reporta robo |
+| `fecha_dia` | Columna virtual: `DATE(fecha_deteccion)` |
+
+Índices: `arco_id`, `fecha_dia`, `[arco_id + fecha_dia]`, `fast_id`
+
+### Endpoints de consulta
+
+| Endpoint | Descripción |
+|---|---|
+| `GET /api/vehicles/detecciones` | Historial paginado (filtros: `placa`, `vin`) |
+| `GET /api/vehicles/detecciones/dia?fecha=YYYY-MM-DD` | Resumen diario (ventana de últimos 10 s) |
+| `GET /api/arcos/{id}/detecciones/dia?fecha=YYYY-MM-DD` | Resumen diario por arco |
+
+---
+
+## Transmisión en Tiempo Real
+
+**Servidor:** Laravel Reverb (WebSocket)
+
+Cuando se detecta un vehículo robado, se emite el evento `VehiculoRobadoDetectado`:
+
+- **Canal:** `alertas-robos` (público)
+- **Nombre en frontend:** `vehiculo.robado.detectado`
+- **Payload:**
+
+```json
+{
+ "alerta_id": 42,
+ "fast_id": "1234567890",
+ "vin": "1G1FB1RX1234567890",
+ "placa": "ABC1234",
+ "marca": "Chevrolet",
+ "modelo": "Cruze",
+ "color": "BLANCO",
+ "arco_id": 3,
+ "arco_nombre": "Arco Reforma Norte",
+ "antena": "1",
+ "fecha_deteccion": "2025-02-01T14:30:00Z",
+ "mensaje": "🚨 VEHÍCULO ROBADO DETECTADO: ABC1234 en Arco Reforma Norte"
+}
+```
+
+El frontend conecta vía **Laravel Echo + Pusher.js**.
+
+---
+
+## Esquema de Base de Datos
+
+### `arcos`
+```sql
+id, nombre, ip_address (UNIQUE), ubicacion, descripcion,
+activo BOOLEAN DEFAULT true,
+antena_1, antena_2, antena_3, antena_4,
+api_token,
+created_at, updated_at
+```
+
+### `detections`
+```sql
+id, arco_id (FK arcos), fast_id, vin, placa,
+marca, modelo, color, antena, fecha_deteccion,
+created_at, updated_at
+```
+
+### `daily_detections`
+```sql
+id, arco_id (FK arcos), arco_nombre, antena,
+fast_id, vin, placa, marca, modelo, color,
+estado ENUM('LIBRE','ROBADO','DESCONOCIDO'),
+tiene_reporte_robo BOOLEAN,
+fecha_deteccion, fecha_dia (virtual: DATE(fecha_deteccion)),
+created_at, updated_at
+```
+
+### `vehicles` — Archivo de recuperaciones
+```sql
+id, fast_id, vin, placa,
+fecha_robo, acta_robo, denunciante, fecha_acta,
+fecha_recuperacion,
+datos_robo_original JSON,
+created_at, updated_at
+```
+
+### `alertas_robos`
+```sql
+id, fast_id, vin, placa, marca, modelo, color,
+arco_id (FK arcos), arco_nombre, antena,
+fecha_deteccion,
+visto BOOLEAN DEFAULT false,
+usuario_id (FK users, NULLABLE),
+fecha_confirmacion NULLABLE,
+created_at, updated_at
+```
+
+---
+
+## Comandos y Tareas Programadas
+
+### `php artisan detecciones:limpiar {--dias=1}`
+
+Elimina registros de `daily_detections` con más de N días de antigüedad (default: 1 día). Se puede ejecutar manualmente o agregar al scheduler.
+
+```bash
+php artisan detecciones:limpiar # elimina detecciones de más de 1 día
+php artisan detecciones:limpiar --dias=7 # elimina detecciones de más de 7 días
+```
+
+### Servicios en producción (PM2)
+
+```bash
+composer run services:start # inicia queue worker, scheduler y Reverb
+composer run services:stop # detiene todos los servicios
+composer run services:status # muestra estado de cada proceso PM2
+```
+
+Procesos individuales:
+```bash
+php artisan queue:work # procesa detecciones encoladas
+php artisan schedule:work # ejecuta tareas programadas
+php artisan reverb:start # servidor WebSocket
+```