feat: mejora validación de vehículos y manejo de datos REPUVE

- Se actualizó la validación de vehículos en la sustitución de tags.
- Se mejoró el manejo de datos provenientes de REPUVE.
This commit is contained in:
Juan Felipe Zapata Moreno 2026-03-27 14:40:34 -06:00
parent c0b263eb87
commit be300d449d
7 changed files with 303 additions and 154 deletions

View File

@ -53,7 +53,7 @@ services:
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_USER: ${DB_USERNAME}
ports:
- "${DB_FORWARD_PORT}:3306"
- "${DB_PORT}:3306"
volumes:
- mysql_data:/var/lib/mysql
networks:

View File

@ -119,7 +119,7 @@ public function vehicleInscription(VehicleStoreRequest $request)
]);
}
// Consultar REPUVE Nacional para obtener datos oficiales del vehículo
// Consultar REPUVE Nacional para corroborar el vehículo y obtener folio_CI
$repuveNacionalData = $this->repuveService->consultarVehiculo($niv, $placa);
// Verificar si hubo error en la consulta a REPUVE Nacional
@ -171,34 +171,11 @@ public function vehicleInscription(VehicleStoreRequest $request)
]
);
// Crear vehículo combinando datos de REPUVE Nacional y estatal
$vehicle = Vehicle::create([
// Datos de Estatal
'placa' => $vehicleDataEstatal['placa'],
'numero_motor' => $vehicleDataEstatal['numero_motor'],
// Datos de REPUVE NACIONAL
'niv' => $repuveNacionalData['niv'],
'marca' => $repuveNacionalData['marca'],
'linea' => $repuveNacionalData['linea'],
'modelo' => $repuveNacionalData['modelo'],
// Otros datos - de estatal
'sublinea' => $vehicleDataEstatal['sublinea'],
'color' => $repuveNacionalData['color'] ?? $vehicleDataEstatal['color'],
'clase_veh' => $vehicleDataEstatal['clase_veh'],
'tipo_servicio' => $vehicleDataEstatal['tipo_servicio'],
'rfv' => $vehicleDataEstatal['rfv'],
'ofcexpedicion' => $vehicleDataEstatal['ofcexpedicion'],
'fechaexpedicion' => $vehicleDataEstatal['fechaexpedicion'],
'tipo_veh' => $vehicleDataEstatal['tipo_veh'],
'numptas' => $vehicleDataEstatal['numptas'],
'observac' => $vehicleDataEstatal['observac'],
'cve_vehi' => $vehicleDataEstatal['cve_vehi'],
'nrpv' => $vehicleDataEstatal['nrpv'],
'tipo_mov' => $vehicleDataEstatal['tipo_mov'],
'owner_id' => $owner->id,
]);
// Crear vehículo con datos del Padrón Estatal (fuente primaria)
$vehicle = Vehicle::create(array_merge(
$vehicleDataEstatal,
['owner_id' => $owner->id]
));
// Asignar Tag al vehículo
$tag->markAsAssigned($vehicle->id, $folio);
@ -385,7 +362,8 @@ public function searchRecord(Request $request)
'tag:id,folio,tag_number,status_id,module_id,package_id',
'tag.status:id,code,name',
'tag.module:id,name',
'tag.package:id,lot,box_number'
'tag.package:id,lot,box_number',
'performedBy:id,name',
])->orderBy('created_at', 'DESC');
},
])->orderBy('id', 'ASC');
@ -454,52 +432,40 @@ public function searchRecord(Request $request)
// Transformación de datos
$paginatedRecords->getCollection()->transform(function ($record) {
$latestLog = $record->vehicle->vehicleTagLogs->first();
// Construir historial completo de tags
$tagsHistory = [];
$order = 1;
$vehicleLogs = $record->vehicle->vehicleTagLogs->sortBy('created_at');
$processedTags = [];
$firstLog = $vehicleLogs->first(); // primer evento
foreach ($vehicleLogs as $log) {
$tagId = $log->tag_id;
if ($tagId && !in_array($tagId, $processedTags)) {
$processedTags[] = $tagId;
// Historial: todos los logs excepto el primero, uno por evento
$tagsHistory = [];
foreach ($vehicleLogs->skip(1)->values() as $index => $log) {
$tag = $log->tag;
// Buscar fecha de cancelación si existe
$cancelLog = $vehicleLogs
->where('tag_id', $tagId)
->whereIn('action_type', ['cancelacion', 'sustitucion'])
->whereNotNull('cancellation_at')
->first();
$tagsHistory[] = [
'order' => $order++,
'tag_id' => $tagId,
'order' => $index + 1,
'log_id' => $log->id,
'tag_id' => $log->tag_id,
'action_type' => $log->action_type,
'folio' => $tag?->folio,
'tag_number' => $tag?->tag_number,
'box_number' => $tag?->package?->box_number,
'status' => $tag?->status?->code ?? 'unknown',
'assigned_at' => $vehicleLogs->where('tag_id', $tagId)
->whereIn('action_type', ['sustitucion_primera_vez', 'sustitucion'])
->first()?->created_at,
'cancelled_at' => $cancelLog?->cancellation_at,
'module' => $tag?->module ? ['id' => $tag->module->id, 'name' => $tag->module->name] : null,
'operator' => $log->performedBy ? ['id' => $log->performedBy->id, 'name' => $log->performedBy->name] : null,
'performed_at' => $log->created_at,
'cancelled_at' => $log->cancellation_at,
'is_current' => $tag?->id === $record->vehicle->tag?->id,
];
}
}
return [
'id' => $record->id,
'folio' => $record->folio,
'created_at' => $record->created_at,
// TIPO DE TRÁMITE
'action_type' => $latestLog?->action_type ?? 'sustitución',
'action_date' => $latestLog?->created_at ?? $record->created_at,
// TIPO DE TRÁMITE (siempre el primer evento)
'action_type' => $firstLog?->action_type,
'action_date' => $firstLog?->created_at ?? $record->created_at,
// HISTORIAL DE TAGS
// HISTORIAL DE TRÁMITES
'tags_history' => $tagsHistory,
'total_tags' => count($tagsHistory),

View File

@ -404,46 +404,16 @@ public function pdfSubstitutedTag($recordId)
try {
$record = Record::with(['vehicle.tag'])->findOrFail($recordId);
// Sustitución real: existe un TAG anterior cancelado
$oldTagLog = VehicleTagLog::where('vehicle_id', $record->vehicle_id)
->where('action_type', 'sustitucion')
->whereNotNull('cancellation_at')
->latest()
->first();
// Primera vez: inscripción vía vehicleInscription (action_type puede ser
// 'sustitucion_primera_vez' o 'sustitucion' cuando el vehículo ya existe en REPUVE Nacional)
$primeraVezLog = !$oldTagLog
? VehicleTagLog::where('vehicle_id', $record->vehicle_id)
->whereIn('action_type', ['sustitucion_primera_vez', 'sustitucion'])
->whereNull('cancellation_at')
->with('performedBy')
->latest()
->first()
: null;
if ($primeraVezLog) {
$newTag = $record->vehicle->tag;
if (!$newTag) {
return ApiResponse::NOT_FOUND->response([
'message' => 'No se encontró el TAG asignado al vehículo.',
'record' => $recordId,
]);
}
$substitutionData = $this->substitutionDataFirstTime($newTag, $primeraVezLog, $record->vehicle);
$isFirstTime = true;
$pdfFilename = 'constancia_primera_vez_' . $newTag->folio . '.pdf';
} else {
// Sustitución regular: TAG anterior cancelado
if (!$oldTagLog) {
$debugLogs = VehicleTagLog::where('vehicle_id', $record->vehicle_id)->get(['id', 'action_type', 'cancellation_at', 'vehicle_id', 'tag_id']);
return response()->json([
'debug_record_id' => $recordId,
'debug_vehicle_id' => $record->vehicle_id,
'debug_logs' => $debugLogs,
return ApiResponse::NOT_FOUND->response([
'message' => 'No se encontró sustitución registrada para este vehículo.',
'record' => $recordId,
]);
}
@ -469,13 +439,11 @@ public function pdfSubstitutedTag($recordId)
}
$substitutionData = $this->substitutionData($oldTag);
$isFirstTime = false;
$pdfFilename = 'constancia_sustituida_' . $oldTag->folio . '.pdf';
}
$pdf = Pdf::loadView('pdfs.tag_sustitution', [
'substitution' => $substitutionData,
'is_first_time' => $isFirstTime,
'is_first_time' => false,
])
->setPaper('a4', 'portrait')
->setOptions([
@ -666,28 +634,6 @@ private function substitutionData(Tag $tag)
return $data;
}
private function substitutionDataFirstTime(Tag $newTag, $log, $vehicle)
{
$data = [
'fecha' => $log->created_at->format('d/m/Y'),
'folio' => $newTag->folio ?? '',
'folio_sustituto' => 'N/A',
'id_chip' => 'N/A',
'placa' => $vehicle->placa ?? '',
'niv' => $vehicle->niv ?? '',
'motivo' => 'Sustitución primera vez',
'operador' => $log->performedBy?->full_name ?? 'N/A',
'modulo' => '',
'ubicacion' => '',
];
if ($log->performedBy) {
$this->loadUserModule($log->performedBy, $data);
}
return $data;
}
/**
* Cargar módulo del usuario
*/

View File

@ -141,6 +141,26 @@ public function tagSubstitution(Request $request)
]);
}
// Validar vehículo en PadronEstatal antes de proceder con la sustitución
try {
$datosEstatal = $this->padronEstatalService->getVehiculoByPlaca($vehicle->placa);
$vehicleDataEstatal = $this->padronEstatalService->extraerDatosVehiculo($datosEstatal);
} catch (PadronEstatalException $e) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'No se pudo validar el vehículo en el Padrón Estatal.',
'placa' => $vehicle->placa,
'error' => $e->getMessage(),
]);
}
if ($vehicleDataEstatal['niv'] !== $vehicle->niv) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'El NIV en el Padrón Estatal no coincide con el registrado en el sistema.',
'niv_bd' => $vehicle->niv,
'niv_padron' => $vehicleDataEstatal['niv'],
]);
}
$roboResult = $this->checkIfStolen($vehicle->niv);
// Solo bloquear si explícitamente está marcado como robado

View File

@ -836,9 +836,9 @@ public function parseConsultarVehiculoResponse(string $xmlResponse): array
'uso' => $campos[20] ?? null,
'clase' => $campos[21] ?? null,
'estatus' => $campos[22] ?? null,
'observaciones' => $campos[31] ?? null,
'folio_CI' => $campos[29] ?? null,
'identificador_CI' => $campos[30] ?? null,
'observaciones' => $campos[31] ?? null,
'raw_response' => $contenido,
];
} catch (Exception $e) {

View File

@ -24,6 +24,7 @@
"tightenco/ziggy": "^2.5"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.7",
"fakerphp/faker": "^1.24",
"laravel/pail": "^1.2",
"laravel/pint": "^1.21",

220
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6a0ffe38dfceb5fa19bb55a5017b4461",
"content-hash": "07cc4733e47502e1f40aa0ee2ab0ae52",
"packages": [
{
"name": "barryvdh/laravel-dompdf",
@ -9732,6 +9732,222 @@
}
],
"packages-dev": [
{
"name": "barryvdh/laravel-ide-helper",
"version": "v3.7.0",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
"reference": "ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a",
"reference": "ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a",
"shasum": ""
},
"require": {
"barryvdh/reflection-docblock": "^2.4",
"composer/class-map-generator": "^1.0",
"ext-json": "*",
"illuminate/console": "^11.15 || ^12 || ^13.0",
"illuminate/database": "^11.15 || ^12 || ^13.0",
"illuminate/filesystem": "^11.15 || ^12 || ^13.0",
"illuminate/support": "^11.15 || ^12 || ^13.0",
"php": "^8.2"
},
"require-dev": {
"ext-pdo_sqlite": "*",
"friendsofphp/php-cs-fixer": "^3",
"illuminate/config": "^11.15 || ^12 || ^13.0",
"illuminate/view": "^11.15 || ^12 || ^13.0",
"larastan/larastan": "^3.1",
"mockery/mockery": "^1.4",
"orchestra/testbench": "^9.2 || ^10 || ^11.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5 || ^11.5.3 || ^12.5.12",
"spatie/phpunit-snapshot-assertions": "^4 || ^5",
"vlucas/phpdotenv": "^5"
},
"suggest": {
"illuminate/events": "Required for automatic helper generation (^6|^7|^8|^9|^10|^11)."
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider"
]
},
"branch-alias": {
"dev-master": "3.6-dev"
}
},
"autoload": {
"psr-4": {
"Barryvdh\\LaravelIdeHelper\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.",
"keywords": [
"autocomplete",
"codeintel",
"dev",
"helper",
"ide",
"laravel",
"netbeans",
"phpdoc",
"phpstorm",
"sublime"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-ide-helper/issues",
"source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.7.0"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2026-03-17T14:12:51+00:00"
},
{
"name": "barryvdh/reflection-docblock",
"version": "v2.4.1",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/ReflectionDocBlock.git",
"reference": "4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b",
"reference": "4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^8.5.14|^9"
},
"suggest": {
"dflydev/markdown": "~1.0",
"erusev/parsedown": "~1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.3.x-dev"
}
},
"autoload": {
"psr-0": {
"Barryvdh": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "mike.vanriel@naenius.com"
}
],
"support": {
"source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.4.1"
},
"time": "2026-03-05T20:09:01+00:00"
},
{
"name": "composer/class-map-generator",
"version": "1.7.1",
"source": {
"type": "git",
"url": "https://github.com/composer/class-map-generator.git",
"reference": "8f5fa3cc214230e71f54924bd0197a3bcc705eb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/class-map-generator/zipball/8f5fa3cc214230e71f54924bd0197a3bcc705eb1",
"reference": "8f5fa3cc214230e71f54924bd0197a3bcc705eb1",
"shasum": ""
},
"require": {
"composer/pcre": "^2.1 || ^3.1",
"php": "^7.2 || ^8.0",
"symfony/finder": "^4.4 || ^5.3 || ^6 || ^7 || ^8"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-deprecation-rules": "^1 || ^2",
"phpstan/phpstan-phpunit": "^1 || ^2",
"phpstan/phpstan-strict-rules": "^1.1 || ^2",
"phpunit/phpunit": "^8",
"symfony/filesystem": "^5.4 || ^6 || ^7 || ^8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\ClassMapGenerator\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "https://seld.be"
}
],
"description": "Utilities to scan PHP code and generate class maps.",
"keywords": [
"classmap"
],
"support": {
"issues": "https://github.com/composer/class-map-generator/issues",
"source": "https://github.com/composer/class-map-generator/tree/1.7.1"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
}
],
"time": "2025-12-29T13:15:25+00:00"
},
{
"name": "fakerphp/faker",
"version": "v1.24.1",
@ -12185,5 +12401,5 @@
"php": "^8.3"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}