- Se agregó autorización basada en permisos en múltiples Requests. - Nuevos Requests para motivos de cancelación y tags con validación y autorización. - Se añadieron métodos de roles al modelo User (isDeveloper, isAdmin, isPrimary). - Se actualizó el acceso a Telescope usando validación por roles. - Mejora en el manejo de excepciones de autorización. - Actualización de RoleSeeder con nuevas convenciones de permisos. - Actualización de dependencias (composer.lock).
396 lines
16 KiB
PHP
396 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Repuve;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\Repuve\CancelConstanciaRequest;
|
|
use App\Models\CatalogCancellationReason;
|
|
use Illuminate\Validation\ValidationException;
|
|
use App\Models\Record;
|
|
use App\Models\Tag;
|
|
use App\Models\TagCancellationLog;
|
|
use App\Models\VehicleTagLog;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
|
use Barryvdh\DomPDF\Facade\Pdf;
|
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
|
|
|
class CancellationController extends Controller implements HasMiddleware
|
|
{
|
|
/**
|
|
* Middleware
|
|
*/
|
|
public static function middleware(): array
|
|
{
|
|
return [
|
|
self::can('cancellations.cancel_constancia', ['cancelarConstancia']),
|
|
self::can('cancellations.cancel_tag_no_asignado', ['cancelarTagNoAsignado']),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Cancelar la constancia/tag
|
|
*/
|
|
public function cancelarConstancia(CancelConstanciaRequest $request, $recordId)
|
|
{
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
// Buscar el expediente con sus relaciones
|
|
$record = Record::with('vehicle.tag.status')->find($recordId);
|
|
|
|
if (!$record) {
|
|
return ApiResponse::NOT_FOUND->response([
|
|
'message' => 'El expediente no existe.',
|
|
'record_id' => $recordId,
|
|
]);
|
|
}
|
|
|
|
$vehicle = $record->vehicle;
|
|
$tag = $vehicle->tag;
|
|
|
|
// Validar que el vehículo tiene un tag asignado
|
|
if (!$tag) {
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'El vehículo no tiene un tag/constancia asignada.'
|
|
]);
|
|
}
|
|
|
|
// Validar que el tag está en estado activo O cancelado (para permitir sustitución posterior)
|
|
if (!$tag->isAssigned() && !$tag->isCancelled()) {
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'El tag debe estar asignado o cancelado. Status actual: ' . $tag->status->name
|
|
]);
|
|
}
|
|
|
|
// Validar que se proporcionen los datos de sustitución
|
|
if (!$request->filled('new_folio') || !$request->filled('new_tag_number')) {
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'Para cancelar la constancia, debe proporcionar: nuevo folio y numero de constancia.',
|
|
'provided' => [
|
|
'new_folio' => $request->filled('new_folio'),
|
|
'new_tag_number' => $request->filled('new_tag_number'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
// Validar que el tag_number tenga exactamente 17 caracteres
|
|
if (strlen($request->new_tag_number) !== 32) {
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'El tag_number debe tener exactamente 32 caracteres',
|
|
'provided_tag_number' => $request->new_tag_number,
|
|
'length' => strlen($request->new_tag_number),
|
|
]);
|
|
}
|
|
|
|
$isSubstitution = true;
|
|
|
|
// Guardar información del tag anterior ANTES de cancelarlo
|
|
$oldTagNumber = $tag->tag_number;
|
|
$oldFolio = $tag->folio;
|
|
|
|
// Crear registro en el log de vehículos
|
|
if ($tag->isAssigned()) {
|
|
$cancellationLog = VehicleTagLog::create([
|
|
'vehicle_id' => $vehicle->id,
|
|
'tag_id' => $tag->id,
|
|
'action_type' => 'cancelacion',
|
|
'cancellation_reason_id' => $request->cancellation_reason_id,
|
|
'cancellation_observations' => $request->cancellation_observations,
|
|
'cancellation_at' => now(),
|
|
'cancelled_by' => Auth::id(),
|
|
'performed_by' => Auth::id(),
|
|
]);
|
|
|
|
// Actualizar estado del tag a 'cancelled'
|
|
$tag->markAsCancelled();
|
|
} else {
|
|
// Si ya estaba cancelado, buscar el último log de cancelación
|
|
$cancellationLog = VehicleTagLog::where('vehicle_id', $vehicle->id)
|
|
->where('tag_id', $tag->id)
|
|
->where('action_type', 'cancelacion')
|
|
->latest()
|
|
->first();
|
|
}
|
|
|
|
$newTag = null;
|
|
$substitutionLog = null;
|
|
|
|
if ($isSubstitution) {
|
|
// Buscar el nuevo tag por folio
|
|
$newTag = Tag::where('folio', $request->new_folio)->first();
|
|
|
|
if (!$newTag) {
|
|
DB::rollBack();
|
|
return ApiResponse::NOT_FOUND->response([
|
|
'message' => 'El tag con el folio proporcionado no existe.',
|
|
'new_folio' => $request->new_folio,
|
|
]);
|
|
}
|
|
|
|
if (!$newTag->isAvailable()) {
|
|
DB::rollBack();
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'El nuevo tag no está disponible para asignación',
|
|
'new_folio' => $request->new_folio,
|
|
'current_status' => $newTag->status->name,
|
|
]);
|
|
}
|
|
|
|
// Asignar tag_number al nuevo tag si no lo tiene
|
|
if (!$newTag->tag_number) {
|
|
// Validar que el tag_number no esté usado por otro tag
|
|
$existingTag = Tag::where('tag_number', $request->new_tag_number)
|
|
->where('id', '!=', $newTag->id)
|
|
->first();
|
|
|
|
if ($existingTag) {
|
|
DB::rollBack();
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'El tag_number ya está asignado a otro tag.',
|
|
'new_tag_number' => $request->new_tag_number,
|
|
'folio_existente' => $existingTag->folio,
|
|
]);
|
|
}
|
|
|
|
$newTag->tag_number = $request->new_tag_number;
|
|
$newTag->save();
|
|
} elseif ($newTag->tag_number !== $request->new_tag_number) {
|
|
DB::rollBack();
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'El tag ya tiene un tag_number diferente asignado.',
|
|
'assigned_tag_number' => $newTag->tag_number,
|
|
'provided_tag_number' => $request->new_tag_number,
|
|
]);
|
|
}
|
|
|
|
// Desasignar el tag viejo para evitar conflicto de unique constraint
|
|
$tag->update(['vehicle_id' => null]);
|
|
|
|
// Asignar el nuevo tag al vehículo (usa el folio del tag encontrado)
|
|
$newTag->markAsAssigned($vehicle->id, $newTag->folio);
|
|
|
|
// Crear log de sustitución
|
|
$substitutionLog = VehicleTagLog::create([
|
|
'vehicle_id' => $vehicle->id,
|
|
'tag_id' => $newTag->id,
|
|
'action_type' => 'sustitucion',
|
|
'cancellation_reason_id' => $request->cancellation_reason_id,
|
|
'cancellation_observations' => 'Tag sustituido. Tag anterior: ' . $oldTagNumber . ' (Folio: ' . $oldFolio . '). Motivo: ' . ($request->cancellation_observations ?? ''),
|
|
'performed_by' => Auth::id(),
|
|
]);
|
|
|
|
// Actualizar el folio del expediente con el folio del nuevo tag
|
|
$record->update(['folio' => $newTag->folio]);
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
$message = $isSubstitution
|
|
? 'Tag cancelado y sustituido exitosamente'
|
|
: 'Constancia cancelada exitosamente';
|
|
|
|
// Agregar alerta si NO hay sustitución
|
|
$alert = null;
|
|
if (!$isSubstitution) {
|
|
$alert = [
|
|
'type' => 'warning',
|
|
'message' => 'El tag ha sido cancelado y necesita sustitución',
|
|
'requires_action' => true,
|
|
'cancellation_date' => $cancellationLog->cancellation_at->format('d/m/Y H:i:s'),
|
|
'cancellation_reason' => $cancellationLog->cancellationReason->name,
|
|
];
|
|
}
|
|
|
|
return ApiResponse::OK->response([
|
|
'message' => $message,
|
|
'is_substitution' => $isSubstitution,
|
|
'alert' => $alert,
|
|
'cancellation' => [
|
|
'id' => $cancellationLog->id,
|
|
'vehicle' => [
|
|
'id' => $vehicle->id,
|
|
'placa' => $vehicle->placa,
|
|
'niv' => $vehicle->niv,
|
|
],
|
|
'old_tag' => [
|
|
'id' => $tag->id,
|
|
'folio' => $oldFolio,
|
|
'tag_number' => $oldTagNumber,
|
|
'new_status' => 'Cancelado',
|
|
],
|
|
'new_tag' => $newTag ? [
|
|
'id' => $newTag->id,
|
|
'folio' => $newTag->folio,
|
|
'tag_number' => $newTag->tag_number,
|
|
'status' => $newTag->status->name,
|
|
] : null,
|
|
'cancellation_reason' => $cancellationLog->cancellationReason->name,
|
|
'cancellation_observations' => $request->cancellation_observations,
|
|
'cancelled_at' => $cancellationLog->cancellation_at->toDateTimeString(),
|
|
'cancelled_by' => Auth::user()->name,
|
|
]
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
|
|
Log::error('Error en cancelarConstancia: ' . $e->getMessage(), [
|
|
'record_id' => $recordId ?? null,
|
|
'cancellation_reason' => $request->cancellation_reason ?? null,
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'Error al cancelar la constancia',
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function cancelarTagNoAsignado(Request $request)
|
|
{
|
|
try {
|
|
$request->validate([
|
|
'folio' => 'required|string|exists:tags,folio',
|
|
'cancellation_reason_id' => 'required|exists:catalog_cancellation_reasons,id',
|
|
'cancellation_observations' => 'nullable|string',
|
|
'module_id' => 'nullable|exists:modules,id',
|
|
]);
|
|
|
|
DB::beginTransaction();
|
|
|
|
$tag = Tag::where('folio', $request->folio)->first();
|
|
|
|
if (!$tag) {
|
|
DB::rollBack();
|
|
return ApiResponse::NOT_FOUND->response([
|
|
'message' => 'No se encontró el tag con el folio proporcionado.',
|
|
'folio' => $request->folio,
|
|
]);
|
|
}
|
|
|
|
// Validar que el tag NO esté asignado
|
|
if ($tag->isAssigned()) {
|
|
DB::rollBack();
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'Este tag está asignado a un vehículo. Usa el endpoint de cancelación de constancia.',
|
|
'tag_status' => $tag->status->name,
|
|
]);
|
|
}
|
|
|
|
// Validar que no esté ya cancelado
|
|
if ($tag->isCancelled()) {
|
|
DB::rollBack();
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'El tag ya está cancelado.',
|
|
]);
|
|
}
|
|
|
|
$observations = $request->cancellation_observations;
|
|
|
|
// Verificar que existe el motivo de cancelación ANTES de crear el log
|
|
$cancellationReason = CatalogCancellationReason::find($request->cancellation_reason_id);
|
|
if (!$cancellationReason) {
|
|
DB::rollBack();
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'El motivo de cancelación no existe.',
|
|
'cancellation_reason_id' => $request->cancellation_reason_id,
|
|
]);
|
|
}
|
|
|
|
$cancellationLog = TagCancellationLog::create([
|
|
'tag_id' => $tag->id,
|
|
'cancellation_reason_id' => $request->cancellation_reason_id,
|
|
'cancellation_observations' => $observations,
|
|
'cancellation_at' => now(),
|
|
'cancelled_by' => Auth::id(),
|
|
]);
|
|
|
|
// Cargar las relaciones necesarias ANTES de usarlas
|
|
$cancellationLog->load(['cancellationReason', 'cancelledBy']);
|
|
|
|
// Actualizar el módulo del tag: usar el enviado o el del usuario autenticado
|
|
$moduleId = $request->filled('module_id') ? $request->module_id : Auth::user()->module_id;
|
|
if ($moduleId) {
|
|
$tag->module_id = $moduleId;
|
|
$tag->save();
|
|
}
|
|
|
|
// Cancelar el tag
|
|
$tag->markAsDamaged();
|
|
|
|
DB::commit();
|
|
|
|
// Recargar el tag con sus relaciones actualizadas
|
|
$tag->load(['status', 'cancellationLogs.cancellationReason', 'cancellationLogs.cancelledBy', 'module']);
|
|
|
|
// Obtener datos de cancelación del último log
|
|
$lastCancellation = $tag->cancellationLogs()
|
|
->with(['cancellationReason', 'cancelledBy'])
|
|
->latest()
|
|
->first();
|
|
|
|
// Validar que existe el log de cancelación
|
|
if (!$lastCancellation) {
|
|
DB::rollBack();
|
|
return ApiResponse::INTERNAL_ERROR->response([
|
|
'message' => 'Error: No se encontró el log de cancelación después de crearlo.',
|
|
'tag_id' => $tag->id,
|
|
]);
|
|
}
|
|
|
|
// Preparar datos para el PDF con validaciones defensivas
|
|
$cancellationData = [
|
|
'fecha' => $lastCancellation->cancellation_at ? $lastCancellation->cancellation_at->format('d/m/Y') : now()->format('d/m/Y'),
|
|
'folio' => $tag->folio ?? '',
|
|
'tag_number' => $tag->tag_number ?? 'N/A',
|
|
'motivo' => ($lastCancellation->cancellationReason && isset($lastCancellation->cancellationReason->name))
|
|
? $lastCancellation->cancellationReason->name
|
|
: 'No especificado',
|
|
'operador' => ($lastCancellation->cancelledBy && isset($lastCancellation->cancelledBy->name))
|
|
? $lastCancellation->cancelledBy->name
|
|
: 'Sistema',
|
|
'modulo' => ($tag->module && isset($tag->module->name)) ? $tag->module->name : 'No especificado',
|
|
'ubicacion' => ($tag->module && isset($tag->module->address)) ? $tag->module->address : 'No especificado',
|
|
];
|
|
|
|
try {
|
|
$pdf = Pdf::loadView('pdfs.tag', [
|
|
'cancellation' => $cancellationData,
|
|
])
|
|
->setPaper('a4', 'portrait')
|
|
->setOptions([
|
|
'defaultFont' => 'sans-serif',
|
|
'isHtml5ParserEnabled' => true,
|
|
'isRemoteEnabled' => true,
|
|
]);
|
|
|
|
return $pdf->stream('constancia_cancelada_' . ($tag->tag_number ?? $tag->folio) . '.pdf');
|
|
} catch (\Exception $e) {
|
|
// Retornar error como JSON
|
|
return ApiResponse::INTERNAL_ERROR->response([
|
|
'message' => 'Tag cancelado pero error al generar PDF',
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
} catch (ValidationException $e) {
|
|
// Errores de validación
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'Error de validación',
|
|
'errors' => $e->errors(),
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return ApiResponse::INTERNAL_ERROR->response([
|
|
'message' => 'Error al cancelar el tag',
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
}
|