baseUrl = config('services.repuve_federal.base_url'); $this->roboEndpoint = config('services.repuve_federal.robo_endpoint'); $this->inscripcionEndpoint = config('services.repuve_federal.inscripcion_endpoint'); } /** * Asegurar que las credenciales estén cargadas (lazy loading) */ private function asegurarCargaCredenciales(): void { if ($this->credentialsLoaded) { return; // Ya están cargadas } $this->loadCredentials(); $this->credentialsLoaded = true; } /** * Cargar credenciales desde BD */ private function loadCredentials(): void { try { // Obtener credenciales encriptadas desde BD $encryptedCredentials = Setting::value('repuve_federal_credentials'); if (!$encryptedCredentials) { throw new Exception('Credenciales REPUVE no configuradas en el sistema'); } $credentials = EncryptionHelper::decryptData($encryptedCredentials); if (!$credentials || !isset($credentials['username'], $credentials['password'])) { throw new Exception('Error al desencriptar credenciales REPUVE'); } $this->username = $credentials['username']; $this->password = $credentials['password']; Log::channel('repuve_nacional')->info('RepuveService: Credenciales cargadas correctamente desde BD'); } catch (Exception $e) { Log::channel('repuve_nacional')->error('RepuveService: Error al cargar credenciales', [ 'error' => $e->getMessage() ]); throw new Exception('No se pudieron cargar las credenciales REPUVE: ' . $e->getMessage()); } } /** * Ejecuta una solicitud cURL con logging completo de conexión, datos enviados, * tiempo de respuesta y respuesta recibida. * * @return array{response: string|false, http_code: int, curl_error: string, elapsed_ms: float} */ private function ejecutarSolicitudSoap(\CurlHandle $ch, string $operacion, string $url, string $soapBody, array $contexto = []): array { Log::channel('repuve_nacional')->info("REPUVE Nacional [{$operacion}]: Enviando solicitud al servidor", array_merge([ 'url' => $url, 'soap_body' => $soapBody, ], $contexto)); $inicio = microtime(true); $response = curl_exec($ch); $elapsedMs = round((microtime(true) - $inicio) * 1000, 2); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); if ($curlError) { Log::channel('repuve_nacional')->error("REPUVE Nacional [{$operacion}]: Sin conexión con el servidor", array_merge([ 'url' => $url, 'curl_error' => $curlError, 'lapso_ms' => $elapsedMs, ], $contexto)); } else { Log::channel('repuve_nacional')->info("REPUVE Nacional [{$operacion}]: Respuesta recibida", array_merge([ 'url' => $url, 'http_code' => $httpCode, 'lapso_ms' => $elapsedMs, 'response_length' => strlen($response ?: ''), 'response' => $response, ], $contexto)); } return [ 'response' => $response, 'http_code' => $httpCode, 'curl_error' => $curlError, 'lapso_ms' => $elapsedMs, ]; } public function consultarPadron(string $niv) { $this->asegurarCargaCredenciales(); $url = $this->baseUrl . $this->roboEndpoint; // 8 posiciones: [0]=NIV, [1]=vacío, [2]=placa, [3-7]=vacíos $campos = array_fill(0, 8, ''); $campos[0] = $niv; $arg2 = implode('|', $campos); $soapBody = << {$this->username} {$this->password} {$arg2} XML; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: text/xml; charset=utf-8', 'SOAPAction: "doConsPadron"', 'Content-Length: ' . strlen($soapBody), ]); try { $result = $this->ejecutarSolicitudSoap($ch, 'doConsPadron', $url, $soapBody, ['niv' => $niv]); $response = $result['response']; $httpCode = $result['http_code']; $error = $result['curl_error']; if ($error) { throw new Exception("Error en la petición SOAP: {$error}"); } if ($httpCode !== 200) { throw new Exception("Error al consultar REPUVE: Código HTTP {$httpCode}"); } return $this->parseVehicleResponse($response, $niv); } finally { unset($ch); } } private function parseVehicleResponse(string $soapResponse, string $niv) { preg_match('/(.*?)<\/return>/s', $soapResponse, $matches); if (!isset($matches[1])) { $errorFromDb = Error::where('code', '108')->first(); return [ 'has_error' => true, 'error_code' => '108', 'error_name' => $errorFromDb?->name, 'error_message' => $errorFromDb?->description ?? 'Error al parsear respuesta', 'timestamp' => now()->toDateTimeString(), 'niv' => $niv, 'repuve_response' => null, ]; } $contenido = trim($matches[1]); // Verificar si hay error de REPUVE Nacional (cualquier formato con ERR o ERROR) if (preg_match('/(ERR|ERROR|err|error):(-?\d+)/i', $contenido, $errorMatch)) { $errorCode = $errorMatch[2]; // Buscar el error completo en la base de datos $errorFromDb = Error::where('code', $errorCode)->first(); return [ 'has_error' => true, 'error_code' => $errorCode, 'error_name' => $errorFromDb?->name, 'error_message' => $errorFromDb?->description ?? "Error código {$errorCode} - no catalogado", 'timestamp' => now()->toDateTimeString(), 'niv' => $niv, 'repuve_response' => $contenido, ]; } // Si empieza con OK:, parsear los datos if (str_starts_with($contenido, 'OK:')) { $datos = str_replace('OK:', '', $contenido); $valores = explode('|', $datos); $campos = [ 'marca', 'submarca', 'tipo_vehiculo', 'fecha_expedicion', 'oficina', 'niv', 'placa', 'motor', 'modelo', 'color', 'version', 'entidad', 'marca_padron', 'submarca_padron', 'tipo_uso_padron', 'tipo_vehiculo_padron', 'estatus_registro', 'aduana', 'nombre_aduana', 'patente', 'pedimento', 'fecha_pedimento', 'clave_importador', 'folio_CI', 'identificador_CI', 'observaciones', ]; $jsonResponse = []; foreach ($campos as $i => $campo) { $jsonResponse[$campo] = $valores[$i] ?? null; } return [ 'has_error' => false, 'error_code' => null, 'error_message' => null, 'timestamp' => now()->toDateTimeString(), 'niv' => $niv, 'repuve_response' => $jsonResponse, ]; } $errorFromDb = Error::where('code', '108')->first(); return [ 'has_error' => true, 'error_code' => '108', 'error_name' => $errorFromDb?->name, 'error_message' => $errorFromDb?->description ?? 'Error al parsear respuesta', 'timestamp' => now()->toDateTimeString(), 'niv' => $niv, 'repuve_response' => null, ]; } public function verificarRobo(?string $niv = null, ?string $placa = null): array { try { $this->asegurarCargaCredenciales(); if (empty($niv) && empty($placa)) { Log::channel('repuve_nacional')->warning('REPUVE verificarRobo: No se proporcionó NIV ni PLACA'); return [ 'es_robado' => false, 'has_error' => true, 'error_message' => 'Debe proporcionar al menos NIV o PLACA para verificar robo', ]; } $url = $this->baseUrl . $this->roboEndpoint; // 8 posiciones: [0]=NIV, [1]=vacío, [2]=placa, [3-7]=vacíos // Ejemplo: LSGHD52H0ND032457||WNU700B||||| $campos = array_fill(0, 8, ''); $campos[0] = $niv ?? ''; $campos[2] = $placa ?? ''; $arg2 = implode('|', $campos); Log::channel('repuve_nacional')->info('REPUVE verificarRobo: Cadena construida', [ 'niv' => $niv, 'placa' => $placa, 'arg2' => $arg2, 'total_pipes' => substr_count($arg2, '|'), ]); $soapBody = << {$this->username} {$this->password} {$arg2} XML; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: text/xml; charset=utf-8', 'SOAPAction: "doConsRepRobo"', 'Content-Length: ' . strlen($soapBody), ]); try { $result = $this->ejecutarSolicitudSoap($ch, 'doConsRepRobo', $url, $soapBody, ['niv' => $niv, 'placa' => $placa]); $response = $result['response']; $httpCode = $result['http_code']; $error = $result['curl_error']; // Si hay error de conexión, retornar error if ($error) { return [ 'is_robado' => false, 'has_error' => true, 'error_message' => 'Error de conexión con el servicio REPUVE', ]; } // Si hay error HTTP, retornar error if ($httpCode !== 200) { return [ 'is_robado' => false, 'has_error' => true, 'error_message' => "Error HTTP {$httpCode} del servicio REPUVE", ]; } // Parsear respuesta $valorBuscado = $niv ?: $placa ?: 'N/A'; $resultado = $this->parseRoboResponse($response, $valorBuscado); // Si hubo error al parsear, loguear pero retornar el resultado completo if ($resultado['has_error'] ?? false) { Log::channel('repuve_nacional')->warning('REPUVE verificarRobo: Error al parsear respuesta', [ 'niv' => $niv, 'placa' => $placa, 'error' => $resultado['error_message'] ?? 'Desconocido', ]); } // Retornar el array completo con toda la información return $resultado; } finally { unset($ch); } } catch (Exception $e) { Log::channel('repuve_nacional')->error('REPUVE verificarRobo: Excepción capturada', [ 'niv' => $niv, 'placa' => $placa, 'exception' => $e->getMessage(), ]); return [ 'es_robado' => false, 'has_error' => true, 'error_message' => 'Excepción capturada: ' . $e->getMessage(), ]; } } public function consultarVehiculo(?string $niv = null, ?string $placa = null) { try { $this->asegurarCargaCredenciales(); $url = $this->baseUrl . '/jaxws-consultarpv/ConsultaRpv'; // 8 posiciones: [0]=NIV, [1]=vacío, [2]=placa, [3-7]=vacíos // Ejemplo: LSGHD52H0ND032457||WNU700B||||| $campos = array_fill(0, 8, ''); $campos[0] = $niv ?? ''; $campos[2] = $placa ?? ''; $arg2 = implode('|', $campos); $soapBody = << {$this->username} {$this->password} {$arg2} XML; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: text/xml; charset=utf-8', 'SOAPAction: "doConsRPV"', 'Content-Length: ' . strlen($soapBody), ]); try { $result = $this->ejecutarSolicitudSoap($ch, 'doConsRPV', $url, $soapBody, ['niv' => $niv, 'placa' => $placa]); $response = $result['response']; $httpCode = $result['http_code']; $error = $result['curl_error']; if ($error) { return [ 'success' => false, 'has_error' => true, 'error_message' => 'Error de conexión con el servicio REPUVE', ]; } if ($httpCode !== 200) { return [ 'success' => false, 'has_error' => true, 'error_message' => "Error HTTP {$httpCode} del servicio REPUVE", ]; } // Parsear respuesta $resultado = $this->parseConsultarVehiculoResponse($response); if ($resultado['has_error'] ?? false) { Log::channel('repuve_nacional')->warning('REPUVE consultarVehiculo: Error al parsear', [ 'niv' => $niv, 'placa' => $placa, 'error' => $resultado['error_message'] ?? 'Desconocido', ]); } return $resultado; } finally { unset($ch); } } catch (Exception $e) { Log::channel('repuve_nacional')->error('REPUVE consultarVehiculo: Excepción', [ 'niv' => $niv, 'placa' => $placa, 'exception' => $e->getMessage(), ]); return [ 'success' => false, 'has_error' => true, 'error_message' => 'Excepción: ' . $e->getMessage(), ]; } } public function inscribirVehiculo(array $datos) { $this->asegurarCargaCredenciales(); $url = $this->baseUrl . $this->inscripcionEndpoint; $arg2 = implode('|', [ $datos['ent_fed'] ?? '', // 1. Entidad federativa $datos['ofcexp'] ?? '', // 2. Oficina expedición $datos['fechaexp'] ?? '', // 3. Fecha expedición $datos['placa'] ?? '', // 4. Placa $datos['tarjetacir'] ?? '', // 5. Tarjeta circulación $datos['marca'] ?? '', // 6. Marca $datos['submarca'] ?? '', // 7. Submarca $datos['version'] ?? '', // 8. Versión $datos['clase_veh'] ?? '', // 9. Clase vehículo $datos['tipo_veh'] ?? '', // 10. Tipo vehículo $datos['tipo_uso'] ?? '', // 11. Tipo uso $datos['modelo'] ?? '', // 12. Modelo (año) $datos['color'] ?? '', // 13. Color $datos['motor'] ?? '', // 14. Número motor $datos['niv'] ?? '', // 15. NIV $datos['rfv'] ?? '', // 16. RFV $datos['numptas'] ?? '', // 17. Número puertas $datos['observac'] ?? '', // 18. Observaciones $datos['tipopers'] ?? '', // 19. Tipo persona $datos['curp'] ?? '', // 20. CURP $datos['rfc'] ?? '', // 21. RFC $datos['pasaporte'] ?? '', // 22. Pasaporte $datos['licencia'] ?? '', // 23. Licencia $datos['nombre'] ?? '', // 24. Nombre $datos['ap_paterno'] ?? '', // 25. Apellido paterno $datos['ap_materno'] ?? '', // 26. Apellido materno $datos['ent_fed'] ?? '', // 27. Entidad federativa propietario $datos['munic'] ?? '', // 28. Municipio $datos['callep'] ?? '', // 29. Calle principal $datos['num_ext'] ?? '', // 30. Número exterior $datos['num_int'] ?? '', // 31. Número interior $datos['colonia'] ?? '', // 32. Colonia $datos['cp'] ?? '', // 33. Código postal $datos['cve_vehi'] ?? '', // 34. Clave vehículo $datos['nrpv'] ?? '', // 35. NRPV $datos['fe_act'] ?? '', // 36. Fecha actualización $datos['tipo_mov'] ?? '', // 37. Tipo movimiento $datos['folio_CI'] ?? '', // 38. Folio constancia de inscripción $datos['identificador_CI'] ?? '', // 39. Identificador constancia de inscripción ]); // Construir el cuerpo SOAP $soapBody = << {$this->username} {$this->password} {$arg2} XML; // Configurar cURL $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $soapBody); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: text/xml; charset=utf-8', 'SOAPAction: "inscribe"', 'Content-Length: ' . strlen($soapBody), ]); try { $result = $this->ejecutarSolicitudSoap($ch, 'inscribe', $url, $soapBody, [ 'niv' => $datos['niv'] ?? null, 'placa' => $datos['placa'] ?? null, 'nrpv' => $datos['nrpv'] ?? null, ]); $response = $result['response']; $httpCode = $result['http_code']; $curlError = $result['curl_error']; if ($curlError) { $errorFromDb = Error::where('code', '103')->first(); return [ 'has_error' => true, 'error_code' => '103', 'error_name' => $errorFromDb?->name, 'error_message' => $errorFromDb?->description ?? "Error de conexión: {$curlError}", 'timestamp' => now()->toDateTimeString(), 'http_code' => $httpCode, 'raw_response' => $response, ]; } if ($httpCode !== 200) { $errorFromDb = Error::where('code', '-1')->first(); return [ 'has_error' => true, 'error_code' => '-1', 'error_name' => $errorFromDb?->name, 'error_message' => $errorFromDb?->description ?? "Error interno HTTP {$httpCode}", 'timestamp' => now()->toDateTimeString(), 'http_code' => $httpCode, 'raw_response' => $response, ]; } // Parsear la respuesta return $this->parsearRespuestaInscripcion($response); } finally { unset($ch); } } /** * Parsea la respuesta */ private function parsearRespuestaInscripcion(string $soapResponse) { preg_match('/(.*?)<\/return>/s', $soapResponse, $matches); if (!isset($matches[1])) { $errorFromDb = Error::where('code', '108')->first(); return [ 'has_error' => true, 'error_code' => '108', 'error_name' => $errorFromDb?->name, 'error_message' => $errorFromDb?->description ?? 'Error al parsear respuesta', 'timestamp' => now()->toDateTimeString(), 'raw_response' => $soapResponse, 'repuve_response' => null, ]; } $contenido = trim($matches[1]); // Buscar patrones de error: ERR:, ERROR:, err:, error: if (preg_match('/(ERR|ERROR|err|error):(-?\d+)/i', $contenido, $errorMatch)) { $errorCode = $errorMatch[2]; // Buscar el error completo en la base de datos $errorFromDb = Error::where('code', $errorCode)->first(); if ($errorFromDb) { // Retornar nombre y descripción de la BD return [ 'has_error' => true, 'error_code' => $errorCode, 'error_name' => $errorFromDb->name, 'error_message' => $errorFromDb->description, 'timestamp' => now()->toDateTimeString(), 'raw_response' => $soapResponse, 'repuve_response' => $contenido, ]; } // Si no existe en BD, retornar el código sin descripción return [ 'has_error' => true, 'error_code' => $errorCode, 'error_name' => null, 'error_message' => "Error código {$errorCode} - no catalogado", 'timestamp' => now()->toDateTimeString(), 'raw_response' => $soapResponse, 'repuve_response' => $contenido, ]; } // Si empieza con OK: es éxito if (preg_match('/^OK:/i', $contenido)) { $datos = preg_replace('/^OK:/i', '', $contenido); return [ 'has_error' => false, 'error_code' => null, 'error_message' => null, 'timestamp' => now()->toDateTimeString(), 'raw_response' => $soapResponse, 'repuve_response' => [ 'status' => 'OK', 'data' => $datos, ], ]; } // Si no hay ERR/ERROR y no es OK, asumir que es respuesta exitosa return [ 'has_error' => false, 'error_code' => null, 'error_name' => null, 'error_message' => null, 'timestamp' => now()->toDateTimeString(), 'raw_response' => $soapResponse, 'repuve_response' => [ 'status' => 'OK', 'data' => $contenido, ], ]; } private function parseRoboResponse(string $soapResponse, string $valor): array { // Extraer contenido del tag preg_match('/(.*?)<\/return>/s', $soapResponse, $matches); if (!isset($matches[1])) { Log::channel('repuve_nacional')->error('REPUVE parseRoboResponse: No se encontró tag ', [ 'soap_response' => substr($soapResponse, 0, 500), 'valor' => $valor, ]); return [ 'has_error' => true, 'is_robado' => false, 'error_message' => 'Respuesta SOAP inválida', ]; } $contenido = trim($matches[1]); // Verificar si hay error de REPUVE Nacional (ERR: o ERROR:) if (preg_match('/(ERR|ERROR|err|error):(-?\d+)/i', $contenido, $errorMatch)) { $errorCode = $errorMatch[2]; Log::channel('repuve_nacional')->warning('REPUVE parseRoboResponse: Servicio retornó error', [ 'error_code' => $errorCode, 'contenido' => $contenido, 'valor' => $valor, ]); return [ 'has_error' => true, 'es_robado' => false, 'error_code' => $errorCode, 'error_message' => "Error REPUVE código {$errorCode}", 'raw_response' => $contenido, ]; } // Si empieza con OK:, parsear los datos de robo if (str_starts_with($contenido, 'OK:')) { $datos = str_replace('OK:', '', $contenido); $valores = explode('|', $datos); // 1 = robado, 0 = no robado $indicador = $valores[0] ?? '0'; $isRobado = ($indicador === '1'); $roboData = [ 'has_error' => false, 'es_robado' => $isRobado, 'indicador' => $indicador, 'fecha_robo' => $valores[1] ?? null, 'placa' => $valores[2] ?? null, 'niv' => $valores[3] ?? null, 'autoridad' => $valores[4] ?? null, 'acta' => $valores[5] ?? null, 'denunciante' => $valores[6] ?? null, 'fecha_acta' => $valores[7] ?? null, 'raw_response' => $contenido, ]; // Log importante si está robado if ($isRobado) { Log::channel('repuve_nacional')->warning('REPUVE: Vehículo reportado como ROBADO', [ 'valor_buscado' => $valor, 'niv' => $roboData['niv'], 'placa' => $roboData['placa'], 'autoridad' => $roboData['autoridad'], 'acta' => $roboData['acta'], 'denunciante' => $roboData['denunciante'], ]); } else { Log::channel('repuve_nacional')->info('REPUVE: Vehículo NO reportado como robado', [ 'valor_buscado' => $valor, ]); } return $roboData; } // Si no tiene formato reconocido Log::channel('repuve_nacional')->error('REPUVE parseRoboResponse: Formato desconocido', [ 'contenido' => $contenido, 'valor' => $valor, ]); return [ 'has_error' => true, 'is_robado' => false, 'error_message' => 'Formato de respuesta no reconocido', 'raw_response' => $contenido, ]; } public function parseConsultarVehiculoResponse(string $xmlResponse): array { try { $xml = simplexml_load_string($xmlResponse); if ($xml === false) { return [ 'success' => false, 'has_error' => true, 'error_message' => 'Error al parsear XML', 'raw_response' => $xmlResponse, ]; } $xml->registerXPathNamespace('ns2', 'http://consultaRpv.org/wsdl'); $return = $xml->xpath('//ns2:doConsRPVResponse/return'); if (empty($return)) { return [ 'success' => false, 'has_error' => true, 'error_message' => 'No se encontró elemento return en la respuesta', 'raw_response' => $xmlResponse, ]; } $contenido = trim((string)$return[0]); // Verificar si la respuesta es OK if (!str_starts_with($contenido, 'OK:')) { return [ 'success' => false, 'has_error' => true, 'error_message' => $contenido, 'raw_response' => $xmlResponse, ]; } // Remover "OK:" del inicio $data = substr($contenido, 3); // Separar por | $campos = explode('|', $data); return [ 'success' => true, 'has_error' => false, 'entidad_federativa' => $campos[0] ?? null, 'oficina' => $campos[1] ?? null, 'folio_tarjeta' => $campos[2] ?? null, 'niv' => $campos[3] ?? null, 'fecha_expedicion' => $campos[4] ?? null, 'hora_expedicion' => $campos[5] ?? null, 'procedencia' => $campos[6] ?? null, 'origen' => $campos[7] ?? null, 'clave_vehicular' => $campos[8] ?? null, 'fecha_emplacado' => $campos[9] ?? null, 'municipio' => $campos[10] ?? null, 'serie' => $campos[11] ?? null, 'placa' => $campos[12] ?? null, 'tipo_vehiculo' => $campos[13] ?? null, 'modelo' => $campos[14] ?? null, 'color' => $campos[15] ?? null, 'version' => $campos[16] ?? null, 'entidad_placas' => $campos[17] ?? null, 'marca' => $campos[18] ?? null, 'linea' => $campos[19] ?? null, 'uso' => $campos[20] ?? null, 'clase' => $campos[21] ?? null, 'estatus' => $campos[22] ?? null, 'folio_CI' => $campos[29] ?? null, 'identificador_CI' => $campos[30] ?? null, 'observaciones' => $campos[31] ?? null, 'raw_response' => $contenido, ]; } catch (Exception $e) { return [ 'success' => false, 'has_error' => true, 'error_message' => 'Excepción al parsear: ' . $e->getMessage(), 'raw_response' => $xmlResponse, ]; } } }