diff --git a/Docker/Dev/docker-compose.yml b/Docker/Dev/docker-compose.yml index 250ae6e..7463fd6 100644 --- a/Docker/Dev/docker-compose.yml +++ b/Docker/Dev/docker-compose.yml @@ -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: diff --git a/app/Http/Controllers/Repuve/InscriptionController.php b/app/Http/Controllers/Repuve/InscriptionController.php index 743e465..7345ea8 100644 --- a/app/Http/Controllers/Repuve/InscriptionController.php +++ b/app/Http/Controllers/Repuve/InscriptionController.php @@ -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,40 +432,28 @@ 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; - $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, - 'folio' => $tag?->folio, - 'tag_number' => $tag?->tag_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, - 'is_current' => $tag?->id === $record->vehicle->tag?->id, - ]; - } + // Historial: todos los logs excepto el primero, uno por evento + $tagsHistory = []; + foreach ($vehicleLogs->skip(1)->values() as $index => $log) { + $tag = $log->tag; + $tagsHistory[] = [ + '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', + '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 [ @@ -495,11 +461,11 @@ public function searchRecord(Request $request) '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), diff --git a/app/Http/Controllers/Repuve/RecordController.php b/app/Http/Controllers/Repuve/RecordController.php index c7c06b0..18bdc39 100644 --- a/app/Http/Controllers/Repuve/RecordController.php +++ b/app/Http/Controllers/Repuve/RecordController.php @@ -404,78 +404,46 @@ 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, - ]); - } - - $oldTag = Tag::with([ - 'vehicleTagLogs' => function ($query) { - $query->where('action_type', 'sustitucion') - ->whereNotNull('cancellation_at') - ->with(['cancellationReason', 'cancelledBy', 'vehicle']) - ->latest(); - } - ])->findOrFail($oldTagLog->tag_id); - - $hasSubstitution = $oldTag->vehicleTagLogs() - ->where('action_type', 'sustitucion') - ->whereNotNull('cancellation_at') - ->exists(); - - if (!$hasSubstitution) { - return ApiResponse::BAD_REQUEST->response([ - 'message' => 'El tag no tiene sustitución registrada.', - 'tag' => $oldTag->folio, - ]); - } - - $substitutionData = $this->substitutionData($oldTag); - $isFirstTime = false; - $pdfFilename = 'constancia_sustituida_' . $oldTag->folio . '.pdf'; + if (!$oldTagLog) { + return ApiResponse::NOT_FOUND->response([ + 'message' => 'No se encontró sustitución registrada para este vehículo.', + 'record' => $recordId, + ]); } + $oldTag = Tag::with([ + 'vehicleTagLogs' => function ($query) { + $query->where('action_type', 'sustitucion') + ->whereNotNull('cancellation_at') + ->with(['cancellationReason', 'cancelledBy', 'vehicle']) + ->latest(); + } + ])->findOrFail($oldTagLog->tag_id); + + $hasSubstitution = $oldTag->vehicleTagLogs() + ->where('action_type', 'sustitucion') + ->whereNotNull('cancellation_at') + ->exists(); + + if (!$hasSubstitution) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'El tag no tiene sustitución registrada.', + 'tag' => $oldTag->folio, + ]); + } + + $substitutionData = $this->substitutionData($oldTag); + $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 */ diff --git a/app/Http/Controllers/Repuve/UpdateController.php b/app/Http/Controllers/Repuve/UpdateController.php index 9098ba0..06a9780 100644 --- a/app/Http/Controllers/Repuve/UpdateController.php +++ b/app/Http/Controllers/Repuve/UpdateController.php @@ -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 diff --git a/app/Services/RepuveService.php b/app/Services/RepuveService.php index 4205cb1..7b77ef8 100644 --- a/app/Services/RepuveService.php +++ b/app/Services/RepuveService.php @@ -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) { diff --git a/composer.json b/composer.json index aa0615a..408fef7 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index d4f718a..fea20e8 100644 --- a/composer.lock +++ b/composer.lock @@ -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" }