repuveService = $repuveService; $this->padronEstatalService = $padronEstatalService; } /** * Sustitución de TAG * Proceso: Buscar TAG actual por folio del expediente, cancelarlo y asignar un nuevo TAG al vehículo */ public function tagSubstitution(Request $request) { try { $request->validate([ 'folio' => 'required|string|exists:records,folio', 'new_tag_folio' => 'required|string|exists:tags,folio', 'new_tag_folio' => 'required|string', 'cancellation_reason_id' => 'nullable|exists:catalog_cancellation_reasons,id', 'cancellation_observations' => 'nullable|string|max:500', ]); $folio = $request->input('folio'); $newTagFolio = $request->input('new_tag_folio'); $newTagNumber = $request->input('new_tag_number'); $cancellationReasonId = $request->input('cancellation_reason_id'); $cancellationObservations = $request->input('cancellation_observations'); // Buscar el expediente por folio $record = Record::with([ 'vehicle.tag.status', 'vehicle.owner', ])->where('folio', $folio)->first(); if (!$record) { return ApiResponse::NOT_FOUND->response([ 'message' => 'No se encontró el expediente con el folio proporcionado', 'folio' => $folio, ]); } $vehicle = $record->vehicle; $oldTag = $vehicle->tag; if (!$oldTag) { return ApiResponse::NOT_FOUND->response([ 'message' => 'El vehículo no tiene un TAG asignado actualmente', 'folio' => $folio, ]); } // Verificar que el TAG antiguo esté asignado if (!$oldTag->isAssigned()) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'El TAG actual no está en estado asignado', 'tag_folio' => $oldTag->folio, 'current_status' => $oldTag->status->name, ]); } // Buscar el nuevo TAG por folio $newTag = Tag::with('status') ->where('folio', $newTagFolio) ->first(); if (!$newTag) { return ApiResponse::NOT_FOUND->response([ 'message' => 'El nuevo TAG no existe', 'new_tag_folio' => $newTagFolio, ]); } // Verificar que el nuevo TAG esté disponible if (!$newTag->isAvailable()) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'El nuevo TAG no está disponible para asignación', 'new_tag_folio' => $newTagFolio, 'current_status' => $newTag->status->name, ]); } if (!$newTag->tag_number) { $existingTag = Tag::where('tag_number', $newTagNumber)->first(); if ($existingTag && $existingTag->id !== $newTag->id) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'El tag_number ya está asignado a otro folio.', 'tag_number' => $newTagNumber, 'folio_existente' => $existingTag->folio, ]); } } elseif ($newTag->tag_number !== $newTagNumber) { // Si el tag ya tiene un tag_number diferente return ApiResponse::BAD_REQUEST->response([ 'message' => 'El folio del nuevo TAG ya tiene un tag_number diferente asignado.', 'new_tag_folio' => $newTagFolio, 'tag_number_actual' => $newTag->tag_number, 'tag_number_enviado' => $newTagNumber, ]); } $roboResult = $this->checkIfStolen($vehicle->niv); // Solo bloquear si explícitamente está marcado como robado if ($roboResult['is_robado'] ?? false) { return ApiResponse::FORBIDDEN->response([ 'message' => '¡El vehículo presenta reporte de robo! No se puede actualizar su información.', ]); } DB::beginTransaction(); // Cancelar el TAG anterior $oldTag->markAsCancelled(); // Registrar log de cancelación del TAG anterior VehicleTagLog::create([ 'vehicle_id' => $vehicle->id, 'tag_id' => $oldTag->id, 'new_tag_folio' => $newTagFolio, 'new_tag_number' => $newTagNumber, 'action_type' => 'sustitucion', 'cancellation_reason_id' => $cancellationReasonId, 'cancellation_observations' => $cancellationObservations, 'cancellation_at' => now(), 'cancelled_by' => Auth::id(), 'performed_by' => Auth::id(), ]); // Actualizar el folio del record con el folio del nuevo TAG $record->update([ 'folio' => $newTagFolio ]); if (!$newTag->tag_number) { $newTag->tag_number = $newTagNumber; $newTag->save(); } // Asignar el nuevo TAG al vehículo (usa el nuevo folio) $newTag->markAsAssigned($vehicle->id, $newTagFolio); // 5. Registrar log de asignación del nuevo TAG VehicleTagLog::create([ 'vehicle_id' => $vehicle->id, 'tag_id' => $newTag->id, 'old_tag_folio' => $oldTag->folio, 'action_type' => 'sustitucion', 'performed_by' => Auth::id(), ]); // 6. Enviar a REPUVE Nacional $datosCompletos = $this->prepararDatosParaInscripcion($vehicle->niv); ProcessRepuveResponse::dispatch($record->id, $datosCompletos); DB::commit(); // Recargar relaciones para la respuesta $record->load(['vehicle.tag.status', 'vehicle.owner']); return ApiResponse::OK->response([ 'message' => 'Sustitución de TAG realizada exitosamente', 'substitution_details' => [ 'old_folio' => $folio, 'new_folio' => $newTagFolio, 'old_tag' => [ 'folio' => $oldTag->folio, 'tag_number' => $oldTag->tag_number, 'status' => 'cancelled', ], 'new_tag' => [ 'folio' => $newTag->folio, 'tag_number' => $newTagNumber, 'status' => 'assigned', ], 'performed_by' => Auth::user()->name, 'performed_at' => now()->format('Y-m-d H:i:s'), ], 'record' => $record, ]); } catch (Exception $e) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al realizar la sustitución del TAG', 'error' => $e->getMessage(), ]); } } public function vehicleUpdate(Request $request) { try { $request->validate([ 'placa' => 'required|string|max:20', ], [ 'placa.required' => 'La placa es requerida', 'placa.string' => 'La placa debe ser texto', 'placa.max' => 'La placa no puede exceder 20 caracteres', ]); $placa = $request->input('placa'); $vehicle = Vehicle::with(['owner', 'tag.status']) ->where('placa', $placa) ->first(); if (!$vehicle) { DB::beginTransaction(); // Consultar Padrón Estatal para datos del vehículo y propietario try { $datosCompletosRaw = $this->padronEstatalService->getVehiculoByPlaca($placa); $vehicleDataEstatal = $this->padronEstatalService->extraerDatosVehiculo($datosCompletosRaw); $ownerDataEstatal = $this->padronEstatalService->extraerDatosPropietario($datosCompletosRaw); } catch (Exception $e) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'Error al consultar el Padrón Estatal', 'placa' => $placa, 'error' => $e->getMessage(), ]); } // Validar NIV del Padrón Estatal primero $nivEstatal = $vehicleDataEstatal['niv']; if (!$nivEstatal) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El Padrón Estatal no retornó un NIV válido', 'placa' => $placa, ]); } // Consultar REPUVE Federal para obtener folio y tag_number try { $repuveResponse = $this->repuveService->consultarVehiculo($nivEstatal, null); if ($repuveResponse['has_error']) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'Error al consultar REPUVE Federal', 'placa' => $placa, 'niv' => $nivEstatal, 'error' => $repuveResponse['error_message'] ?? 'Error desconocido', 'error_code' => $repuveResponse['error_code'] ?? null, 'raw_response' => $repuveResponse['raw_response'] ?? null, 'repuve_response_completo' => $repuveResponse, ]); } // Parsear folio y tag_number de la respuesta $rawResponse = $repuveResponse['raw_response']; $campos = explode('|', $rawResponse); $folio = $campos[29] ?? null; $tagNumber = $campos[30] ?? null; if (!$folio || !$tagNumber) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'No se pudo obtener el folio o numero de tag del REPUVE Federal', 'placa' => $placa, 'niv' => $nivEstatal, 'folio_obtenido' => $folio, 'tag_number_obtenido' => $tagNumber, ]); } } catch (Exception $e) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'Error al consultar REPUVE Federal', 'placa' => $placa, 'niv' => $nivEstatal, 'error' => $e->getMessage(), ]); } // Validar RFC del propietario if (!$ownerDataEstatal['rfc']) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El Padrón Estatal no retornó un RFC válido para el propietario', 'placa' => $placa, ]); } // Crear o actualizar Owner $owner = Owner::updateOrCreate( ['rfc' => $ownerDataEstatal['rfc']], $ownerDataEstatal ); // Crear Vehicle $vehicle = Vehicle::create(array_merge( $vehicleDataEstatal, ['owner_id' => $owner->id] )); // Verificar si el Tag ya existe por folio $tag = Tag::where('folio', $folio)->first(); if ($tag) { // Si el tag existe, actualizar con el nuevo vehículo $statusAssigned = CatalogTagStatus::where('code', Tag::STATUS_ASSIGNED)->first(); if (!$statusAssigned) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'No se encontró el estado "disponible" en el catálogo de estados de TAG', ]); } $tag->update([ 'vehicle_id' => $vehicle->id, 'tag_number' => $tagNumber, 'status_id' => $statusAssigned->id, ]); } else { // Si no existe, crear nuevo Tag $statusAssigned = CatalogTagStatus::where('code', Tag::STATUS_ASSIGNED)->first(); if (!$statusAssigned) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'No se encontró el estado "disponible" en el catálogo de estados de TAG', ]); } $tag = Tag::create([ 'folio' => $folio, 'tag_number' => $tagNumber, 'vehicle_id' => $vehicle->id, 'status_id' => $statusAssigned->id, 'module_id' => Auth::user()->module_id ?? null, 'package_id' => null, ]); } // Crear Record $record = Record::create([ 'folio' => $folio, 'vehicle_id' => $vehicle->id, 'user_id' => Auth::id(), 'module_id' => Auth::user()->module_id ?? null, ]); // REPUVE Nacional ProcessRepuveResponse::dispatch($record->id, $datosCompletosRaw); DB::commit(); // Recargar relaciones para la respuesta $record->load(['vehicle.owner', 'vehicle.tag.status', 'files']); return ApiResponse::OK->response([ 'message' => 'Vehículo inscrito exitosamente en el sistema', 'placa' => $placa, 'is_new' => true, 'folio' => $folio, 'tag_number' => $tagNumber, 'vehicle_info' => [ 'niv' => $vehicle->niv, 'marca' => $vehicle->marca, 'linea' => $vehicle->linea, 'modelo' => $vehicle->modelo, ], 'owner_info' => [ 'rfc' => $owner->rfc, 'full_name' => $owner->full_name, ], 'record' => $record, ]); } if (!$vehicle->owner) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'El vehículo no tiene un propietario asociado', 'placa' => $placa, 'vehicle_id' => $vehicle->id, ]); } $tag = $vehicle->tag; if (!$tag) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'El vehículo no tiene un TAG asignado', 'placa' => $placa, 'vehicle_id' => $vehicle->id, ]); } if (!$tag->isAssigned()) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'El TAG del vehículo no está en estado activo', 'placa' => $placa, 'tag_status' => $tag->status->name, 'tag_folio' => $tag->folio, ]); } $record = Record::where('vehicle_id', $vehicle->id)->first(); if (!$record) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'El vehículo no tiene un expediente asociado', 'placa' => $placa, 'vehicle_id' => $vehicle->id, ]); } try { $datosCompletosRaw = $this->padronEstatalService->getVehiculoByPlaca($placa); $vehicleDataEstatal = $this->padronEstatalService->extraerDatosVehiculo($datosCompletosRaw); $ownerDataEstatal = $this->padronEstatalService->extraerDatosPropietario($datosCompletosRaw); } catch (Exception $e) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'Error al consultar el Padrón Estatal', 'placa' => $placa, 'error' => $e->getMessage(), ]); } $nivEstatal = $vehicleDataEstatal['niv']; $placaEstatal = $vehicleDataEstatal['placa']; if ($vehicle->niv !== $nivEstatal) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'El NIV en el Padrón Estatal no coincide con el registrado en el sistema', 'placa' => $placa, 'niv_bd' => $vehicle->niv, 'niv_padron' => $nivEstatal, ]); } // Validar también la placa if (strtoupper($placa) !== strtoupper($placaEstatal)) { return ApiResponse::BAD_REQUEST->response([ 'message' => 'La placa retornada por el Padrón Estatal no coincide con la buscada', 'placa_buscada' => $placa, 'placa_padron' => $placaEstatal, ]); } $roboResult = $this->checkIfStolen($vehicle->niv); // Solo bloquear si explícitamente está marcado como robado if ($roboResult['is_robado'] ?? false) { return ApiResponse::FORBIDDEN->response([ 'message' => '¡El vehículo presenta reporte de robo! No se puede actualizar su información.', ]); } $vehicleChangedFields = $this->detectVehicleChanges($vehicle, $vehicleDataEstatal); $ownerChangedFields = $this->detectOwnerChanges($vehicle->owner, $ownerDataEstatal); $hasVehicleChanges = !empty($vehicleChangedFields); $hasOwnerChanges = !empty($ownerChangedFields); if (!$hasVehicleChanges && !$hasOwnerChanges) { return ApiResponse::OK->response([ 'message' => 'No se detectaron cambios. Los datos ya están actualizados', 'placa' => $placa, 'cambios_hechos' => [ 'vehicle_updated' => false, 'owner_updated' => false, 'owner_changed' => false, ], ]); } DB::beginTransaction(); $oldOwner = $vehicle->owner; $ownerChanged = false; if ($hasVehicleChanges) { $vehicle->update($vehicleDataEstatal); } if ($hasOwnerChanges) { $newRfc = $ownerDataEstatal['rfc']; $oldRfc = $vehicle->owner->rfc; if (!$newRfc) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El Padrón Estatal no retornó un RFC válido para el propietario', 'placa' => $placa, ]); } if ($oldRfc !== $newRfc) { $newOwner = Owner::updateOrCreate( ['rfc' => $newRfc], $ownerDataEstatal ); $vehicle->update(['owner_id' => $newOwner->id]); $ownerChanged = true; } else { $vehicle->owner->update($ownerDataEstatal); } } VehicleTagLog::create([ 'vehicle_id' => $vehicle->id, 'tag_id' => $tag->id, 'action_type' => 'actualizacion', 'performed_by' => Auth::id(), ]); ProcessRepuveResponse::dispatch($record->id, $datosCompletosRaw); DB::commit(); $vehicle->refresh(); $vehicle->load(['owner', 'tag.status']); $record->load(['vehicle.owner', 'vehicle.tag', 'files']); $message = ''; if ($ownerChanged) { $message = 'Propietario del vehículo actualizado (cambio de dueño detectado)'; } elseif ($hasVehicleChanges && $hasOwnerChanges) { $message = 'Datos del vehículo y propietario actualizados exitosamente'; } elseif ($hasVehicleChanges) { $message = 'Datos del vehículo actualizados exitosamente'; } elseif ($hasOwnerChanges) { $message = 'Datos del propietario actualizados exitosamente'; } return ApiResponse::OK->response([ 'message' => $message, 'placa' => $placa, 'cambios_hechos' => [ 'vehicle_updated' => $hasVehicleChanges, 'owner_updated' => $hasOwnerChanges, 'owner_changed' => $ownerChanged, ], 'campos_cambiados' => [ 'vehicle' => $vehicleChangedFields, 'owner' => $ownerChangedFields, ], 'info_propietario' => $ownerChanged ? [ 'old_owner' => [ 'id' => $oldOwner->id, 'rfc' => $oldOwner->rfc, 'full_name' => $oldOwner->full_name, ], 'nuevo_propietario' => [ 'id' => $vehicle->owner->id, 'rfc' => $vehicle->owner->rfc, 'full_name' => $vehicle->owner->full_name, ], ] : null, 'record' => $record, ]); } catch (Exception $e) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al actualizar el vehículo', 'error' => $e->getMessage(), ]); } } private function checkIfStolen(?string $niv = null, ?string $placa = null) { return $this->repuveService->verificarRobo($niv, $placa); } private function prepararDatosParaInscripcion(string $niv): array { $datos = $this->padronEstatalService->getVehiculoByNiv($niv); return [ 'ent_fed' => $datos['ent_fed'] ?? '', 'ofcexp' => $datos['ofcexp'] ?? '', 'fechaexp' => $datos['fechaexp'] ?? '', 'placa' => $datos['placa'] ?? '', 'tarjetacir' => $datos['tarjetacir'] ?? '', 'marca' => $datos['marca'] ?? '', 'submarca' => $datos['submarca'] ?? '', 'version' => $datos['version'] ?? '', 'clase_veh' => $datos['clase_veh'] ?? '', 'tipo_veh' => $datos['tipo_veh'] ?? '', 'tipo_uso' => $datos['tipo_uso'] ?? '', 'modelo' => $datos['modelo'] ?? '', 'color' => $datos['color'] ?? '', 'motor' => $datos['motor'] ?? '', 'niv' => $datos['niv'] ?? '', 'rfv' => $datos['rfv'] ?? '', 'numptas' => $datos['numptas'] ?? '', 'observac' => $datos['observac'] ?? '', 'tipopers' => $datos['tipopers'] ?? '', 'curp' => $datos['curp'] ?? '', 'rfc' => $datos['rfc'] ?? '', 'pasaporte' => $datos['pasaporte'] ?? '', 'licencia' => $datos['licencia'] ?? '', 'nombre' => $datos['nombre'] ?? '', 'ap_paterno' => $datos['ap_paterno'] ?? '', 'ap_materno' => $datos['ap_materno'] ?? '', 'munic' => $datos['munic'] ?? '', 'callep' => $datos['callep'] ?? '', 'num_ext' => $datos['num_ext'] ?? '', 'num_int' => $datos['num_int'] ?? '', 'colonia' => $datos['colonia'] ?? '', 'cp' => $datos['cp'] ?? '', 'cve_vehi' => $datos['cve_vehi'] ?? '', 'nrpv' => $datos['nrpv'] ?? '', 'tipo_mov' => $datos['tipo_mov'] ?? '', ]; } private function detectVehicleChanges($vehicle, array $vehicleDataEstatal) { $changedFields = []; $fieldsToCompare = [ 'placa', 'marca', 'linea', 'sublinea', 'modelo', 'color', 'numero_motor', 'clase_veh', 'tipo_servicio', 'rfv', 'ofcexpedicion', 'tipo_veh', 'numptas', 'observac', 'cve_vehi', 'nrpv', 'tipo_mov' ]; foreach ($fieldsToCompare as $field) { $bdValue = $vehicle->$field ?? null; $estatalValue = $vehicleDataEstatal[$field] ?? null; if (strval($bdValue) !== strval($estatalValue)) { $changedFields[] = [ 'field' => $field, 'old_value' => $bdValue, 'new_value' => $estatalValue, ]; } } return $changedFields; } private function detectOwnerChanges($owner, array $ownerDataEstatal) { $changedFields = []; $fieldsToCompare = [ 'name', 'paternal', 'maternal', 'rfc', 'curp', 'address', 'tipopers', 'pasaporte', 'licencia', 'ent_fed', 'munic', 'callep', 'num_ext', 'num_int', 'colonia', 'cp' ]; foreach ($fieldsToCompare as $field) { $bdValue = $owner->$field ?? null; $estatalValue = $ownerDataEstatal[$field] ?? null; if (strval($bdValue) !== strval($estatalValue)) { $changedFields[] = [ 'field' => $field, 'old_value' => $bdValue, 'new_value' => $estatalValue, ]; } } return $changedFields; } /* --------------------------------------------------------- */ public function updateData(VehicleUpdateRequest $request, $id) { try { $record = Record::with(['vehicle.owner', 'vehicle.tag', 'files', 'error']) ->findOrFail($id); $vehicle = $record->vehicle; $owner = $vehicle->owner; $tag = $vehicle->tag; DB::beginTransaction(); $hasVehicleChanges = false; $hasOwnerChanges = false; $hasFolioChange = false; $oldFolio = $record->folio; if ($request->has('vehicle')) { $vehicleData = $request->input('vehicle', []); $allowedVehicleFields = [ 'placa', 'niv', 'marca', 'linea', 'sublinea', 'modelo', 'color', 'numero_motor', 'clase_veh', 'tipo_servicio', 'rfv', 'ofcexpedicion', 'fechaexpedicion', 'tipo_veh', 'numptas', 'observac', 'cve_vehi', 'nrpv', 'tipo_mov' ]; $vehicleData = array_filter( array_intersect_key($vehicleData, array_flip($allowedVehicleFields)), fn($value) => $value !== null && $value !== '' ); if (!empty($vehicleData)) { $vehicle->update($vehicleData); $hasVehicleChanges = true; } } // Actualizar folio si se envía if ($request->has('folio')) { $newFolio = $request->input('folio'); if ($newFolio && $newFolio !== $record->folio) { // Verificar que el nuevo folio no exista $existingRecord = Record::where('folio', $newFolio)->first(); if ($existingRecord && $existingRecord->id !== $record->id) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El folio ya existe en el sistema y pertenece a un expediente', 'folio' => $newFolio, 'record_id' => $existingRecord->id, ]); } // Mover carpeta de archivos $this->moveRecordFiles($record->id, $oldFolio, $newFolio); // Actualizar folio del record $record->update(['folio' => $newFolio]); $hasFolioChange = true; } } // Actualizar propietario si se envían datos if ($request->has('owner')) { $ownerInput = $request->input('owner', []); $allowedOwnerFields = [ 'name', 'paternal', 'maternal', 'rfc', 'curp', 'address', 'tipopers', 'pasaporte', 'licencia', 'ent_fed', 'munic', 'callep', 'num_ext', 'num_int', 'colonia', 'cp', 'telefono' ]; $ownerData = array_filter( array_intersect_key($ownerInput, array_flip($allowedOwnerFields)), fn($value) => $value !== null && $value !== '' ); if (!empty($ownerData)) { // Si se intenta cambiar el RFC if (isset($ownerData['rfc']) && $ownerData['rfc'] !== $owner->rfc) { // Verificar que el nuevo RFC no exista ya $existingOwner = Owner::where('rfc', $ownerData['rfc'])->first(); if ($existingOwner) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El RFC ya existe en el sistema', 'rfc' => $ownerData['rfc'], 'owner_id' => $existingOwner->id, ]); } // Si no existe, actualizar el RFC del propietario actual $owner->update($ownerData); } else { $owner->update($ownerData); // ← Debe estar AQUÍ } $hasOwnerChanges = true; } } // ACTUALIZAR TAG_NUMBER $hasTagChanges = false; $oldTagNumber = null; $newTagNumber = null; if ($request->has('tag.tag_number')) { $requestedTagNumber = $request->input('tag.tag_number'); // Validar que el tag exista if (!$tag) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El vehículo no tiene un TAG asignado', 'vehicle_id' => $vehicle->id, ]); } // Validar que el tag esté en estado asignado if (!$tag->isAssigned()) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'No se puede actualizar el tag_number porque el TAG no está asignado', 'tag_folio' => $tag->folio, 'tag_status' => $tag->status->name, ]); } // Validar que no esté vacío if (empty(trim($requestedTagNumber))) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El tag_number no puede estar vacío', ]); } // Solo actualizar si es diferente if ($requestedTagNumber !== $tag->tag_number) { // Verificar que el nuevo tag_number no esté duplicado $existingTag = Tag::where('tag_number', $requestedTagNumber) ->where('id', '!=', $tag->id) ->first(); if ($existingTag) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'El tag_number ya está asignado a otro TAG', 'tag_number' => $requestedTagNumber, 'existing_tag_folio' => $existingTag->folio, 'existing_tag_id' => $existingTag->id, ]); } // Guardar el valor antiguo $oldTagNumber = $tag->tag_number; $newTagNumber = $requestedTagNumber; // Actualizar SOLO el tag_number $tag->update(['tag_number' => $requestedTagNumber]); $hasTagChanges = true; } } // Procesar archivos $uploadedFiles = []; $replacedFiles = []; $deletedFiles = []; $skippedFiles = []; // Manejar eliminación de archivos if ($request->has('delete_files')) { $filesToDelete = $request->input('delete_files', []); foreach ($filesToDelete as $fileId) { $fileToDelete = File::where('id', $fileId) ->where('record_id', $record->id) ->first(); if ($fileToDelete) { $catalogName = $fileToDelete->catalogName; $deletedFiles[] = [ 'id' => $fileToDelete->id, 'name_id' => $fileToDelete->name_id, 'name' => $catalogName->name ?? 'Desconocido', 'path' => $fileToDelete->path, ]; // Eliminar archivo físico y registro de BD Storage::disk('public')->delete($fileToDelete->path); $fileToDelete->delete(); // Si es Evidencia Adicional, renumerar las restantes if ($catalogName && $catalogName->name === 'EVIDENCIA ADICIONAL') { $this->renumberEvidenciasAdicionales($record->id, $record->folio); } } } } // Manejar carga/reemplazo de archivos if ($request->hasFile('files')) { $files = $request->file('files'); $nameIds = $request->input('name_id', []); $observations = $request->input('observations', []); // Normalizar arrays if (!is_array($nameIds)) { $nameIds = [$nameIds]; } if (!is_array($observations)) { $observations = [$observations]; } if (!is_array($files)) { $files = [$files]; } if (!empty($nameIds)) { // Obtener IDs únicos para validar $uniqueNameIds = array_unique($nameIds); $validIds = CatalogNameImg::whereIn('id', $uniqueNameIds)->pluck('id')->toArray(); // Verificar que todos los IDs únicos existan en el catálogo if (count($validIds) !== count($uniqueNameIds)) { $invalidIds = array_diff($uniqueNameIds, $validIds); DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => 'Algunos ids del catálogo de nombres no son válidos', 'provided_ids' => $nameIds, 'invalid_ids' => array_values($invalidIds), ]); } } foreach ($files as $indx => $file) { $nameId = $nameIds[$indx] ?? null; if ($nameId === null || $nameId === '') { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => "Falta el nombre para el archivo en el índice {$indx}", ]); } // Obtener el nombre del catálogo $catalogName = CatalogNameImg::find($nameId); if (!$catalogName) { DB::rollBack(); return ApiResponse::BAD_REQUEST->response([ 'message' => "No se encontró el catálogo de nombre con id {$nameId}", ]); } $extension = $file->getClientOriginalExtension(); $isEvidenciaAdicional = $catalogName->name === 'EVIDENCIA ADICIONAL'; // Calcular MD5 antes de guardar para verificar duplicados $md5 = md5_file($file->getRealPath()); // Verificar si el archivo ya existe por MD5 en este expediente $existingByMd5 = File::where('record_id', $record->id) ->where('md5', $md5) ->first(); if ($existingByMd5) { // Archivo duplicado detectado, registrar y omitir $skippedFiles[] = [ 'index' => $indx, 'catalog_name' => $catalogName->name, 'md5' => $md5, 'reason' => 'Archivo duplicado ya existe en el expediente', 'existing_file_id' => $existingByMd5->id, 'existing_file_path' => $existingByMd5->path, ]; continue; // Salta al siguiente archivo sin guardarlo } // Verificar si existe archivo para reemplazar $existingFile = null; if (!$isEvidenciaAdicional) { // Para archivos estándar (FACTURA, INE, etc.), buscar por name_id $existingFile = File::where('record_id', $record->id) ->where('name_id', $nameId) ->first(); } else { // Para EVIDENCIA ADICIONAL con observación, buscar por name_id + observations $observation = $observations[$indx] ?? null; if (!empty($observation)) { $existingFile = File::where('record_id', $record->id) ->where('name_id', $nameId) ->where('observations', $observation) ->first(); } // Si no tiene observación o no se encuentra, se creará nuevo archivo } // Si existe archivo, reemplazarlo if ($existingFile) { $oldPath = $existingFile->path; $oldMd5 = $existingFile->md5; // Eliminar archivo físico viejo Storage::disk('public')->delete($oldPath); // Mantener el mismo nombre de archivo pero actualizar extensión si cambió $pathInfo = pathinfo($oldPath); $fileName = $pathInfo['filename'] . '.' . $extension; $directory = $pathInfo['dirname']; // Guardar nuevo archivo con el mismo nombre $path = $file->storeAs($directory, $fileName, 'public'); // Actualizar registro existente $existingFile->update([ 'path' => $path, 'md5' => $md5, 'observations' => $observations[$indx] ?? $existingFile->observations, ]); $replacedFiles[] = [ 'file_id' => $existingFile->id, 'name' => $catalogName->name, 'old_path' => $oldPath, 'new_path' => $path, 'old_md5' => $oldMd5, 'new_md5' => $md5, 'replaced_by' => 'observation_match', ]; // Agregar a uploadedFiles también para mantener compatibilidad $uploadedFiles[] = [ 'file_id' => $existingFile->id, 'name' => $catalogName->name, 'path' => $path, 'md5' => $md5, 'observations' => $existingFile->observations, 'action' => 'replaced', ]; continue; // Siguiente archivo } // Si no existe, crear nuevo archivo if (!$isEvidenciaAdicional) { $fileName = $catalogName->name . '_' . date('dmY_His') . '.' . $extension; } else { // Si es Evidencia Adicional, contar cuántas ya existen y agregar número $count = File::where('record_id', $record->id) ->where('name_id', $nameId) ->count(); $fileName = 'Evidencia_Adicional_' . ($count + 1) . '_' . date('dmY_His') . '.' . $extension; } $path = $file->storeAs("records/{$record->folio}", $fileName, 'public'); $fileRecord = File::create([ 'name_id' => $nameId, 'path' => $path, 'md5' => $md5, 'record_id' => $record->id, 'observations' => $observations[$indx] ?? null, ]); // Calcular el número de Evidencia Adicional si aplica $displayNumber = null; if ($isEvidenciaAdicional) { $displayNumber = File::where('record_id', $record->id) ->where('name_id', $nameId) ->where('id', '<=', $fileRecord->id) ->count(); } $uploadedFiles[] = [ 'id' => $fileRecord->id, 'name' => $catalogName->name, 'display_name' => $isEvidenciaAdicional ? "Evidencia Adicional {$displayNumber}" : $catalogName->name, 'path' => $fileRecord->path, 'url' => $fileRecord->url, 'observations' => $fileRecord->observations, 'number' => $displayNumber, 'replaced' => isset($existingFile) && $existingFile !== null, ]; } } // Registrar el log de cambios si hubo actualizaciones if ($hasVehicleChanges || $hasOwnerChanges || $hasTagChanges || $hasFolioChange || count($uploadedFiles) > 0 || count($deletedFiles) > 0) { VehicleTagLog::create([ 'vehicle_id' => $vehicle->id, 'tag_id' => $tag->id, 'action_type' => 'actualizacion', 'performed_by' => Auth::id() ]); } // datos para REPUVE Nacional usando datos actuales de la BD if ($hasVehicleChanges || $hasOwnerChanges || $hasTagChanges || $hasFolioChange || count($uploadedFiles) > 0 || count($deletedFiles) > 0) { // Recargar el vehículo y propietario con los datos actualizados $vehicle->refresh(); $owner->refresh(); $datosCompletos = [ 'ent_fed' => $owner->ent_fed ?? '', 'ofcexp' => $vehicle->ofcexpedicion ?? '', 'fechaexp' => $vehicle->fechaexpedicion ?? '', 'placa' => $vehicle->placa ?? '', 'tarjetacir' => $vehicle->rfv ?? '', 'marca' => $vehicle->marca ?? '', 'submarca' => $vehicle->linea ?? '', 'version' => $vehicle->sublinea ?? '', 'clase_veh' => $vehicle->clase_veh ?? '', 'tipo_veh' => $vehicle->tipo_veh ?? '', 'tipo_uso' => $vehicle->tipo_servicio ?? '', 'modelo' => $vehicle->modelo ?? '', 'color' => $vehicle->color ?? '', 'motor' => $vehicle->numero_motor ?? '', 'niv' => $vehicle->niv ?? '', 'rfv' => $vehicle->rfv ?? '', 'numptas' => $vehicle->numptas ?? '', 'observac' => $vehicle->observac ?? '', 'tipopers' => $owner->tipopers ?? '', 'curp' => $owner->curp ?? '', 'rfc' => $owner->rfc ?? '', 'pasaporte' => $owner->pasaporte ?? '', 'licencia' => $owner->licencia ?? '', 'nombre' => $owner->name ?? '', 'ap_paterno' => $owner->paternal ?? '', 'ap_materno' => $owner->maternal ?? '', 'munic' => $owner->munic ?? '', 'callep' => $owner->callep ?? '', 'num_ext' => $owner->num_ext ?? '', 'num_int' => $owner->num_int ?? '', 'colonia' => $owner->colonia ?? '', 'cp' => $owner->cp ?? '', 'cve_vehi' => $vehicle->cve_vehi ?? '', 'nrpv' => $vehicle->nrpv ?? '', 'tipo_mov' => $vehicle->tipo_mov ?? '', ]; // Enviar job a REPUVE Nacional ProcessRepuveResponse::dispatch($record->id, $datosCompletos); } DB::commit(); // Recargar relaciones para la respuesta $record->load(['vehicle.owner', 'vehicle.tag', 'files', 'error']); $message = 'Expediente actualizado exitosamente'; if (!$hasVehicleChanges && !$hasOwnerChanges && !$hasTagChanges && !$hasFolioChange && empty($uploadedFiles) && empty($deletedFiles)) { $message = 'No se detectaron cambios. Los datos ya estaban actualizados.'; if (!empty($skippedFiles)) { $message .= ' Se omitieron ' . count($skippedFiles) . ' archivo(s) duplicado(s).'; } } elseif ($hasTagChanges && !$hasVehicleChanges && !$hasOwnerChanges && !$hasFolioChange) { $message = 'Tag number actualizado exitosamente de "' . ($oldTagNumber ?? 'null') . '" a "' . $newTagNumber . '".'; if (!empty($uploadedFiles) || !empty($deletedFiles)) { $message .= ' Archivos modificados.'; } if (!empty($skippedFiles)) { $message .= ' Se omitieron ' . count($skippedFiles) . ' archivo(s) duplicado(s).'; } } elseif ($hasTagChanges && ($hasVehicleChanges || $hasOwnerChanges)) { $message = 'Datos del vehículo/propietario y tag actualizados exitosamente.'; if (!empty($uploadedFiles) || !empty($deletedFiles)) { $message .= ' Archivos modificados.'; } if (!empty($skippedFiles)) { $message .= ' Se omitieron ' . count($skippedFiles) . ' archivo(s) duplicado(s).'; } } elseif ($hasFolioChange) { $message = 'Folio actualizado exitosamente de "' . $oldFolio . '" a "' . $record->folio . '".'; if ($hasVehicleChanges || $hasOwnerChanges) { $message .= ' Datos del vehículo/propietario actualizados.'; } if (!empty($uploadedFiles) || !empty($deletedFiles)) { $message .= ' Archivos modificados.'; } if (!empty($skippedFiles)) { $message .= ' Se omitieron ' . count($skippedFiles) . ' archivo(s) duplicado(s).'; } } elseif (($hasVehicleChanges || $hasOwnerChanges) && (!empty($uploadedFiles) || !empty($deletedFiles))) { $message = 'Datos del vehículo/propietario y archivos actualizados exitosamente.'; if (!empty($skippedFiles)) { $message .= ' Se omitieron ' . count($skippedFiles) . ' archivo(s) duplicado(s).'; } } elseif (($hasVehicleChanges || $hasOwnerChanges) && empty($uploadedFiles) && empty($deletedFiles)) { $message = 'Datos del vehículo/propietario actualizados exitosamente. No se modificaron archivos.'; if (!empty($skippedFiles)) { $message .= ' Se omitieron ' . count($skippedFiles) . ' archivo(s) duplicado(s).'; } } elseif (!$hasVehicleChanges && !$hasOwnerChanges && (!empty($uploadedFiles) || !empty($deletedFiles))) { $message = 'Archivos modificados exitosamente. No hubo cambios en los datos del vehículo/propietario.'; if (!empty($skippedFiles)) { $message .= ' Se omitieron ' . count($skippedFiles) . ' archivo(s) duplicado(s).'; } } return ApiResponse::OK->response([ 'message' => $message, 'has_error' => false, 'tag' => $hasTagChanges ? [ 'old_tag_number' => $oldTagNumber, 'new_tag_number' => $newTagNumber, 'folio' => $tag->folio, ] : null, 'record' => $record, ]); } catch (Exception $e) { DB::rollBack(); return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al actualizar el expediente', 'error' => $e->getMessage(), ]); } } public function resendToRepuve($id) { try { $record = Record::with('vehicle.owner') ->where('id', $id) ->first(); $vehicle = $record->vehicle; $owner = $vehicle->owner; // Preparar datos actuales de la BD $datosCompletos = [ 'ent_fed' => $owner->ent_fed ?? '', 'ofcexp' => $vehicle->ofcexpedicion ?? '', 'fechaexp' => $vehicle->fechaexpedicion ?? '', 'placa' => $vehicle->placa ?? '', 'tarjetacir' => $vehicle->rfv ?? '', 'marca' => $vehicle->marca ?? '', 'submarca' => $vehicle->linea ?? '', 'version' => $vehicle->sublinea ?? '', 'clase_veh' => $vehicle->clase_veh ?? '', 'tipo_veh' => $vehicle->tipo_veh ?? '', 'tipo_uso' => $vehicle->tipo_servicio ?? '', 'modelo' => $vehicle->modelo ?? '', 'color' => $vehicle->color ?? '', 'motor' => $vehicle->numero_motor ?? '', 'niv' => $vehicle->niv ?? '', 'rfv' => $vehicle->rfv ?? '', 'numptas' => $vehicle->numptas ?? '', 'observac' => $vehicle->observac ?? '', 'tipopers' => $owner->tipopers ?? '', 'curp' => $owner->curp ?? '', 'rfc' => $owner->rfc ?? '', 'pasaporte' => $owner->pasaporte ?? '', 'licencia' => $owner->licencia ?? '', 'nombre' => $owner->name ?? '', 'ap_paterno' => $owner->paternal ?? '', 'ap_materno' => $owner->maternal ?? '', 'munic' => $owner->munic ?? '', 'callep' => $owner->callep ?? '', 'num_ext' => $owner->num_ext ?? '', 'num_int' => $owner->num_int ?? '', 'colonia' => $owner->colonia ?? '', 'cp' => $owner->cp ?? '', 'cve_vehi' => $vehicle->cve_vehi ?? '', 'nrpv' => $vehicle->nrpv ?? '', 'tipo_mov' => $vehicle->tipo_mov ?? '', ]; // reenviar a REPUVE Nacional ProcessRepuveResponse::dispatch($record->id, $datosCompletos); return ApiResponse::OK->response([ 'message' => 'Solicitud de reenvío a REPUVE Nacional procesada exitosamente', 'folio' => $record->folio, 'record_id' => $record->id, ]); } catch (Exception $e) { return ApiResponse::INTERNAL_ERROR->response([ 'message' => 'Error al procesar el reenvío', 'error' => $e->getMessage(), ]); } } /** * Renumerar las Evidencias Adicionales después de eliminar una */ private function renumberEvidenciasAdicionales(int $recordId, string $folio): void { $catalogName = CatalogNameImg::where('name', 'EVIDENCIA ADICIONAL')->first(); if (!$catalogName) { return; } // Obtener todas las evidencias adicionales ordenadas por ID $files = File::where('record_id', $recordId) ->where('name_id', $catalogName->id) ->orderBy('id') ->get(); foreach ($files as $index => $file) { $newNumber = $index + 1; $extension = pathinfo($file->path, PATHINFO_EXTENSION); $oldPath = $file->path; // Nuevo nombre de archivo con numeración actualizada $newFileName = 'Evidencia_Adicional_' . $newNumber . '_' . date('dmY_His') . '.' . $extension; $newPath = "records/{$folio}/" . $newFileName; // Solo renombrar si el path es diferente if ($oldPath !== $newPath) { // Verificar que el archivo antiguo existe antes de intentar moverlo if (Storage::disk('public')->exists($oldPath)) { // Renombrar archivo físico Storage::disk('public')->move($oldPath, $newPath); // Actualizar registro en BD $file->update(['path' => $newPath]); } } } } private function moveRecordFiles(int $recordId, string $oldFolio, string $newFolio): void { $files = File::where('record_id', $recordId)->get(); $oldDirectory = "records/{$oldFolio}"; $newDirectory = "records/{$newFolio}"; // Verificar si existe la carpeta antigua if (!Storage::disk('public')->exists($oldDirectory)) { return; } // Crear la nueva carpeta si no existe if (!Storage::disk('public')->exists($newDirectory)) { Storage::disk('public')->makeDirectory($newDirectory); } // Mover cada archivo foreach ($files as $file) { $oldPath = $file->path; $fileName = basename($oldPath); $newPath = "{$newDirectory}/{$fileName}"; // Verificar que el archivo existe antes de moverlo if (Storage::disk('public')->exists($oldPath)) { // Mover archivo físico Storage::disk('public')->move($oldPath, $newPath); // Actualizar registro en BD $file->update(['path' => $newPath]); } } // Eliminar carpeta antigua si está vacía $remainingFiles = Storage::disk('public')->files($oldDirectory); if (empty($remainingFiles)) { Storage::disk('public')->deleteDirectory($oldDirectory); } } }