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->full_name)) ? $lastCancellation->cancelledBy->full_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(), ]); } } }