From 706625575c3a957d5aefac0515d9f23a4202cc44 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Thu, 2 Apr 2026 16:50:28 -0600 Subject: [PATCH] feat: cambiar tipo de datos de starting_page y ending_page a string en la tabla packages y actualizar validaciones en las solicitudes --- .../Controllers/Repuve/PackageController.php | 36 ++++++-- .../Controllers/Repuve/TagsController.php | 34 ++++--- .../Controllers/Repuve/UpdateController.php | 88 ++++++++++++++++--- .../Requests/Repuve/PackageStoreRequest.php | 10 +-- .../Requests/Repuve/PackageUpdateRequest.php | 10 +-- app/Models/Package.php | 5 +- ...ange_pages_to_string_in_packages_table.php | 24 +++++ 7 files changed, 155 insertions(+), 52 deletions(-) create mode 100644 database/migrations/2026_04_02_000001_change_pages_to_string_in_packages_table.php diff --git a/app/Http/Controllers/Repuve/PackageController.php b/app/Http/Controllers/Repuve/PackageController.php index 75263ae..fefc9fc 100644 --- a/app/Http/Controllers/Repuve/PackageController.php +++ b/app/Http/Controllers/Repuve/PackageController.php @@ -144,18 +144,18 @@ public function store(PackageStoreRequest $request) try { DB::beginTransaction(); - // Verificar folios en el mismo lote antes de crear - $packageIds = Package::where('lot', $request->lot)->pluck('id'); - - $conflicting = Tag::whereIn('package_id', $packageIds) - ->whereRaw('CAST(folio AS UNSIGNED) BETWEEN ? AND ?', [$request->starting_page, $request->ending_page]) + // Verificar folios duplicados globalmente (la restricción tags_folio_unique es global) + $conflicting = Tag::whereRaw('CAST(folio AS UNSIGNED) BETWEEN ? AND ?', [$request->starting_page, $request->ending_page]) ->pluck('folio'); if ($conflicting->isNotEmpty()) { DB::rollBack(); return ApiResponse::UNPROCESSABLE_CONTENT->response([ - 'starting_page' => [ - 'Los folios ' . $conflicting->join(', ') . ' ya están registrados en el lote "' . $request->lot . '".', + 'message' => 'Los folios ingresados ya están registrados.', + 'errors' => [ + 'starting_page' => [ + 'Los folios ' . $conflicting->join(', ') . ' ya existen en el sistema.', + ], ], ]); } @@ -167,9 +167,13 @@ public function store(PackageStoreRequest $request) $statusAvailable = CatalogTagStatus::where('code', 'available')->firstOrFail(); - for ($page = $request->starting_page; $page <= $request->ending_page; $page++) { + $padLength = strlen($request->starting_page); + $numericStart = (int) $request->starting_page; + $numericEnd = (int) $request->ending_page; + + for ($page = $numericStart; $page <= $numericEnd; $page++) { Tag::create([ - 'folio' => $page, + 'folio' => str_pad($page, $padLength, '0', STR_PAD_LEFT), 'tag_number' => null, 'package_id' => $package->id, 'status_id' => $statusAvailable->id, @@ -183,6 +187,20 @@ public function store(PackageStoreRequest $request) 'package' => $package->load('tags'), 'tags_created' => $package->tags()->count(), ]); + } catch (QueryException $e) { + DB::rollBack(); + if ($e->getCode() == 23000 && str_contains($e->getMessage(), 'tags_folio_unique')) { + return ApiResponse::UNPROCESSABLE_CONTENT->response([ + 'message' => 'Uno o más folios del rango ingresado ya existen en el sistema.', + 'errors' => [ + 'starting_page' => ['El rango de folios contiene duplicados. Verifica los valores e intenta de nuevo.'], + ], + ]); + } + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al crear el paquete', + 'error' => $e->getMessage(), + ]); } catch (\Exception $e) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ diff --git a/app/Http/Controllers/Repuve/TagsController.php b/app/Http/Controllers/Repuve/TagsController.php index 836a3c5..7432c26 100644 --- a/app/Http/Controllers/Repuve/TagsController.php +++ b/app/Http/Controllers/Repuve/TagsController.php @@ -137,65 +137,71 @@ public function store(TagStoreRequest $request) // Obtener el paquete $package = Package::findOrFail($validated['package_id']); $folioNumerico = (int) $validated['folio']; + $padLength = strlen($validated['folio']); // Verificar si el folio está fuera del rango actual del paquete $packageUpdated = false; $rangeChanges = []; $missingTags = []; + $packageStartNumeric = (int) $package->starting_page; + $packageEndNumeric = (int) $package->ending_page; + // Caso 1: El folio es MENOR que el starting_page (crear tags intermedios) - if ($folioNumerico < $package->starting_page) { + if ($folioNumerico < $packageStartNumeric) { $rangeChanges['starting_page'] = [ 'old' => $package->starting_page, - 'new' => $folioNumerico, + 'new' => $validated['folio'], ]; // Crear tags intermedios (desde el nuevo folio hasta el starting_page - 1) - for ($i = $folioNumerico + 1; $i < $package->starting_page; $i++) { + for ($i = $folioNumerico + 1; $i < $packageStartNumeric; $i++) { + $folioIntermedio = str_pad($i, $padLength, '0', STR_PAD_LEFT); // Verificar que el tag no exista - $existingTag = Tag::where('folio', $i)->where('package_id', $package->id)->first(); + $existingTag = Tag::where('folio', $folioIntermedio)->where('package_id', $package->id)->first(); if (!$existingTag) { Tag::create([ - 'folio' => $i, + 'folio' => $folioIntermedio, 'tag_number' => null, 'package_id' => $package->id, 'module_id' => null, 'status_id' => $statusAvailable->id, 'vehicle_id' => null, ]); - $missingTags[] = $i; + $missingTags[] = $folioIntermedio; } } - $package->starting_page = $folioNumerico; + $package->starting_page = $validated['folio']; $packageUpdated = true; } // Caso 2: El folio es MAYOR que el ending_page (crear tags intermedios) - if ($folioNumerico > $package->ending_page) { + if ($folioNumerico > $packageEndNumeric) { $rangeChanges['ending_page'] = [ 'old' => $package->ending_page, - 'new' => $folioNumerico, + 'new' => $validated['folio'], ]; // Crear tags intermedios (desde ending_page + 1 hasta el nuevo folio - 1) - for ($i = $package->ending_page + 1; $i < $folioNumerico; $i++) { + for ($i = $packageEndNumeric + 1; $i < $folioNumerico; $i++) { + $folioIntermedio = str_pad($i, $padLength, '0', STR_PAD_LEFT); // Verificar que el tag no exista - $existingTag = Tag::where('folio', $i)->where('package_id', $package->id)->first(); + $existingTag = Tag::where('folio', $folioIntermedio)->where('package_id', $package->id)->first(); if (!$existingTag) { Tag::create([ - 'folio' => $i, + 'folio' => $folioIntermedio, 'tag_number' => null, 'package_id' => $package->id, 'module_id' => null, 'status_id' => $statusAvailable->id, 'vehicle_id' => null, ]); - $missingTags[] = $i; + $missingTags[] = $folioIntermedio; } } - $package->ending_page = $folioNumerico; + $package->ending_page = $validated['folio']; $packageUpdated = true; } diff --git a/app/Http/Controllers/Repuve/UpdateController.php b/app/Http/Controllers/Repuve/UpdateController.php index 0f090e0..7c6e012 100644 --- a/app/Http/Controllers/Repuve/UpdateController.php +++ b/app/Http/Controllers/Repuve/UpdateController.php @@ -284,6 +284,7 @@ public function vehicleUpdate(Request $request) //ACTUALIZAR INFORMACIÓN VEHÍC $request->validate([ 'placa' => 'required|string|max:7', 'telefono' => 'required|string|max:10', + 'vin' => 'nullable|string|max:32' ], [ 'placa.required' => 'La placa es requerida', 'placa.max' => 'La placa no puede exceder 7 caracteres', @@ -292,9 +293,16 @@ public function vehicleUpdate(Request $request) //ACTUALIZAR INFORMACIÓN VEHÍC ]); $placa = $request->input('placa'); + $vin = $request->input('vin'); + // Buscar vehículo por placa o por VIN $vehicle = Vehicle::with(['owner', 'tag.status']) - ->where('placa', $placa) + ->where(function ($query) use ($placa, $vin) { + $query->where('placa', $placa); + if ($vin) { + $query->orWhere('niv', $vin); + } + }) ->first(); if (!$vehicle) { @@ -383,11 +391,11 @@ public function vehicleUpdate(Request $request) //ACTUALIZAR INFORMACIÓN VEHÍC $ownerDataEstatal ); - // Crear Vehicle - $vehicle = Vehicle::create(array_merge( - $vehicleDataEstatal, - ['owner_id' => $owner->id] - )); + // Crear o actualizar Vehicle (updateOrCreate por NIV para manejar cambios de placa) + $vehicle = Vehicle::updateOrCreate( + ['niv' => $vehicleDataEstatal['niv']], + array_merge($vehicleDataEstatal, ['owner_id' => $owner->id]) + ); // Verificar si el Tag ya existe por folio $tag = Tag::where('folio', $folio)->first(); @@ -437,6 +445,14 @@ public function vehicleUpdate(Request $request) //ACTUALIZAR INFORMACIÓN VEHÍC 'module_id' => Auth::user()->module_id ?? null, ]); + // Registrar log de actualización + VehicleTagLog::create([ + 'vehicle_id' => $vehicle->id, + 'tag_id' => $tag->id, + 'action_type' => 'actualizacion', + 'performed_by' => Auth::id(), + ]); + // REPUVE Nacional ProcessRepuveResponse::dispatch($record->id, $datosCompletosRaw); @@ -670,7 +686,7 @@ private function prepararDatosParaInscripcion(string $niv): array return [ 'ent_fed' => $datos['ent_fed'] ?? '', 'ofcexp' => $datos['ofcexp'] ?? '', - 'fechaexp' => $datos['fechaexp'] ?? '', + 'fechaexp' => $this->formatFechaForRepuve($datos['fechaexp'] ?? ''), 'placa' => $datos['placa'] ?? '', 'tarjetacir' => $datos['tarjetacir'] ?? '', 'marca' => $datos['marca'] ?? '', @@ -803,6 +819,8 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF $hasOwnerChanges = false; $hasFolioChange = false; $oldFolio = $record->folio; + $newlyStoredPaths = []; + $pathsToDeleteAfterCommit = []; if ($request->has('vehicle')) { $vehicleData = $request->input('vehicle', []); @@ -1005,8 +1023,8 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF 'path' => $fileToDelete->path, ]; - // Eliminar archivo físico y registro de BD - Storage::disk('public')->delete($fileToDelete->path); + // Diferir eliminación del archivo físico hasta después del commit + $pathsToDeleteAfterCommit[] = $fileToDelete->path; $fileToDelete->delete(); // Si es Evidencia Adicional, renumerar las restantes @@ -1055,6 +1073,9 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF $nameId = $nameIds[$indx] ?? null; if ($nameId === null || $nameId === '') { + foreach ($newlyStoredPaths as $orphanPath) { + Storage::disk('public')->delete($orphanPath); + } DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => "Falta el nombre para el archivo en el índice {$indx}", @@ -1065,6 +1086,9 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF $catalogName = CatalogNameImg::find($nameId); if (!$catalogName) { + foreach ($newlyStoredPaths as $orphanPath) { + Storage::disk('public')->delete($orphanPath); + } DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => "No se encontró el catálogo de nombre con id {$nameId}", @@ -1121,8 +1145,8 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF $oldPath = $existingFile->path; $oldMd5 = $existingFile->md5; - // Eliminar archivo físico viejo - Storage::disk('public')->delete($oldPath); + // Diferir eliminación del archivo viejo hasta después del commit + $pathsToDeleteAfterCommit[] = $oldPath; // Mantener el mismo nombre de archivo pero actualizar extensión si cambió $pathInfo = pathinfo($oldPath); @@ -1131,6 +1155,7 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF // Guardar nuevo archivo con el mismo nombre $path = $file->storeAs($directory, $fileName, 'public'); + $newlyStoredPaths[] = $path; // Actualizar registro existente $existingFile->update([ @@ -1175,6 +1200,7 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF } $path = $file->storeAs("records/{$record->folio}", $fileName, 'public'); + $newlyStoredPaths[] = $path; $fileRecord = File::create([ 'name_id' => $nameId, @@ -1225,7 +1251,7 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF $datosCompletos = [ 'ent_fed' => $owner->ent_fed ?? '', 'ofcexp' => $vehicle->ofcexpedicion ?? '', - 'fechaexp' => $vehicle->fechaexpedicion ?? '', + 'fechaexp' => $this->formatFechaForRepuve($vehicle->fechaexpedicion ?? ''), 'placa' => $vehicle->placa ?? '', 'tarjetacir' => $vehicle->rfv ?? '', 'marca' => $vehicle->marca ?? '', @@ -1266,6 +1292,11 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF DB::commit(); + // Eliminar archivos viejos del disco solo después de commit exitoso + foreach ($pathsToDeleteAfterCommit as $pathToDelete) { + Storage::disk('public')->delete($pathToDelete); + } + // Recargar relaciones para la respuesta $record->load(['vehicle.owner', 'vehicle.tag', 'files', 'error']); @@ -1333,6 +1364,13 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF } catch (Exception $e) { DB::rollBack(); + // Limpiar archivos nuevos del disco que quedaron huérfanos + if (isset($newlyStoredPaths)) { + foreach ($newlyStoredPaths as $orphanPath) { + Storage::disk('public')->delete($orphanPath); + } + } + return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al actualizar el expediente', 'error' => $e->getMessage(), @@ -1354,7 +1392,7 @@ public function resendToRepuve($id) $datosCompletos = [ 'ent_fed' => $owner->ent_fed ?? '', 'ofcexp' => $vehicle->ofcexpedicion ?? '', - 'fechaexp' => $vehicle->fechaexpedicion ?? '', + 'fechaexp' => $this->formatFechaForRepuve($vehicle->fechaexpedicion ?? ''), 'placa' => $vehicle->placa ?? '', 'tarjetacir' => $vehicle->rfv ?? '', 'marca' => $vehicle->marca ?? '', @@ -1484,4 +1522,28 @@ private function moveRecordFiles(int $recordId, string $oldFolio, string $newFol } } + /** + * Normaliza la fecha de expedición para envío a REPUVE. + * Elimina cualquier componente de hora que pueda haber sido agregado + * por el ORM o la base de datos (ej: '2026-04-02 00:00:00' → '2026-04-02'). + */ + private function formatFechaForRepuve(?string $fecha): string + { + if (empty($fecha)) { + return ''; + } + + // Si tiene hora después de la fecha en formato Y-m-d (ej: '2026-04-02 00:00:00') + if (preg_match('/^(\d{4}-\d{2}-\d{2})[\sT]/', $fecha, $matches)) { + return $matches[1]; + } + + // Si tiene hora después de la fecha en formato d/m/Y (ej: '02/04/2026 00:00:00') + if (preg_match('/^(\d{1,2}\/\d{1,2}\/\d{4})[\sT]/', $fecha, $matches)) { + return $matches[1]; + } + + // Ya está limpia, retornar tal cual + return $fecha; + } } diff --git a/app/Http/Requests/Repuve/PackageStoreRequest.php b/app/Http/Requests/Repuve/PackageStoreRequest.php index fede0ca..78ff7ef 100644 --- a/app/Http/Requests/Repuve/PackageStoreRequest.php +++ b/app/Http/Requests/Repuve/PackageStoreRequest.php @@ -15,8 +15,8 @@ public function rules(): array return [ 'lot' => ['required', 'string', Rule::unique('packages', 'lot')->where(fn($q) => $q->where('box_number', $this->input('box_number')))], 'box_number' => ['required', 'integer'], - 'starting_page' => ['required', 'integer', 'min:1'], - 'ending_page' => ['required', 'integer', 'min:1', 'gte:starting_page'], + 'starting_page' => ['required', 'string', 'regex:/^\d+$/'], + 'ending_page' => ['required', 'string', 'regex:/^\d+$/', 'gte:starting_page'], ]; } @@ -29,12 +29,10 @@ public function messages(): array 'box_number.required' => 'El número de caja es requerido', 'starting_page.required' => 'La página inicial es requerida', - 'starting_page.integer' => 'La página inicial debe ser un número', - 'starting_page.min' => 'La página inicial debe ser al menos 1', + 'starting_page.regex' => 'La página inicial debe ser un número', 'ending_page.required' => 'La página final es requerida', - 'ending_page.integer' => 'La página final debe ser un número', - 'ending_page.min' => 'La página final debe ser al menos 1', + 'ending_page.regex' => 'La página final debe ser un número', 'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial', ]; } diff --git a/app/Http/Requests/Repuve/PackageUpdateRequest.php b/app/Http/Requests/Repuve/PackageUpdateRequest.php index 28fcf4c..983f510 100644 --- a/app/Http/Requests/Repuve/PackageUpdateRequest.php +++ b/app/Http/Requests/Repuve/PackageUpdateRequest.php @@ -16,8 +16,8 @@ public function rules(): array return [ 'lot' => ['sometimes', 'string'], 'box_number' => ['sometimes', 'integer'], - 'starting_page' => ['sometimes', 'integer', 'min:1'], - 'ending_page' => ['sometimes', 'integer', 'min:1', 'gte:starting_page'], + 'starting_page' => ['sometimes', 'string', 'regex:/^\d+$/'], + 'ending_page' => ['sometimes', 'string', 'regex:/^\d+$/', 'gte:starting_page'], ]; } @@ -29,12 +29,10 @@ public function messages(): array 'box_number.required' => 'El número de caja es requerido', 'starting_page.required' => 'La página inicial es requerida', - 'starting_page.integer' => 'La página inicial debe ser un número', - 'starting_page.min' => 'La página inicial debe ser al menos 1', + 'starting_page.regex' => 'La página inicial debe ser un número', 'ending_page.required' => 'La página final es requerida', - 'ending_page.integer' => 'La página final debe ser un número', - 'ending_page.min' => 'La página final debe ser al menos 1', + 'ending_page.regex' => 'La página final debe ser un número', 'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial', ]; } diff --git a/app/Models/Package.php b/app/Models/Package.php index 4f83311..60e24e9 100644 --- a/app/Models/Package.php +++ b/app/Models/Package.php @@ -17,10 +17,7 @@ class Package extends Model 'user_id', ]; - protected $casts = [ - 'starting_page' => 'integer', - 'ending_page' => 'integer', - ]; + protected $casts = []; public function tags() { diff --git a/database/migrations/2026_04_02_000001_change_pages_to_string_in_packages_table.php b/database/migrations/2026_04_02_000001_change_pages_to_string_in_packages_table.php new file mode 100644 index 0000000..24a0647 --- /dev/null +++ b/database/migrations/2026_04_02_000001_change_pages_to_string_in_packages_table.php @@ -0,0 +1,24 @@ +string('starting_page')->change(); + $table->string('ending_page')->change(); + }); + } + + public function down(): void + { + Schema::table('packages', function (Blueprint $table) { + $table->integer('starting_page')->change(); + $table->integer('ending_page')->change(); + }); + } +};