ADD: Proceso de Cancelación

This commit is contained in:
Juan Felipe Zapata Moreno 2025-10-20 21:47:44 -06:00
parent de0ac7a3aa
commit bf0346dabf
9 changed files with 478 additions and 5 deletions

View File

@ -0,0 +1,319 @@
<?php
namespace App\Http\Controllers\Repuve;
use App\Http\Controllers\Controller;
use App\Http\Requests\Repuve\ConsultaVehiculoRequest;
use App\Http\Requests\Repuve\CancelConstanciaRequest;
use App\Models\Vehicle;
use App\Models\Tag;
use App\Models\VehicleTagLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Notsoweb\ApiResponse\Enums\ApiResponse;
class CancellationController extends Controller
{
/* ===========================================================
* PASO 1: Buscar vehículo para cancelar
* Muestra la tabla con los datos del vehículo encontrado
* ===========================================================
*/
public function searchToCancel(ConsultaVehiculoRequest $request)
{
try {
$searchType = $request->input('search_type');
$searchValue = $request->input('search_value');
// Simulación: consulta a base de datos REPUVE hardcodeada
$vehicleData = $this->getVehicleDataFromRepuve($searchType, $searchValue);
if (!$vehicleData) {
return ApiResponse::NOT_FOUND->response([
'message' => 'No se encontró ningún vehículo con los datos proporcionados en REPUVE.',
'search_type' => $searchType,
'search_value' => $searchValue,
]);
}
// Buscar vehículo en nuestra base de datos local
$vehicle = Vehicle::where('numero_serie', $vehicleData['NO_SERIE'])
->with(['owner'])
->first();
if (!$vehicle) {
return ApiResponse::NOT_FOUND->response([
'message' => 'El vehículo existe en REPUVE pero no está registrado localmente.',
'repuve_data' => $vehicleData,
]);
}
// Buscar tag asignado al vehículo (solo assigned)
$tag = Tag::where('vehicle_id', $vehicle->id)
->where('status', 'assigned')
->first();
// Verificar si ya tiene cancelaciones previas
$cancelaciones = VehicleTagLog::where('vehicle_id', $vehicle->id)
->whereNotNull('cancellation_at')
->count();
$canCancel = !is_null($tag) && $tag->status === 'assigned';
return ApiResponse::OK->response([
'vehicle' => [
'id' => $vehicle->id,
'estatus' => $tag ? $tag->status : 'sin_tag',
'folio' => $vehicle->folio,
'tag' => $tag ? $tag->folio : null,
'niv' => $vehicle->numero_serie,
'tipo' => $vehicle->tipo,
'registro' => $vehicle->created_at->format('d/m/Y'),
'placa' => $vehicle->placa,
'marca' => $vehicle->marca,
'modelo' => $vehicle->modelo,
'color' => $vehicle->color,
],
'tag' => $tag ? [
'id' => $tag->id,
'folio' => $tag->folio,
'status' => $tag->status,
] : null,
'can_cancel' => $canCancel,
'total_cancelaciones_previas' => $cancelaciones,
'message' => $canCancel
? 'Vehículo encontrado. Puede proceder con la cancelación.'
: 'Vehículo encontrado pero no tiene tag asignado o ya está cancelado.',
]);
} catch (\Exception $e) {
Log::error('Error en buscarVehiculoParaCancelar: ' . $e->getMessage(), [
'search_type' => $searchType ?? null,
'search_value' => $searchValue ?? null,
'trace' => $e->getTraceAsString()
]);
return ApiResponse::INTERNAL_SERVER_ERROR->response([
'message' => 'Error al buscar el vehículo',
'error' => $e->getMessage(),
]);
}
}
/* ===========================================================
* Cancelar constancia
* ===========================================================
*/
public function cancelarConstancia(CancelConstanciaRequest $request)
{
// Iniciar transacción
DB::beginTransaction();
try {
$vehicleId = $request->input('vehicle_id');
$tagId = $request->input('tag_id');
$reason = $request->input('cancellation_reason');
$observations = $request->input('cancellation_observations');
// Validar que el vehículo existe
$vehicle = Vehicle::findOrFail($vehicleId);
// Validar que el tag existe
$tag = Tag::findOrFail($tagId);
// Validar que el tag pertenece al vehículo
if ($tag->vehicle_id !== $vehicle->id) {
DB::rollBack();
return ApiResponse::BAD_REQUEST->response([
'message' => 'El tag no está asignado al vehículo especificado',
]);
}
// Validar que el tag esté en estado 'assigned'
if ($tag->status !== 'assigned') {
DB::rollBack();
return ApiResponse::BAD_REQUEST->response([
'message' => "El tag no puede ser cancelado. Estado actual: {$tag->status}",
'current_status' => $tag->status,
]);
}
// Verificar que no exista ya un registro de cancelación para este tag
$existingCancellation = VehicleTagLog::where('tag_id', $tagId)
->whereNotNull('cancellation_at')
->first();
if ($existingCancellation) {
DB::rollBack();
return ApiResponse::BAD_REQUEST->response([
'message' => 'Este tag ya tiene un registro de cancelación',
'cancellation_date' => $existingCancellation->cancellation_at->toDateTimeString(),
]);
}
// Crear registro de cancelación en vehicle_tags_logs
$cancellationLog = VehicleTagLog::create([
'vehicle_id' => $vehicleId,
'tag_id' => $tagId,
'cancellation_at' => now(),
'cancellation_reason' => $reason,
'cancellation_observations' => $observations,
'cancelled_by' => auth()->id(),
]);
// Actualizar estado del tag a 'cancelled'
$tag->update(['status' => 'cancelled']);
// Confirmar transacción
DB::commit();
// Recargar vehículo con tag actualizado
$vehicle->load('owner');
$tag->refresh();
return ApiResponse::OK->response([
'success' => true,
'message' => 'Constancia de inscripción cancelada exitosamente',
'vehicle' => [
'id' => $vehicle->id,
'estatus' => 'cancelada',
'folio' => $vehicle->folio,
'tag' => $tag->folio,
'niv' => $vehicle->numero_serie,
'tipo' => $vehicle->tipo,
'registro' => $vehicle->created_at->format('d/m/Y'),
],
'cancellation' => [
'id' => $cancellationLog->id,
'motivo' => $cancellationLog->cancellation_reason,
'observaciones' => $cancellationLog->cancellation_observations,
'fecha_cancelacion' => $cancellationLog->cancellation_at->format('d/m/Y H:i:s'),
'cancelado_por' => auth()->user()->name ?? 'Sistema',
],
]);
} catch (\Exception $e) {
// Revertir transacción en caso de error
DB::rollBack();
Log::error('Error en cancelarConstancia: ' . $e->getMessage(), [
'vehicle_id' => $request->input('vehicle_id'),
'tag_id' => $request->input('tag_id'),
'trace' => $e->getTraceAsString()
]);
return ApiResponse::INTERNAL_SERVER_ERROR->response([
'message' => 'Error al cancelar la constancia de inscripción',
'error' => $e->getMessage(),
]);
}
}
/**
* Obtiene historial de cancelaciones de un vehículo
*/
public function historialCancelaciones($vehicleId)
{
try {
$vehicle = Vehicle::findOrFail($vehicleId);
$cancelaciones = VehicleTagLog::where('vehicle_id', $vehicleId)
->whereNotNull('cancellation_at')
->with(['tag', 'cancelledBy'])
->orderBy('cancellation_at', 'desc')
->get();
return ApiResponse::OK->response([
'vehicle' => [
'id' => $vehicle->id,
'placa' => $vehicle->placa,
'numero_serie' => $vehicle->numero_serie,
],
'total_cancelaciones' => $cancelaciones->count(),
'cancelaciones' => $cancelaciones->map(function ($log) {
return [
'id' => $log->id,
'tag_folio' => $log->tag->folio ?? 'N/A',
'cancellation_at' => $log->cancellation_at->toDateTimeString(),
'cancellation_reason' => $log->cancellation_reason,
'cancellation_observations' => $log->cancellation_observations,
'cancelled_by' => $log->cancelledBy ? [
'id' => $log->cancelledBy->id,
'name' => $log->cancelledBy->name,
] : null,
];
}),
]);
} catch (\Exception $e) {
Log::error('Error en historialCancelaciones: ' . $e->getMessage(), [
'vehicle_id' => $vehicleId ?? null,
'trace' => $e->getTraceAsString()
]);
return ApiResponse::INTERNAL_SERVER_ERROR->response([
'message' => 'Error al obtener el historial de cancelaciones',
'error' => $e->getMessage(),
]);
}
}
/**
* Simula consulta a base de datos REPUVE
*
*/
private function getVehicleDataFromRepuve(string $searchType, string $searchValue): ?array
{
// Datos hardcodeados del vehículo de ejemplo
$vehicleData = [
"ANIO_PLACA" => "2020",
"PLACA" => "WNU700B",
"NO_SERIE" => "LSGHD52H0ND032457",
"RFC" => "GME111116GJA",
"FOLIO" => "3962243",
"VIGENCIA" => "2025",
"FECHA_IMPRESION" => "10-01-2025",
"QR_HASH" => "Vu5TF4kYsbbltzjDdGQyenKfZoIk2wro34a5Gkh9JVh0CFxfPlrd92YEWK21JF.nLjQNyzKmqRvWYuPiS.kU7A--",
"VALIDO" => true,
"FOLIOTEMP" => false,
"NOMBRE" => "GOLSYSTEMS DE MEXICO S DE RL DE CV",
"NOMBRE2" => "GOLS*MS DXICOE RL*CV",
"MUNICIPIO" => "CENTRO",
"LOCALIDAD" => "VILLAHERMOSA",
"CALLE" => "C BUGAMBILIAS 118 ",
"CALLE2" => "C BU*ILIA*18 ",
"TIPO" => "SEDAN",
"TIPO_SERVICIO" => "PARTICULAR",
"MARCA" => "CHEVROLET G.M.C.",
"LINEA" => "AVEO",
"SUBLINEA" => "PAQ. \"A\" LS",
"MODELO" => 2022,
"NUMERO_SERIE" => "LSGHD52H0ND032457",
"NUMERO_MOTOR" => "H. EN WUHANLL,SGM",
"DESCRIPCION_ORIGEN" => "IMPORTADO",
"COLOR" => "BLANCO",
"CODIGO_POSTAL" => "86179",
"SERIE_FOLIO" => "D3962243",
"SFOLIO" => "3962243"
];
// Normalizar el valor de búsqueda (trim y uppercase)
$searchValue = trim(strtoupper($searchValue));
// Simular búsqueda por tipo
switch ($searchType) {
case 'folio':
return (strtoupper($vehicleData['FOLIO']) === $searchValue) ? $vehicleData : null;
case 'vin':
return (strtoupper($vehicleData['NO_SERIE']) === $searchValue) ? $vehicleData : null;
case 'fecha':
// Para fecha, comparar sin case sensitivity
return (strtoupper($vehicleData['FECHA_IMPRESION']) === $searchValue) ? $vehicleData : null;
default:
return null;
}
}
}

View File

@ -13,6 +13,12 @@
class RepuveController extends Controller
{
/* ===========================================================
* Inscripción de vehículo al REPUVE
* ===========================================================
*/
public function inscripcionVehiculo(VehicleStoreRequest $request)
{
try {
@ -98,7 +104,7 @@ public function inscripcionVehiculo(VehicleStoreRequest $request)
$uploadedFiles = [];
if ($request->hasFile('files')) {
$files = $request->file('files');
$fileNames = $request->input('file_names', []);
$fileNames = $request->input('names', []);
foreach ($files as $index => $file) {
// Generar nombre único

View File

@ -0,0 +1,64 @@
<?php
namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class CancelConstanciaRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'vehicle_id' => 'required|integer|exists:vehicle,id',
'tag_id' => 'required|integer|exists:tags,id',
'cancellation_reason' => 'required|in:fallo_lectura_handheld,cambio_parabrisas,roto_al_pegarlo,extravio,otro',
'cancellation_observations' => 'nullable|string|max:1000',
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'vehicle_id.required' => 'El id del vehículo es obligatorio.',
'vehicle_id.integer' => 'El id del vehículo debe ser un número entero.',
'vehicle_id.exists' => 'El vehículo especificado no existe.',
'tag_id.required' => 'El id del tag es obligatorio.',
'tag_id.integer' => 'El id del tag debe ser un número entero.',
'tag_id.exists' => 'El tag especificado no existe.',
'cancellation_reason.required' => 'El motivo de cancelación es obligatorio.',
'cancellation_reason.in' => 'El motivo de cancelación no es válido. Opciones: fallo_lectura_handheld, cambio_parabrisas, roto_al_pegarlo, extravio, otro.',
'cancellation_observations.string' => 'Las observaciones deben ser texto.',
'cancellation_observations.max' => 'Las observaciones no pueden exceder 1000 caracteres.',
];
}
/**
* Get custom attributes for validator errors.
*/
public function attributes(): array
{
return [
'vehicle_id' => 'id del vehículo',
'tag_id' => 'id del tag',
'cancellation_reason' => 'motivo de cancelación',
'cancellation_observations' => 'observaciones',
];
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Requests\Repuve;
use Illuminate\Foundation\Http\FormRequest;
class ConsultaVehiculoRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'search_type' => 'required|in:folio,vin,fecha',
'search_value' => 'required|string|max:255',
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'search_type.required' => 'El tipo de búsqueda es obligatorio.',
'search_type.in' => 'El tipo de búsqueda debe ser: folio, vin o fecha.',
'search_value.required' => 'El valor de búsqueda es obligatorio.',
'search_value.string' => 'El valor de búsqueda debe ser una cadena de texto.',
'search_value.max' => 'El valor de búsqueda no puede exceder 255 caracteres.',
];
}
/**
* Get custom attributes for validator errors.
*/
public function attributes(): array
{
return [
'search_type' => 'tipo de búsqueda',
'search_value' => 'valor de búsqueda',
];
}
}

View File

@ -14,7 +14,10 @@ class VehicleTagLog extends Model
protected $fillable = [
'vehicle_id',
'tag_id',
'cancellation_reason',
'cancellation_observations',
'cancellation_at',
'cancelled_by',
];
protected function casts(): array
@ -24,4 +27,16 @@ protected function casts(): array
];
}
public function vehicle() {
return $this->belongsTo(Vehicle::class);
}
public function tag() {
return $this->belongsTo(Tag::class);
}
public function cancelledBy() {
return $this->belongsTo(User::class, 'cancelled_by');
}
}

View File

@ -16,6 +16,15 @@ public function up(): void
$table->foreignId('vehicle_id')->constrained('vehicle')->cascadeOnDelete();
$table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete();
$table->timestamp('cancellation_at')->nullable();
$table->enum('cancellation_reason', [
'fallo_lectura_handheld',
'cambio_parabrisas',
'roto_al_pegarlo',
'extravio',
'otro'
])->nullable();
$table->text('cancellation_observations')->nullable();
$table->foreignId('cancelled_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -174,7 +174,7 @@
<!-- Header con logo centrado -->
<div class="header">
<?php
$imagePath = storage_path('app/images/logo-seguridad.png');
$imagePath = resource_path('images/logo-seguridad.png');
$imageData = base64_encode(file_get_contents($imagePath));
$imageSrc = 'data:image/png;base64,' . $imageData;
?>
@ -258,7 +258,8 @@
<p class="signature-role">Propietario</p>
</td>
<td class="signature-cell">
<div class="signature-line">{{ $record->user->full_name }}</div>
<div class="signature-line"></div>
<p class="signature-name">{{ $record->user->full_name }}</p>
<p class="signature-role">Operador</p>
</td>
</tr>

View File

@ -2,6 +2,7 @@
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Repuve\RepuveController;
use App\Http\Controllers\Repuve\RecordController;
use App\Http\Controllers\Repuve\CancellationController;
/**
* Rutas del núcleo de la aplicación.
@ -19,14 +20,20 @@
/** Rutas protegidas (requieren autenticación) */
Route::middleware('auth:api')->group(function() {
// Tus rutas protegidas
// Rutas de inscripción de vehículos
Route::post('inscripcion', [RepuveController::class, 'inscripcionVehiculo']);
Route::post('consulta', [RepuveController::class, 'consultaExpediente']);
// Rutas de expedientes y documentos
Route::get('expediente/{id}/pdf', [RecordController::class, 'generatePdf']);
Route::get('expediente/{recordId}/documentos', [RecordController::class, 'getFile']);
Route::post('expediente/documentos', [RecordController::class, 'uploadFile']);
Route::delete('expediente/documentos/{fileId}', [RecordController::class, 'deleteFile']);
// Rutas de cancelación de constancias
Route::post('cancelacion/buscar', [CancellationController::class, 'searchToCancel']);
Route::post('cancelacion/cancelar', [CancellationController::class, 'cancelarConstancia']);
Route::get('cancelacion/historial/{vehicleId}', [CancellationController::class, 'historialCancelaciones']);
});
/** Rutas públicas */