feat: cambiar tipo de datos de starting_page y ending_page a string en la tabla packages y actualizar validaciones en las solicitudes

This commit is contained in:
Juan Felipe Zapata Moreno 2026-04-02 16:50:28 -06:00
parent 73b0e6f1b1
commit 706625575c
7 changed files with 155 additions and 52 deletions

View File

@ -144,18 +144,18 @@ public function store(PackageStoreRequest $request)
try { try {
DB::beginTransaction(); DB::beginTransaction();
// Verificar folios en el mismo lote antes de crear // Verificar folios duplicados globalmente (la restricción tags_folio_unique es global)
$packageIds = Package::where('lot', $request->lot)->pluck('id'); $conflicting = Tag::whereRaw('CAST(folio AS UNSIGNED) BETWEEN ? AND ?', [$request->starting_page, $request->ending_page])
$conflicting = Tag::whereIn('package_id', $packageIds)
->whereRaw('CAST(folio AS UNSIGNED) BETWEEN ? AND ?', [$request->starting_page, $request->ending_page])
->pluck('folio'); ->pluck('folio');
if ($conflicting->isNotEmpty()) { if ($conflicting->isNotEmpty()) {
DB::rollBack(); DB::rollBack();
return ApiResponse::UNPROCESSABLE_CONTENT->response([ return ApiResponse::UNPROCESSABLE_CONTENT->response([
'starting_page' => [ 'message' => 'Los folios ingresados ya están registrados.',
'Los folios ' . $conflicting->join(', ') . ' ya están registrados en el lote "' . $request->lot . '".', '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(); $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([ Tag::create([
'folio' => $page, 'folio' => str_pad($page, $padLength, '0', STR_PAD_LEFT),
'tag_number' => null, 'tag_number' => null,
'package_id' => $package->id, 'package_id' => $package->id,
'status_id' => $statusAvailable->id, 'status_id' => $statusAvailable->id,
@ -183,6 +187,20 @@ public function store(PackageStoreRequest $request)
'package' => $package->load('tags'), 'package' => $package->load('tags'),
'tags_created' => $package->tags()->count(), '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) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
return ApiResponse::INTERNAL_ERROR->response([ return ApiResponse::INTERNAL_ERROR->response([

View File

@ -137,65 +137,71 @@ public function store(TagStoreRequest $request)
// Obtener el paquete // Obtener el paquete
$package = Package::findOrFail($validated['package_id']); $package = Package::findOrFail($validated['package_id']);
$folioNumerico = (int) $validated['folio']; $folioNumerico = (int) $validated['folio'];
$padLength = strlen($validated['folio']);
// Verificar si el folio está fuera del rango actual del paquete // Verificar si el folio está fuera del rango actual del paquete
$packageUpdated = false; $packageUpdated = false;
$rangeChanges = []; $rangeChanges = [];
$missingTags = []; $missingTags = [];
$packageStartNumeric = (int) $package->starting_page;
$packageEndNumeric = (int) $package->ending_page;
// Caso 1: El folio es MENOR que el starting_page (crear tags intermedios) // Caso 1: El folio es MENOR que el starting_page (crear tags intermedios)
if ($folioNumerico < $package->starting_page) { if ($folioNumerico < $packageStartNumeric) {
$rangeChanges['starting_page'] = [ $rangeChanges['starting_page'] = [
'old' => $package->starting_page, 'old' => $package->starting_page,
'new' => $folioNumerico, 'new' => $validated['folio'],
]; ];
// Crear tags intermedios (desde el nuevo folio hasta el starting_page - 1) // 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 // 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) { if (!$existingTag) {
Tag::create([ Tag::create([
'folio' => $i, 'folio' => $folioIntermedio,
'tag_number' => null, 'tag_number' => null,
'package_id' => $package->id, 'package_id' => $package->id,
'module_id' => null, 'module_id' => null,
'status_id' => $statusAvailable->id, 'status_id' => $statusAvailable->id,
'vehicle_id' => null, 'vehicle_id' => null,
]); ]);
$missingTags[] = $i; $missingTags[] = $folioIntermedio;
} }
} }
$package->starting_page = $folioNumerico; $package->starting_page = $validated['folio'];
$packageUpdated = true; $packageUpdated = true;
} }
// Caso 2: El folio es MAYOR que el ending_page (crear tags intermedios) // Caso 2: El folio es MAYOR que el ending_page (crear tags intermedios)
if ($folioNumerico > $package->ending_page) { if ($folioNumerico > $packageEndNumeric) {
$rangeChanges['ending_page'] = [ $rangeChanges['ending_page'] = [
'old' => $package->ending_page, 'old' => $package->ending_page,
'new' => $folioNumerico, 'new' => $validated['folio'],
]; ];
// Crear tags intermedios (desde ending_page + 1 hasta el nuevo folio - 1) // 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 // 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) { if (!$existingTag) {
Tag::create([ Tag::create([
'folio' => $i, 'folio' => $folioIntermedio,
'tag_number' => null, 'tag_number' => null,
'package_id' => $package->id, 'package_id' => $package->id,
'module_id' => null, 'module_id' => null,
'status_id' => $statusAvailable->id, 'status_id' => $statusAvailable->id,
'vehicle_id' => null, 'vehicle_id' => null,
]); ]);
$missingTags[] = $i; $missingTags[] = $folioIntermedio;
} }
} }
$package->ending_page = $folioNumerico; $package->ending_page = $validated['folio'];
$packageUpdated = true; $packageUpdated = true;
} }

View File

@ -284,6 +284,7 @@ public function vehicleUpdate(Request $request) //ACTUALIZAR INFORMACIÓN VEHÍC
$request->validate([ $request->validate([
'placa' => 'required|string|max:7', 'placa' => 'required|string|max:7',
'telefono' => 'required|string|max:10', 'telefono' => 'required|string|max:10',
'vin' => 'nullable|string|max:32'
], [ ], [
'placa.required' => 'La placa es requerida', 'placa.required' => 'La placa es requerida',
'placa.max' => 'La placa no puede exceder 7 caracteres', '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'); $placa = $request->input('placa');
$vin = $request->input('vin');
// Buscar vehículo por placa o por VIN
$vehicle = Vehicle::with(['owner', 'tag.status']) $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(); ->first();
if (!$vehicle) { if (!$vehicle) {
@ -383,11 +391,11 @@ public function vehicleUpdate(Request $request) //ACTUALIZAR INFORMACIÓN VEHÍC
$ownerDataEstatal $ownerDataEstatal
); );
// Crear Vehicle // Crear o actualizar Vehicle (updateOrCreate por NIV para manejar cambios de placa)
$vehicle = Vehicle::create(array_merge( $vehicle = Vehicle::updateOrCreate(
$vehicleDataEstatal, ['niv' => $vehicleDataEstatal['niv']],
['owner_id' => $owner->id] array_merge($vehicleDataEstatal, ['owner_id' => $owner->id])
)); );
// Verificar si el Tag ya existe por folio // Verificar si el Tag ya existe por folio
$tag = Tag::where('folio', $folio)->first(); $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, '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 // REPUVE Nacional
ProcessRepuveResponse::dispatch($record->id, $datosCompletosRaw); ProcessRepuveResponse::dispatch($record->id, $datosCompletosRaw);
@ -670,7 +686,7 @@ private function prepararDatosParaInscripcion(string $niv): array
return [ return [
'ent_fed' => $datos['ent_fed'] ?? '', 'ent_fed' => $datos['ent_fed'] ?? '',
'ofcexp' => $datos['ofcexp'] ?? '', 'ofcexp' => $datos['ofcexp'] ?? '',
'fechaexp' => $datos['fechaexp'] ?? '', 'fechaexp' => $this->formatFechaForRepuve($datos['fechaexp'] ?? ''),
'placa' => $datos['placa'] ?? '', 'placa' => $datos['placa'] ?? '',
'tarjetacir' => $datos['tarjetacir'] ?? '', 'tarjetacir' => $datos['tarjetacir'] ?? '',
'marca' => $datos['marca'] ?? '', 'marca' => $datos['marca'] ?? '',
@ -803,6 +819,8 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF
$hasOwnerChanges = false; $hasOwnerChanges = false;
$hasFolioChange = false; $hasFolioChange = false;
$oldFolio = $record->folio; $oldFolio = $record->folio;
$newlyStoredPaths = [];
$pathsToDeleteAfterCommit = [];
if ($request->has('vehicle')) { if ($request->has('vehicle')) {
$vehicleData = $request->input('vehicle', []); $vehicleData = $request->input('vehicle', []);
@ -1005,8 +1023,8 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF
'path' => $fileToDelete->path, 'path' => $fileToDelete->path,
]; ];
// Eliminar archivo físico y registro de BD // Diferir eliminación del archivo físico hasta después del commit
Storage::disk('public')->delete($fileToDelete->path); $pathsToDeleteAfterCommit[] = $fileToDelete->path;
$fileToDelete->delete(); $fileToDelete->delete();
// Si es Evidencia Adicional, renumerar las restantes // Si es Evidencia Adicional, renumerar las restantes
@ -1055,6 +1073,9 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF
$nameId = $nameIds[$indx] ?? null; $nameId = $nameIds[$indx] ?? null;
if ($nameId === null || $nameId === '') { if ($nameId === null || $nameId === '') {
foreach ($newlyStoredPaths as $orphanPath) {
Storage::disk('public')->delete($orphanPath);
}
DB::rollBack(); DB::rollBack();
return ApiResponse::BAD_REQUEST->response([ return ApiResponse::BAD_REQUEST->response([
'message' => "Falta el nombre para el archivo en el índice {$indx}", '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); $catalogName = CatalogNameImg::find($nameId);
if (!$catalogName) { if (!$catalogName) {
foreach ($newlyStoredPaths as $orphanPath) {
Storage::disk('public')->delete($orphanPath);
}
DB::rollBack(); DB::rollBack();
return ApiResponse::BAD_REQUEST->response([ return ApiResponse::BAD_REQUEST->response([
'message' => "No se encontró el catálogo de nombre con id {$nameId}", '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; $oldPath = $existingFile->path;
$oldMd5 = $existingFile->md5; $oldMd5 = $existingFile->md5;
// Eliminar archivo físico viejo // Diferir eliminación del archivo viejo hasta después del commit
Storage::disk('public')->delete($oldPath); $pathsToDeleteAfterCommit[] = $oldPath;
// Mantener el mismo nombre de archivo pero actualizar extensión si cambió // Mantener el mismo nombre de archivo pero actualizar extensión si cambió
$pathInfo = pathinfo($oldPath); $pathInfo = pathinfo($oldPath);
@ -1131,6 +1155,7 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF
// Guardar nuevo archivo con el mismo nombre // Guardar nuevo archivo con el mismo nombre
$path = $file->storeAs($directory, $fileName, 'public'); $path = $file->storeAs($directory, $fileName, 'public');
$newlyStoredPaths[] = $path;
// Actualizar registro existente // Actualizar registro existente
$existingFile->update([ $existingFile->update([
@ -1175,6 +1200,7 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF
} }
$path = $file->storeAs("records/{$record->folio}", $fileName, 'public'); $path = $file->storeAs("records/{$record->folio}", $fileName, 'public');
$newlyStoredPaths[] = $path;
$fileRecord = File::create([ $fileRecord = File::create([
'name_id' => $nameId, 'name_id' => $nameId,
@ -1225,7 +1251,7 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF
$datosCompletos = [ $datosCompletos = [
'ent_fed' => $owner->ent_fed ?? '', 'ent_fed' => $owner->ent_fed ?? '',
'ofcexp' => $vehicle->ofcexpedicion ?? '', 'ofcexp' => $vehicle->ofcexpedicion ?? '',
'fechaexp' => $vehicle->fechaexpedicion ?? '', 'fechaexp' => $this->formatFechaForRepuve($vehicle->fechaexpedicion ?? ''),
'placa' => $vehicle->placa ?? '', 'placa' => $vehicle->placa ?? '',
'tarjetacir' => $vehicle->rfv ?? '', 'tarjetacir' => $vehicle->rfv ?? '',
'marca' => $vehicle->marca ?? '', 'marca' => $vehicle->marca ?? '',
@ -1266,6 +1292,11 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF
DB::commit(); 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 // Recargar relaciones para la respuesta
$record->load(['vehicle.owner', 'vehicle.tag', 'files', 'error']); $record->load(['vehicle.owner', 'vehicle.tag', 'files', 'error']);
@ -1333,6 +1364,13 @@ public function updateData(VehicleUpdateRequest $request, $id) // ACTUALIZAR INF
} catch (Exception $e) { } catch (Exception $e) {
DB::rollBack(); 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([ return ApiResponse::INTERNAL_ERROR->response([
'message' => 'Error al actualizar el expediente', 'message' => 'Error al actualizar el expediente',
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -1354,7 +1392,7 @@ public function resendToRepuve($id)
$datosCompletos = [ $datosCompletos = [
'ent_fed' => $owner->ent_fed ?? '', 'ent_fed' => $owner->ent_fed ?? '',
'ofcexp' => $vehicle->ofcexpedicion ?? '', 'ofcexp' => $vehicle->ofcexpedicion ?? '',
'fechaexp' => $vehicle->fechaexpedicion ?? '', 'fechaexp' => $this->formatFechaForRepuve($vehicle->fechaexpedicion ?? ''),
'placa' => $vehicle->placa ?? '', 'placa' => $vehicle->placa ?? '',
'tarjetacir' => $vehicle->rfv ?? '', 'tarjetacir' => $vehicle->rfv ?? '',
'marca' => $vehicle->marca ?? '', '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;
}
} }

View File

@ -15,8 +15,8 @@ public function rules(): array
return [ return [
'lot' => ['required', 'string', Rule::unique('packages', 'lot')->where(fn($q) => $q->where('box_number', $this->input('box_number')))], 'lot' => ['required', 'string', Rule::unique('packages', 'lot')->where(fn($q) => $q->where('box_number', $this->input('box_number')))],
'box_number' => ['required', 'integer'], 'box_number' => ['required', 'integer'],
'starting_page' => ['required', 'integer', 'min:1'], 'starting_page' => ['required', 'string', 'regex:/^\d+$/'],
'ending_page' => ['required', 'integer', 'min:1', 'gte:starting_page'], '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', 'box_number.required' => 'El número de caja es requerido',
'starting_page.required' => 'La página inicial es requerida', 'starting_page.required' => 'La página inicial es requerida',
'starting_page.integer' => 'La página inicial debe ser un número', 'starting_page.regex' => 'La página inicial debe ser un número',
'starting_page.min' => 'La página inicial debe ser al menos 1',
'ending_page.required' => 'La página final es requerida', 'ending_page.required' => 'La página final es requerida',
'ending_page.integer' => 'La página final debe ser un número', 'ending_page.regex' => 'La página final debe ser un número',
'ending_page.min' => 'La página final debe ser al menos 1',
'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial', 'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial',
]; ];
} }

View File

@ -16,8 +16,8 @@ public function rules(): array
return [ return [
'lot' => ['sometimes', 'string'], 'lot' => ['sometimes', 'string'],
'box_number' => ['sometimes', 'integer'], 'box_number' => ['sometimes', 'integer'],
'starting_page' => ['sometimes', 'integer', 'min:1'], 'starting_page' => ['sometimes', 'string', 'regex:/^\d+$/'],
'ending_page' => ['sometimes', 'integer', 'min:1', 'gte:starting_page'], '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', 'box_number.required' => 'El número de caja es requerido',
'starting_page.required' => 'La página inicial es requerida', 'starting_page.required' => 'La página inicial es requerida',
'starting_page.integer' => 'La página inicial debe ser un número', 'starting_page.regex' => 'La página inicial debe ser un número',
'starting_page.min' => 'La página inicial debe ser al menos 1',
'ending_page.required' => 'La página final es requerida', 'ending_page.required' => 'La página final es requerida',
'ending_page.integer' => 'La página final debe ser un número', 'ending_page.regex' => 'La página final debe ser un número',
'ending_page.min' => 'La página final debe ser al menos 1',
'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial', 'ending_page.gte' => 'La página final debe ser mayor o igual a la página inicial',
]; ];
} }

View File

@ -17,10 +17,7 @@ class Package extends Model
'user_id', 'user_id',
]; ];
protected $casts = [ protected $casts = [];
'starting_page' => 'integer',
'ending_page' => 'integer',
];
public function tags() public function tags()
{ {

View File

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('packages', function (Blueprint $table) {
$table->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();
});
}
};