ADD: Implementa controlador y modelo para gestión de arcos RFID, incluyendo rutas y migraciones

This commit is contained in:
Juan Felipe Zapata Moreno 2026-01-06 12:45:18 -06:00
parent 19e9491e04
commit 505729ba19
9 changed files with 352 additions and 68 deletions

View File

@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Arco;
use Illuminate\Http\Request;
use Notsoweb\ApiResponse\Enums\ApiResponse;
/**
* Controlador para gestión de Arcos
*/
class ArcoController extends Controller
{
/**
* Listar todos los arcos
* GET /api/arcos
*/
public function index(Request $request)
{
$query = Arco::query();
// Filtro por estado activo
if ($request->has('activo')) {
$query->where('activo', $request->boolean('activo'));
}
// Filtro por IP
if ($request->has('ip')) {
$query->where('ip_address', 'like', '%' . $request->ip . '%');
}
$arcos = $query->orderBy('created_at', 'desc')->get();
return ApiResponse::OK->response([
'arcos' => $arcos
]);
}
/**
* Crear un nuevo arco
* POST /api/arcos
*/
public function store(Request $request)
{
$validated = $request->validate([
'nombre' => 'required|string|max:255',
'ip_address' => 'required|ip|unique:arcos,ip_address',
'ubicacion' => 'nullable|string|max:255',
'descripcion' => 'nullable|string',
'activo' => 'boolean'
]);
$arco = Arco::create($validated);
return ApiResponse::CREATED->response([
'message' => 'Arco creado exitosamente',
'arco' => $arco
]);
}
/**
* Mostrar un arco específico
* GET /api/arcos/{id}
*/
public function show(int $id)
{
$arco = Arco::with(['detecciones' => function ($query) {
$query->latest()->limit(10);
}])->find($id);
if (!$arco) {
return ApiResponse::NOT_FOUND->response([
'message' => 'Arco no encontrado'
]);
}
return ApiResponse::OK->response([
'arco' => $arco
]);
}
/**
* Actualizar un arco
* PUT/PATCH /api/arcos/{id}
*/
public function update(Request $request, int $id)
{
$arco = Arco::find($id);
if (!$arco) {
return ApiResponse::NOT_FOUND->response([
'message' => 'Arco no encontrado'
]);
}
$validated = $request->validate([
'nombre' => 'sometimes|required|string|max:255',
'ip_address' => 'sometimes|required|ip|unique:arcos,ip_address,' . $id,
'ubicacion' => 'nullable|string|max:255',
'descripcion' => 'nullable|string',
'activo' => 'boolean'
]);
$arco->update($validated);
return ApiResponse::OK->response([
'message' => 'Arco actualizado exitosamente',
'arco' => $arco
]);
}
/**
* Eliminar un arco
* DELETE /api/arcos/{id}
*/
public function destroy(int $id)
{
$arco = Arco::find($id);
if (!$arco) {
return ApiResponse::NOT_FOUND->response([
'message' => 'Arco no encontrado'
]);
}
$arco->delete();
return ApiResponse::OK->response([
'message' => 'Arco eliminado exitosamente'
]);
}
/**
* Activar/Desactivar un arco
* PATCH /api/arcos/{id}/toggle-estado
*/
public function toggleEstado(int $id)
{
$arco = Arco::find($id);
if (!$arco) {
return ApiResponse::NOT_FOUND->response([
'message' => 'Arco no encontrado'
]);
}
$arco->activo = !$arco->activo;
$arco->save();
return ApiResponse::OK->response([
'message' => $arco->activo ? 'Arco activado' : 'Arco desactivado',
'arco' => $arco
]);
}
}

View File

@ -377,30 +377,35 @@ public function buscarVehiculoRobado(Request $request)
}
/**
* Buscar vehículo por tag_id (tag_number)
* GET /api/vehicles/buscar
* Procesar detección de tag RFID
* POST /api/vehicles/buscar
*/
public function buscarPorTag(Request $request)
{
$validated = $request->validate([
'tag_id' => 'required|string',
'fast_id' => 'required|string',
'epc' => 'nullable|string',
'arco_ip' => 'nullable|ip',
'timestamp' => 'nullable|date'
]);
$resultado = $this->vehicleService->consultarVehiculoPorTag(
epc: null,
tagId: $validated['tag_id']
// Si no se proporciona EPC, usar el FastID como identificador
$epc = $validated['epc'] ?? $validated['fast_id'];
$resultado = $this->vehicleService->procesarDeteccion(
$epc,
$validated['fast_id'],
$validated['arco_ip'],
$validated['timestamp'] ?? null
);
if (!$resultado) {
if (!$resultado['success']) {
return ApiResponse::NOT_FOUND->response([
'success' => false,
'message' => 'No se encontró el vehículo'
'message' => $resultado['message'] ?? 'No se encontró el vehículo'
]);
}
return ApiResponse::OK->response([
'success' => true,
'vehiculo' => $resultado
]);
return ApiResponse::OK->response($resultado);
}
}

59
app/Models/Arco.php Normal file
View File

@ -0,0 +1,59 @@
<?php namespace App\Models;
/**
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Modelo Arco RFID
*
* Representa un arco lector RFID físico identificado por su dirección IP
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
* @version 1.0.0
*/
class Arco extends Model
{
protected $table = 'arcos';
protected $fillable = [
'nombre',
'ip_address',
'ubicacion',
'activo'
];
protected $casts = [
'activo' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* Relación con detecciones
*/
public function detecciones(): HasMany
{
return $this->hasMany(Detection::class, 'arco_id');
}
/**
* Scope para obtener solo arcos activos
*/
public function scopeActivos($query)
{
return $query->where('activo', true);
}
/**
* Buscar arco por IP
*/
public static function buscarPorIp(string $ip): ?self
{
return self::where('ip_address', $ip)->first();
}
}

View File

@ -14,25 +14,6 @@ public function __construct()
$this->soapUrl = config('services.padron_estatal.url');
}
/**
* Consulta vehículo
*/
public function consultarPorEpc(string $epc): ?array
{
try {
$datos = $this->consultarPadron('niv', $epc);
return [
'placa' => $datos['placa'] ?? null,
'vin' => $datos['niv'] ?? null,
];
} catch (Exception $e) {
Log::error("Error consultando padrón por EPC: {$epc} - " . $e->getMessage());
return null;
}
}
/**
* Consulta el padrón vehicular estatal vía SOAP
*/

View File

@ -20,7 +20,7 @@ public function __construct()
$this->password = config('services.repuve_federal.password');
}
public function consultarPorVin(?string $vin = null, ?string $placa = null)
public function consultarRobado(?string $vin = null, ?string $placa = null)
{
try {
if (empty($vin) && empty($placa)) {
@ -43,11 +43,6 @@ public function consultarPorVin(?string $vin = null, ?string $placa = null)
$arg2 = '||' . $placa . str_repeat('|', 5);
}
Log::info('ReporteRoboService: Consultando REPUVE', [
'vin' => $vin,
'placa' => $placa
]);
$soapBody = <<<XML
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsdl="http://consultaRpv.org/wsdl">
<soapenv:Header/>
@ -125,7 +120,7 @@ private function parseRoboResponse(string $soapResponse, string $valor)
return [
'tiene_reporte' => false,
'datos' => [],
'error' => 'Respuesta SOAP inválida'
'error' => 'Respuesta inválida'
];
}

View File

@ -4,94 +4,111 @@
use App\Models\Vehicle;
use App\Models\Detection;
use App\Models\Arco;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;
class VehicleService
{
public function __construct(
private ConsultaEstatalService $consultaEstatal,
private ReporteRoboService $reporteRobo,
private ConsultaRepuveConstancia $consultaRepuveCons
) {}
public function procesarDeteccion(string $epc, ?int $arcoId = null): array
public function procesarDeteccion(string $epc, string $fastId, string $arcoIp, ?string $timestamp = null): array
{
$key = "vehiculo:robado:{$epc}";
// Buscar o crear arco por IP
$arco = Arco::buscarPorIp($arcoIp);
$arcoId = $arco?->id;
// Verificar si está en Redis
$enRedis = Redis::get($key);
if ($enRedis) {
// Ya está marcado como robado, verificar si sigue así
$resultado = $this->verificarVehiculoRobado($epc, $arcoId, json_decode($enRedis, true));
$this->registrarDeteccion($epc, $resultado);
$resultado = $this->verificarVehiculoRobado($epc, json_decode($enRedis, true));
$this->registrarDeteccion($epc, $resultado, $arcoId);
return $resultado;
}
// No está en Redis, consultar servicios
$resultado = $this->consultarNuevoVehiculo($epc, $arcoId);
$this->registrarDeteccion($epc, $resultado);
$resultado = $this->consultarNuevoVehiculo($epc, $fastId);
$this->registrarDeteccion($epc, $resultado, $arcoId);
return $resultado;
}
private function consultarNuevoVehiculo(string $epc, ?int $arcoId): array
private function consultarNuevoVehiculo(string $epc, string $fastId): array
{
// Consultar padrón estatal
$datosEstatal = $this->consultaEstatal->consultarPorEpc($epc);
// Consultar con FastID (tag_number)
$datosVehiculo = $this->consultaRepuveCons->consultarVehiculoPorTag($fastId);
if (!$datosVehiculo || !$datosVehiculo['vin']) {
Log::warning('Vehículo NO encontrado en API externa', [
'epc' => $epc,
'fast_id' => $fastId
]);
if (!$datosEstatal || !$datosEstatal['vin']) {
return [
'success' => false,
'message' => 'No se encontró información del vehículo'
'message' => 'No se encontró información del vehículo en la API externa'
];
}
// Consultar REPUVE
$reporteRobo = $this->reporteRobo->consultarPorVin(
$datosEstatal['vin'],
$datosEstatal['placa']
// Consultar REPUVE para verificar si está robado
$reporteRobo = $this->reporteRobo->consultarRobado(
$datosVehiculo['vin'],
$datosVehiculo['placa']
);
if ($reporteRobo['tiene_reporte']) {
// Está robado → Guardar en Redis
$this->guardarEnRedis($epc, $datosEstatal, $reporteRobo['datos']);
$this->guardarEnRedis($epc, $datosVehiculo, $reporteRobo['datos']);
Log::warning('¡VEHÍCULO ROBADO DETECTADO!', [
'epc' => $epc,
'vin' => $datosEstatal['vin'],
'placa' => $datosEstatal['placa']
'fast_id' => $fastId,
'vin' => $datosVehiculo['vin'],
'placa' => $datosVehiculo['placa']
]);
return [
'success' => true,
'tiene_reporte_robo' => true,
'estado' => 'ROBADO',
'accion' => 'GUARDADO_EN_REDIS',
'vehiculo' => array_merge($datosEstatal, $reporteRobo['datos'])
'accion' => 'GUARDADO EN BD DE ROBADOS',
'vehiculo' => array_merge($datosVehiculo, $reporteRobo['datos'])
];
}
// No está robado, no hacer nada
// No está robado - vehículo LIBRE
Log::info('Vehículo detectado - LIBRE (sin reporte de robo)', [
'epc' => $epc,
'fast_id' => $fastId,
'vin' => $datosVehiculo['vin'],
'placa' => $datosVehiculo['placa']
]);
return [
'success' => true,
'tiene_reporte_robo' => false,
'estado' => 'LIBRE',
'vehiculo' => $datosEstatal
'vehiculo' => $datosVehiculo
];
}
private function verificarVehiculoRobado(string $epc, ?int $arcoId, array $datosRedis): array
private function verificarVehiculoRobado(string $epc, array $datosRedis): array
{
// Consultar REPUVE para verificar estado actual
$reporteRobo = $this->reporteRobo->consultarPorVin(
$reporteRobo = $this->reporteRobo->consultarRobado(
$datosRedis['vin'],
$datosRedis['placa']
);
if (!$reporteRobo['tiene_reporte']) {
// No tiene reporte robo
$this->registrarRecuperacion($epc, $arcoId, $datosRedis);
// No tiene reporte robo - RECUPERADO
$this->registrarRecuperacion($epc, $datosRedis);
Log::info('¡VEHÍCULO RECUPERADO!', [
'epc' => $epc,
@ -103,7 +120,7 @@ private function verificarVehiculoRobado(string $epc, ?int $arcoId, array $datos
'success' => true,
'tiene_reporte_robo' => false,
'estado' => 'RECUPERADO',
'accion' => 'GUARDADO_EN_MYSQL_Y_ELIMINADO_DE_REDIS',
'accion' => 'GUARDADO EN RECUPERADOS Y ELIMINADO DE ROBADOS',
'vehiculo' => $datosRedis
];
}
@ -156,7 +173,7 @@ private function actualizarDeteccionRedis(string $epc, array $datosActuales)
Redis::set($key, json_encode($datosActuales));
}
private function registrarRecuperacion(string $epc, ?int $arcoId, array $datosRedis)
private function registrarRecuperacion(string $epc, array $datosRedis)
{
// Guardar en MySQL
Vehicle::create([
@ -191,7 +208,7 @@ public function listarVehiculosRobados(): array
return $vehiculos;
}
private function registrarDeteccion(string $epc, array $resultado)
private function registrarDeteccion(string $epc, array $resultado, ?int $arcoId = null)
{
if (!$resultado['success'] || !isset($resultado['vehiculo'])) {
return;
@ -200,6 +217,7 @@ private function registrarDeteccion(string $epc, array $resultado)
$vehiculo = $resultado['vehiculo'];
Detection::create([
'arco_id' => $arcoId,
'epc' => $epc,
'vin' => $vehiculo['vin'] ?? null,
'placa' => $vehiculo['placa'] ?? null,

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('arcos', function (Blueprint $table) {
$table->id();
$table->string('nombre');
$table->string('ip_address')->unique();
$table->string('ubicacion')->nullable();
$table->boolean('activo');
$table->timestamps();
$table->index('ip_address');
$table->index('activo');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('arcos');
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('detections', function (Blueprint $table) {
$table->foreignId('arco_id')->nullable()->after('id')->constrained('arcos')->onDelete('set null');
$table->index('arco_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('detections', function (Blueprint $table) {
$table->dropForeign(['arco_id']);
$table->dropColumn('arco_id');
});
}
};

View File

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\Api\VehicleController;
use App\Http\Controllers\Api\ArcoController;
use Illuminate\Support\Facades\Route;
/**
@ -20,14 +21,19 @@
/** Rutas protegidas (requieren autenticación) */
Route::middleware('auth:api')->group(function() {
// Rutas de Vehículos
Route::post('/vehicles/consultar', [VehicleController::class, 'consultarVehiculo']);
Route::post('/vehicles/recuperar', [VehicleController::class, 'recuperarVehiculo']);
Route::post('/vehicles/buscar', [VehicleController::class, 'buscarPorTag']);
Route::get('/vehicles/detectar', [VehicleController::class, 'buscarVehiculo']);
Route::get('/vehicles/robados', [VehicleController::class, 'listarRobados']);
Route::get('/vehicles', [VehicleController::class, 'listarRecuperados']);
Route::get('/vehicles/detecciones', [VehicleController::class, 'listarDetecciones']);
Route::get('/vehicles/robado', [VehicleController::class, 'buscarVehiculoRobado']);
Route::get('/vehicles/buscar', [VehicleController::class, 'buscarPorTag']);
// Rutas de Arcos RFID
Route::resource('/arcos', ArcoController::class);
Route::patch('/arcos/{id}/toggle-estado', [ArcoController::class, 'toggleEstado']);
});
/** Rutas públicas */