import { createHash } from 'crypto' export interface LaximoBrand { brand: string code: string icon: string name: string supportdetailapplicability: boolean supportparameteridentification2: boolean supportquickgroups: boolean supportvinsearch: boolean supportframesearch?: boolean vinexample?: string frameexample?: string features: LaximoFeature[] extensions?: LaximoExtensions } export interface LaximoFeature { name: string example?: string } export interface LaximoExtensions { operations?: LaximoOperation[] } export interface LaximoOperation { description: string kind: string name: string fields: LaximoField[] } export interface LaximoField { description: string example?: string name: string pattern?: string } // Новые интерфейсы для поиска автомобилей export interface LaximoCatalogInfo { brand: string code: string icon: string name: string supportdetailapplicability: boolean supportparameteridentification2: boolean supportquickgroups: boolean supportvinsearch: boolean supportplateidentification?: boolean vinexample?: string plateexample?: string features: LaximoFeature[] permissions: string[] } export interface LaximoWizardStep { allowlistvehicles: boolean automatic: boolean conditionid: string determined: boolean name: string type: string ssd?: string value?: string valueid?: string options: LaximoWizardOption[] } export interface LaximoWizardOption { key: string value: string } export interface LaximoVehicleSearchResult { vehicleid: string name?: string brand: string catalog?: string model: string modification: string year: string bodytype: string engine: string notes?: string ssd?: string transmission?: string date?: string manufactured?: string framecolor?: string trimcolor?: string engine_info?: string engineno?: string market?: string prodRange?: string prodPeriod?: string destinationregion?: string creationregion?: string datefrom?: string dateto?: string modelyearfrom?: string modelyearto?: string options?: string description?: string grade?: string attributes: LaximoVehicleAttribute[] } export interface LaximoVehicleInfo { vehicleid: string name: string ssd: string brand: string catalog: string attributes: LaximoVehicleAttribute[] } export interface LaximoVehicleAttribute { key: string name: string value: string } export interface LaximoQuickGroup { quickgroupid: string name: string link: boolean children?: LaximoQuickGroup[] code?: string imageurl?: string largeimageurl?: string } export interface LaximoQuickDetail { quickgroupid: string name: string units?: LaximoUnit[] } export interface LaximoUnit { unitid: string name: string code?: string description?: string imageurl?: string largeimageurl?: string details?: LaximoDetail[] attributes?: LaximoDetailAttribute[] } export interface LaximoDetail { detailid: string name: string oem: string brand?: string description?: string applicablemodels?: string note?: string amount?: string range?: string codeonimage?: string match?: boolean dateRange?: string ssd?: string attributes?: LaximoDetailAttribute[] } export interface LaximoDetailAttribute { key: string name?: string value: string } export interface LaximoOEMResult { oemNumber: string categories: LaximoOEMCategory[] } export interface LaximoOEMCategory { categoryid: string name: string units: LaximoOEMUnit[] } export interface LaximoOEMUnit { unitid: string name: string code?: string imageurl?: string details: LaximoOEMDetail[] } export interface LaximoOEMDetail { detailid: string name: string oem: string brand?: string amount?: string range?: string attributes?: LaximoDetailAttribute[] } export interface LaximoFulltextSearchResult { searchQuery: string details: LaximoFulltextDetail[] } export interface LaximoFulltextDetail { oem: string name: string brand?: string description?: string } // Интерфейсы для модуля Doc export interface LaximoDocFindOEMResult { details: LaximoDocDetail[] } export interface LaximoDocDetail { detailid: string formattedoem: string manufacturer: string manufacturerid: string name: string oem: string volume?: string weight?: string replacements: LaximoDocReplacement[] } export interface LaximoDocReplacement { type: string way: string replacementid: string rate?: string detail: LaximoDocReplacementDetail } export interface LaximoDocReplacementDetail { detailid: string formattedoem: string manufacturer: string manufacturerid: string name: string oem: string weight?: string icon?: string } export interface LaximoCatalogVehicleResult { catalogCode: string catalogName: string brand: string vehicles: LaximoVehicleSearchResult[] vehicleCount: number } export interface LaximoVehiclesByPartResult { partNumber: string catalogs: LaximoCatalogVehicleResult[] totalVehicles: number } // Дополнительные интерфейсы для работы с деталями узлов export interface LaximoUnitImageMap { unitid: string imageurl?: string largeimageurl?: string coordinates: LaximoImageCoordinate[] } export interface LaximoImageCoordinate { detailid: string codeonimage?: string x: number y: number width: number height: number shape: string } /** * Laximo Doc Service для поиска деталей по артикулу * Использует отдельные данные авторизации для модуля Doc */ class LaximoDocService { // Endpoints для Aftermarket (Doc) модуля согласно WSDL private soap11Url = 'https://aws.laximo.ru/ec.Kito.Aftermarket/services/Catalog.CatalogHttpSoap11Endpoint/' private soap12Url = 'https://aws.laximo.ru/ec.Kito.Aftermarket/services/Catalog.CatalogHttpSoap12Endpoint/' private login = process.env.LAXIMO_DOC_LOGIN || '' private password = process.env.LAXIMO_DOC_PASSWORD || '' constructor() { console.log('🔧 LaximoDocService инициализация:') console.log('📧 Login:', this.login ? `${this.login.substring(0, 3)}***` : 'НЕ ЗАДАН') console.log('🔑 Password:', this.password ? `${this.password.substring(0, 3)}***` : 'НЕ ЗАДАН') console.log('🌐 SOAP11 URL:', this.soap11Url) if (!this.login || !this.password) { console.error('❌ Учетные данные для Doc модуля не настроены!') } } /** * Создает HMAC контрольный код для авторизации */ private createHMAC(command: string): string { if (!this.password) { throw new Error('Doc password is required for HMAC generation') } const combinedString = command + this.password return createHash('md5').update(combinedString).digest('hex') } /** * Создает SOAP 1.1 конверт */ private createSOAP11Envelope(command: string, login: string, hmac: string): string { return ` ${command} ${login} ${hmac} ` } /** * Выполняет SOAP запрос */ private async makeSOAPRequest(url: string, soapEnvelope: string, soapAction: string): Promise { try { console.log('🌐 Doc SOAP Request URL:', url) console.log('📋 Doc SOAP Action:', soapAction) console.log('📄 Doc SOAP Envelope (first 500 chars):', soapEnvelope.substring(0, 500)) const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': soapAction }, body: soapEnvelope }) console.log('📡 Doc Response Status:', response.status) console.log('📡 Doc Response Headers:', Object.fromEntries(response.headers.entries())) if (!response.ok) { const errorText = await response.text() console.log('❌ Doc Error Response Body:', errorText) // Проверяем на ошибку лимита запросов if (response.status === 500 && errorText.includes('E_TOO_MANY_REQUESTS')) { console.log('⚠️ Превышен лимит запросов Laximo API - возвращаем пустой результат') return '' } throw new Error(`HTTP error! status: ${response.status}`) } const responseText = await response.text() console.log('✅ Doc Response received, length:', responseText.length) console.log('📄 Doc Response (first 1000 chars):', responseText.substring(0, 1000)) return responseText } catch (error) { console.error('SOAP request failed:', error) throw error } } /** * Поиск деталей по артикулу через Doc: findOem */ async findOEM(oemNumber: string, brand?: string, replacementTypes?: string): Promise { try { console.log('🔍 Doc: findOem поиск по артикулу:', oemNumber) // Команда для Doc модуля согласно документации let command = `FindOEM:Locale=ru_RU|OEM=${oemNumber}|Options=crosses` if (brand) { command += `|Brand=${brand}` } if (replacementTypes) { command += `|ReplacementTypes=${replacementTypes}` } const hmac = this.createHMAC(command) console.log('📝 Doc findOem Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') return this.parseFindOEMResponse(xmlText) } catch (error) { console.error('Ошибка Doc findOem:', error) throw error } } /** * Парсит ответ findOem */ private parseFindOEMResponse(xmlText: string): LaximoDocFindOEMResult | null { try { console.log('📄 Парсинг ответа Doc findOem...') // Извлекаем данные из SOAP ответа const resultMatch = xmlText.match(/]*>([\s\S]*?)<\/ns:return>/) || xmlText.match(/]*>([\s\S]*?)<\/return>/) if (!resultMatch) { console.log('❌ Не найден return в ответе') return null } let resultData = resultMatch[1] // Декодируем HTML entities если данные экранированы resultData = resultData .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/&/g, '&') console.log('📋 Данные результата (первые 1000 символов):', resultData.substring(0, 1000)) console.log('📋 Полные данные результата:', resultData) // Ищем блок FindOEM const findOemMatch = resultData.match(/([\s\S]*?)<\/FindOEM>/) || resultData.match(/([\s\S]*?)<\/findOem>/) || resultData.match(/([\s\S]*?)<\/response>/) if (!findOemMatch) { console.log('❌ Не найден блок FindOEM в ответе') return null } const findOemData = findOemMatch[1] // Парсим детали const details: LaximoDocDetail[] = [] const detailPattern = /]*)>(.*?)<\/detail>/g let detailMatch while ((detailMatch = detailPattern.exec(findOemData)) !== null) { const detailAttrs = detailMatch[1] const detailContent = detailMatch[2] const getAttribute = (name: string): string => { const match = detailAttrs.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } // Парсим замены const replacements: LaximoDocReplacement[] = [] const replacementPattern = /]*)>(.*?)<\/replacement>/g let replMatch while ((replMatch = replacementPattern.exec(detailContent)) !== null) { const replAttrs = replMatch[1] const replContent = replMatch[2] const getReplAttr = (name: string): string => { const match = replAttrs.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } // Парсим деталь замены const replDetailMatch = replContent.match(/]*)/) let replDetail: LaximoDocReplacementDetail = { detailid: '', formattedoem: '', manufacturer: '', manufacturerid: '', name: '', oem: '' } if (replDetailMatch) { const replDetailAttrs = replDetailMatch[1] const getReplDetailAttr = (name: string): string => { const match = replDetailAttrs.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } replDetail = { detailid: getReplDetailAttr('detailid'), formattedoem: getReplDetailAttr('formattedoem'), manufacturer: getReplDetailAttr('manufacturer'), manufacturerid: getReplDetailAttr('manufacturerid'), name: getReplDetailAttr('name'), oem: getReplDetailAttr('oem'), weight: getReplDetailAttr('weight'), icon: getReplDetailAttr('icon') } } replacements.push({ type: getReplAttr('type'), way: getReplAttr('way'), replacementid: getReplAttr('replacementid'), rate: getReplAttr('rate'), detail: replDetail }) } const detail: LaximoDocDetail = { detailid: getAttribute('detailid'), formattedoem: getAttribute('formattedoem'), manufacturer: getAttribute('manufacturer'), manufacturerid: getAttribute('manufacturerid'), name: getAttribute('name'), oem: getAttribute('oem'), volume: getAttribute('volume'), weight: getAttribute('weight'), replacements } details.push(detail) console.log('🔩 Найдена деталь:', { oem: detail.oem, name: detail.name, manufacturer: detail.manufacturer, replacements: detail.replacements.length }) } console.log('✅ Всего найдено деталей:', details.length) return { details } } catch (error) { console.error('Ошибка парсинга findOem ответа:', error) return null } } } /** * Laximo SOAP API Service для интеграции с каталогом автозапчастей * * Использует актуальные endpoints согласно WSDL: * - SOAP 1.1: https://ws.laximo.ru/ec.Kito.WebCatalog/services/Catalog.CatalogHttpSoap11Endpoint/ * - SOAP 1.2: https://ws.laximo.ru/ec.Kito.WebCatalog/services/Catalog.CatalogHttpSoap12Endpoint/ * - Функция QueryDataLogin для авторизации * - HMAC контрольный код с MD5 хешированием * - Команда ListCatalogs:Locale=ru_RU для получения каталогов */ class LaximoService { // Актуальные endpoints согласно WSDL схеме protected soap11Url = 'https://ws.laximo.ru/ec.Kito.WebCatalog/services/Catalog.CatalogHttpSoap11Endpoint/' protected soap12Url = 'https://ws.laximo.ru/ec.Kito.WebCatalog/services/Catalog.CatalogHttpSoap12Endpoint/' protected login = process.env.LAXIMO_LOGIN || '' protected password = process.env.LAXIMO_PASSWORD || '' /** * Создает HMAC контрольный код для авторизации * Формула: MD5(команда + пароль) */ protected createHMAC(command: string): string { if (!this.password) { throw new Error('Password is required for HMAC generation') } const combinedString = command + this.password return createHash('md5').update(combinedString).digest('hex') } /** * Экранирует специальные символы XML в SSD параметре */ protected escapeSsdForXML(ssd: string): string { return ssd .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } /** * Создает SOAP 1.1 конверт согласно WSDL схеме */ protected createSOAP11Envelope(command: string, login: string, hmac: string): string { return ` ${command} ${login} ${hmac} ` } /** * Создает SOAP 1.2 конверт согласно WSDL схеме */ private createSOAP12Envelope(command: string, login: string, hmac: string): string { return ` ${command} ${login} ${hmac} ` } /** * Парсит XML ответ согласно официальной документации Laximo */ private parseListCatalogsResponse(xmlText: string): LaximoBrand[] { const brands: LaximoBrand[] = [] // Извлекаем данные между тегами QueryDataLoginResponse/return или response let resultData = '' // Пытаемся найти данные в разных форматах ответа const soapResultMatch = xmlText.match(/]*>([\s\S]*?)<\/ns:return>/) || xmlText.match(/]*>([\s\S]*?)<\/return>/) const responseMatch = xmlText.match(/]*>([\s\S]*?)<\/response>/) if (soapResultMatch) { resultData = soapResultMatch[1] // Декодируем HTML entities если данные экранированы resultData = resultData .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/&/g, '&') } else if (responseMatch) { resultData = responseMatch[1] } else { console.log('🔍 Не найден результат в XML ответе') return brands } // Ищем секцию ListCatalogs const catalogsMatch = resultData.match(/]*>([\s\S]*?)<\/ListCatalogs>/) if (!catalogsMatch) { console.log('🔍 Не найдена секция ListCatalogs') return brands } const catalogsData = catalogsMatch[1] // Ищем все row элементы с их содержимым const rowMatches = catalogsData.match(/]*>[\s\S]*?<\/row>/g) if (!rowMatches) { console.log('🔍 Не найдены row элементы') return brands } console.log(`🔍 Найдено ${rowMatches.length} брендов`) for (const rowMatch of rowMatches) { // Извлекаем атрибуты из тега row const rowTagMatch = rowMatch.match(/]*)>/); if (!rowTagMatch) continue; const rowAttributes = rowTagMatch[1]; const getAttribute = (name: string): string => { const match = rowAttributes.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } const brand: LaximoBrand = { brand: getAttribute('brand'), code: getAttribute('code'), icon: getAttribute('icon'), name: getAttribute('name'), supportdetailapplicability: getAttribute('supportdetailapplicability') === 'true', supportparameteridentification2: getAttribute('supportparameteridentification2') === 'true', supportquickgroups: getAttribute('supportquickgroups') === 'true', supportvinsearch: getAttribute('supportvinsearch') === 'true', features: [] } // Опциональные атрибуты const supportframesearch = getAttribute('supportframesearch') if (supportframesearch) { brand.supportframesearch = supportframesearch === 'true' } const vinexample = getAttribute('vinexample') if (vinexample) { brand.vinexample = vinexample } const frameexample = getAttribute('frameexample') if (frameexample) { brand.frameexample = frameexample } // Парсим features согласно документации brand.features = this.parseFeatures(rowMatch) // Парсим extensions если есть brand.extensions = this.parseExtensions(rowMatch) brands.push(brand) } return brands } /** * Парсит секцию features согласно документации */ private parseFeatures(rowXml: string): LaximoFeature[] { const features: LaximoFeature[] = [] const featuresMatch = rowXml.match(/]*>([\s\S]*?)<\/features>/) if (!featuresMatch) { return features } const featuresData = featuresMatch[1] const featureMatches = featuresData.match(/]*\/?>|]*>[\s\S]*?<\/feature>/g) if (!featureMatches) { return features } for (const featureMatch of featureMatches) { const getAttribute = (name: string): string => { const match = featureMatch.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } const feature: LaximoFeature = { name: getAttribute('name') } const example = getAttribute('example') if (example) { feature.example = example } features.push(feature) } return features } /** * Парсит секцию extensions согласно документации */ private parseExtensions(rowXml: string): LaximoExtensions | undefined { const extensionsMatch = rowXml.match(/]*>([\s\S]*?)<\/extensions>/) if (!extensionsMatch) { return undefined } const extensionsData = extensionsMatch[1] const operationsMatch = extensionsData.match(/]*>([\s\S]*?)<\/operations>/) if (!operationsMatch) { return undefined } const operationsData = operationsMatch[1] const operationMatches = operationsData.match(/]*>[\s\S]*?<\/operation>/g) if (!operationMatches) { return undefined } const operations: LaximoOperation[] = [] for (const operationMatch of operationMatches) { const getAttribute = (name: string): string => { const match = operationMatch.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } const operation: LaximoOperation = { name: getAttribute('name'), kind: getAttribute('kind'), description: getAttribute('description'), fields: [] } // Парсим поля операции const fieldMatches = operationMatch.match(/]*\/?>|]*>[\s\S]*?<\/field>/g) if (fieldMatches) { for (const fieldMatch of fieldMatches) { const getFieldAttr = (name: string): string => { const match = fieldMatch.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } const field: LaximoField = { name: getFieldAttr('name'), description: getFieldAttr('description') } const pattern = getFieldAttr('pattern') if (pattern) { field.pattern = pattern } const example = getFieldAttr('example') if (example) { field.example = example } operation.fields.push(field) } } operations.push(operation) } return { operations } } /** * Получает список каталогов через SOAP API */ async getListCatalogs(): Promise { // Проверяем наличие учетных данных if (!this.login || !this.password) { throw new Error('Laximo credentials not configured. Please set LAXIMO_LOGIN and LAXIMO_PASSWORD environment variables.') } const command = 'ListCatalogs:Locale=ru_RU' const hmac = this.createHMAC(command) console.log('🔍 Отправляем SOAP запрос к Laximo API...') console.log('🔐 Login:', this.login) console.log('📝 Command:', command) console.log('🔗 HMAC:', hmac) // Сначала пробуем SOAP 1.1 try { console.log('📍 Trying SOAP 1.1:', this.soap11Url) return await this.makeSOAPRequest(this.soap11Url, this.createSOAP11Envelope(command, this.login, hmac), 'urn:QueryDataLogin') } catch (soap11Error) { console.log('❌ SOAP 1.1 failed:', soap11Error instanceof Error ? soap11Error.message : 'Unknown error') // Fallback на SOAP 1.2 try { console.log('📍 Trying SOAP 1.2:', this.soap12Url) return await this.makeSOAPRequest(this.soap12Url, this.createSOAP12Envelope(command, this.login, hmac), 'urn:QueryDataLogin') } catch (soap12Error) { console.error('❌ Both SOAP 1.1 and 1.2 failed') throw soap12Error } } } /** * Выполняет SOAP запрос */ private async makeSOAPRequest(url: string, soapEnvelope: string, soapAction: string): Promise { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': `"${soapAction}"` }, body: soapEnvelope }) if (!response.ok) { if (response.status === 404) { throw new Error(`Laximo API endpoint not found: ${url}. Please check the current API documentation.`) } throw new Error(`Laximo SOAP API error: ${response.status} ${response.statusText}`) } const xmlText = await response.text() console.log('📥 Получен ответ от Laximo API') console.log('📋 Response length:', xmlText.length) // Проверяем на ошибки в ответе if (xmlText.includes('E_ACCESSDENIED')) { if (xmlText.includes('MAC check failed')) { throw new Error('Invalid Laximo credentials: MAC check failed') } if (xmlText.includes('You don`t have active subscription')) { throw new Error('No active Laximo subscription') } throw new Error('Access denied to Laximo API') } return this.parseListCatalogsResponse(xmlText) } /** * Получает информацию о каталоге */ async getCatalogInfo(catalogCode: string): Promise { const command = `GetCatalogInfo:Locale=ru_RU|Catalog=${catalogCode}|withPermissions=true` const hmac = this.createHMAC(command) console.log('🔍 Получаем информацию о каталоге:', catalogCode) console.log('📝 Command:', command) console.log('🔗 HMAC:', hmac) try { const response = await this.makeBasicSOAPRequest(this.soap11Url, this.createSOAP11Envelope(command, this.login, hmac), 'urn:QueryDataLogin') console.log('📥 Получен ответ от Laximo API для каталога') console.log('📋 Response length:', response.length) const result = this.parseCatalogInfoResponse(response) console.log('🎯 Результат парсинга каталога:', result ? 'успешно' : 'неудачно') return result } catch (error) { console.error('❌ Ошибка получения информации о каталоге:', error) return null } } /** * Получает параметры для поиска автомобиля по wizard */ async getWizard2(catalogCode: string, ssd: string = ''): Promise { const command = `GetWizard2:Locale=ru_RU|Catalog=${catalogCode}|ssd=${ssd}` const hmac = this.createHMAC(command) console.log('🔍 Получаем параметры wizard для каталога:', catalogCode) try { const response = await this.makeBasicSOAPRequest(this.soap11Url, this.createSOAP11Envelope(command, this.login, hmac), 'urn:QueryDataLogin') return this.parseWizard2Response(response) } catch (error) { console.error('Ошибка получения параметров wizard:', error) return [] } } /** * Глобальный поиск автомобилей по VIN/Frame во всех каталогах * @see https://doc.laximo.ru/ru/cat/FindVehicle */ async findVehicleGlobal(vin: string): Promise { try { console.log('🌍 Глобальный поиск автомобиля по VIN/Frame:', vin) const command = `FindVehicle:Locale=ru_RU|IdentString=${vin}` const hmac = this.createHMAC(command) console.log('📝 Global FindVehicle Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') return this.parseVehicleSearchResponse(xmlText) } catch (error) { console.error('❌ Ошибка глобального поиска автомобиля по VIN/Frame:', error) return [] } } /** * Поиск автомобиля по VIN/Frame согласно документации Laximo * @see https://doc.laximo.ru/ru/cat/FindVehicle */ async findVehicle(catalogCode: string, vin: string): Promise { try { console.log('🔍 Поиск автомобиля по VIN/Frame:', vin) console.log('📋 Каталог:', catalogCode) // Согласно документации используем IdentString вместо vin const command = `FindVehicle:Locale=ru_RU|Catalog=${catalogCode}|IdentString=${vin}` const hmac = this.createHMAC(command) console.log('📝 FindVehicle Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') const vehicles = this.parseVehicleSearchResponse(xmlText) if (vehicles.length === 0) { console.log('⚠️ Автомобили не найдены по VIN/Frame:', vin) // Попробуем поиск без указания каталога (поиск во всех каталогах) console.log('🔄 Пробуем поиск во всех каталогах...') const globalCommand = `FindVehicle:Locale=ru_RU|IdentString=${vin}` const globalHmac = this.createHMAC(globalCommand) console.log('📝 Global FindVehicle Command:', globalCommand) const globalSoapEnvelope = this.createSOAP11Envelope(globalCommand, this.login, globalHmac) const globalXmlText = await this.makeBasicSOAPRequest(this.soap11Url, globalSoapEnvelope, 'urn:QueryDataLogin') return this.parseVehicleSearchResponse(globalXmlText) } console.log(`✅ Найдено ${vehicles.length} автомобилей`) return vehicles } catch (error) { console.error('❌ Ошибка поиска автомобиля по VIN/Frame:', error) return [] } } /** * Поиск автомобилей по wizard (SSD) */ async findVehicleByWizard(catalogCode: string, ssd: string): Promise { const command = `FindVehicleByWizard2:Locale=ru_RU|Catalog=${catalogCode}|ssd=${ssd}` const hmac = this.createHMAC(command) console.log('🔍 Поиск автомобилей по wizard SSD:', ssd) try { const response = await this.makeBasicSOAPRequest(this.soap11Url, this.createSOAP11Envelope(command, this.login, hmac), 'urn:QueryDataLogin') const vehicles = this.parseVehicleSearchResponse(response) // Используем SSD из ответа API, если он есть, иначе используем поисковый SSD return vehicles.map(vehicle => ({ ...vehicle, ssd: vehicle.ssd || ssd })) } catch (error) { console.error('Ошибка поиска по wizard:', error) return [] } } /** * Получает информацию о конкретном автомобиле */ async getVehicleInfo(catalogCode: string, vehicleId: string, ssd?: string, localized: boolean = true): Promise { console.log('🔍 Получаем информацию об автомобиле:', vehicleId) console.log('📋 Входные параметры - SSD:', ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует') // Для автомобилей найденных через wizard, SSD является обязательным if (!ssd || ssd.trim() === '') { console.log('⚠️ SSD не предоставлен, но может быть обязательным для этого автомобиля') // Возвращаем базовую информацию return { vehicleid: vehicleId, name: `Автомобиль ${catalogCode}`, ssd: '', brand: catalogCode.replace(/\d+$/, ''), catalog: catalogCode, attributes: [] } } const command = `GetVehicleInfo:Locale=ru_RU|Catalog=${catalogCode}|VehicleId=${vehicleId}|ssd=${ssd}|Localized=${localized}` const hmac = this.createHMAC(command) console.log('📝 Command:', command) console.log('🔗 HMAC:', hmac) try { const response = await this.makeBasicSOAPRequest(this.soap11Url, this.createSOAP11Envelope(command, this.login, hmac), 'urn:QueryDataLogin') return this.parseVehicleInfoResponse(response) } catch (error) { console.error('Ошибка получения информации об автомобиле:', error) // Возвращаем базовую информацию об автомобиле если API недоступен console.log('⚠️ Возвращаем базовую информацию об автомобиле') return { vehicleid: vehicleId, name: `Автомобиль ${catalogCode}`, ssd: ssd || '', brand: catalogCode.replace(/\d+$/, ''), catalog: catalogCode, attributes: [] } } } /** * Получает список узлов каталога (альтернатива для групп быстрого поиска) */ async getListUnits(catalogCode: string, vehicleId?: string, ssd?: string, categoryId?: string): Promise { try { console.log('🔍 LaximoService.getListUnits - начало запроса:', catalogCode) console.log('📋 Параметры:', { vehicleId, categoryId, ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует' }) // Формируем команду в зависимости от наличия vehicleId, SSD и categoryId let command = `ListUnits:Locale=ru_RU|Catalog=${catalogCode}` if (vehicleId) { command += `|VehicleId=${vehicleId}` } // 🎯 ИСПРАВЛЕНИЕ: Для категорий каталога НЕ используем SSD // SSD используется только для QuickGroup'ов, но не для категорий if (categoryId) { // Если указана категория, НЕ добавляем SSD - показываем ВСЕ узлы категории command += `|CategoryId=${categoryId}` console.log('🔧 Режим "От производителя": используем CategoryId БЕЗ SSD для получения всех узлов категории') } else if (ssd && ssd.trim() !== '') { // SSD добавляем только если НЕТ CategoryId (для QuickGroup'ов) const escapedSsd = this.escapeSsdForXML(ssd) command += `|ssd=${escapedSsd}` console.log('🔧 Режим "Общие": используем SSD для получения узлов автомобиля') } const hmac = this.createHMAC(command) console.log('📝 ListUnits Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) console.log('🌐 Отправляем SOAP запрос...') const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') console.log('📥 Получен ответ от Laximo, начинаем парсинг...') const result = this.parseListUnitsResponse(xmlText) console.log('✅ LaximoService.getListUnits - завершено, получено узлов:', result.length) if (result.length > 0) { console.log('📦 Первый узел из LaximoService:', { quickgroupid: result[0].quickgroupid, name: result[0].name, code: result[0].code, hasImageUrl: !!result[0].imageurl, imageUrl: result[0].imageurl ? result[0].imageurl.substring(0, 80) + '...' : 'отсутствует' }) } return result } catch (error) { console.error('❌ LaximoService.getListUnits - ошибка:', error) if (error instanceof Error) { console.error('❌ Подробности ошибки:', { name: error.name, message: error.message, stack: error.stack?.substring(0, 500) }) } return [] } } /** * Парсит ответ ListUnits и преобразует в формат LaximoQuickGroup */ private parseListUnitsResponse(xmlText: string): LaximoQuickGroup[] { console.log('🔍 Парсим узлы каталога...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return [] } // Ищем секцию ListUnits const unitsMatch = resultData.match(/]*>([\s\S]*?)<\/ListUnits?>/) || resultData.match(/]*>([\s\S]*?)<\/response>/) if (!unitsMatch) { console.log('❌ Не найдена секция ListUnits') return [] } const groups: LaximoQuickGroup[] = [] const rowPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/row>)/g let match while ((match = rowPattern.exec(unitsMatch[1])) !== null) { const attributes = match[1] const content = match[2] || '' // Извлекаем атрибуты согласно API Laximo ListUnits const unitid = this.extractAttribute(attributes, 'unitid') || this.extractAttribute(attributes, 'id') const name = this.extractAttribute(attributes, 'name') || this.extractAttribute(attributes, 'description') const code = this.extractAttribute(attributes, 'code') const imageurl = this.extractAttribute(attributes, 'imageurl') const largeimageurl = this.extractAttribute(attributes, 'largeimageurl') const hasDetails = this.extractAttribute(attributes, 'hasdetails') === 'true' if (unitid && name) { const group: LaximoQuickGroup = { quickgroupid: unitid, name: name, link: hasDetails, code: code || undefined, imageurl: imageurl || undefined, largeimageurl: largeimageurl || undefined } console.log('📦 Найден узел каталога:', { unitid, name, code, imageurl: imageurl ? imageurl.substring(0, 50) + '...' : 'отсутствует', hasDetails }) groups.push(group) } } console.log(`✅ Обработано ${groups.length} узлов каталога`) return groups } /** * Получает список категорий каталога (альтернатива для групп быстрого поиска) */ async getListCategories(catalogCode: string, vehicleId?: string, ssd?: string): Promise { try { console.log('🔍 Получаем категории каталога:', catalogCode) console.log('📋 Параметры:', { vehicleId, ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует' }) // Формируем команду согласно документации Laximo // CategoryId=-1 необходим для получения полного списка категорий let command = `ListCategories:Locale=ru_RU|Catalog=${catalogCode}|CategoryId=-1` // Добавляем VehicleId и ssd если они предоставлены if (vehicleId) { command += `|VehicleId=${vehicleId}` } if (ssd && ssd.trim() !== '') { const escapedSsd = this.escapeSsdForXML(ssd) command += `|ssd=${escapedSsd}` } const hmac = this.createHMAC(command) console.log('📝 ListCategories Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') return this.parseListCategoriesResponse(xmlText) } catch (error) { console.error('Ошибка получения категорий каталога:', error) return [] } } /** * Парсит ответ ListCategories и преобразует в формат LaximoQuickGroup */ private parseListCategoriesResponse(xmlText: string): LaximoQuickGroup[] { console.log('🔍 Парсим категории каталога...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return [] } // Ищем секцию ListCategories const categoriesMatch = resultData.match(/]*>([\s\S]*?)<\/ListCategories?>/) || resultData.match(/]*>([\s\S]*?)<\/response>/) if (!categoriesMatch) { console.log('❌ Не найдена секция ListCategories') console.log('📋 Доступные данные результата (первые 500 символов):', resultData.substring(0, 500)) return [] } const groups: LaximoQuickGroup[] = [] const rowPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/row>)/g let match while ((match = rowPattern.exec(categoriesMatch[1])) !== null) { const attributes = match[1] const content = match[2] || '' // Извлекаем атрибуты согласно документации Laximo const categoryid = this.extractAttribute(attributes, 'categoryid') const name = this.extractAttribute(attributes, 'name') const childrens = this.extractAttribute(attributes, 'childrens') === 'true' const parentcategoryid = this.extractAttribute(attributes, 'parentcategoryid') console.log('🔍 Обрабатываем row:', { categoryid, name, childrens, parentcategoryid, attributes }) if (categoryid && name) { const group: LaximoQuickGroup = { quickgroupid: categoryid, name: name, link: true // Для категорий всегда true, так как они могут содержать узлы } console.log('📦 Найдена категория каталога:', { categoryid, name, childrens, parentcategoryid }) groups.push(group) } } console.log(`✅ Обработано ${groups.length} категорий каталога`) return groups } /** * Получает список групп быстрого поиска для автомобиля */ async getListQuickGroup(catalogCode: string, vehicleId: string, ssd?: string): Promise { console.log('🔍 Получаем группы быстрого поиска для автомобиля:', vehicleId) console.log('📋 Входные параметры - SSD:', ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует') // Для автомобилей найденных через wizard, SSD является обязательным if (!ssd || ssd.trim() === '') { console.log('⚠️ SSD не предоставлен, пробуем альтернативные методы...') // Попробуем общие группы каталога try { const catalogCommand = `ListQuickGroup:Locale=ru_RU|Catalog=${catalogCode}` const catalogHmac = this.createHMAC(catalogCommand) console.log('📝 Catalog command:', catalogCommand) const soapEnvelope = this.createSOAP11Envelope(catalogCommand, this.login, catalogHmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') return this.parseListQuickGroupResponse(xmlText) } catch (catalogError) { console.error('Ошибка получения общих групп каталога:', catalogError) } // Альтернативный способ - попробовать ListCategories try { return await this.getListCategories(catalogCode) } catch (categoriesError) { console.error('Ошибка получения категорий каталога:', categoriesError) return [] } } try { // Экранируем специальные символы XML в SSD параметре const escapedSsd = this.escapeSsdForXML(ssd) const command = `ListQuickGroup:Locale=ru_RU|Catalog=${catalogCode}|VehicleId=${vehicleId}|ssd=${escapedSsd}` const hmac = this.createHMAC(command) console.log('📝 Исходный SSD:', ssd) console.log('📝 Экранированный SSD:', escapedSsd) console.log('📝 Laximo ListQuickGroup Command:', command) console.log('🔗 HMAC:', hmac) console.log('👤 Login:', this.login) console.log('🏭 SOAP URL:', this.soap11Url) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) console.log('📦 Создан SOAP envelope для ListQuickGroup') const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') console.log('🎯 Начинаем парсинг XML ответа от Laximo...') const result = this.parseListQuickGroupResponse(xmlText) console.log('✅ Парсинг завершен, получено групп:', result.length) return result } catch (error) { console.error('Ошибка получения групп быстрого поиска:', error) // Альтернативный способ - попробовать ListUnits console.log('🔄 Попытка получить узлы каталога как альтернативу...') try { return await this.getListUnits(catalogCode, vehicleId, ssd) } catch (unitsError) { console.error('Ошибка получения узлов каталога:', unitsError) } // Последний шанс - попробовать ListCategories console.log('🔄 Попытка получить категории каталога...') try { return await this.getListCategories(catalogCode, vehicleId, ssd) } catch (categoriesError) { console.error('Ошибка получения категорий каталога:', categoriesError) } return [] } } /** * Получение групп быстрого поиска с RAW XML ответом */ async getListQuickGroupWithXML(catalogCode: string, vehicleId: string, ssd?: string): Promise<{ groups: LaximoQuickGroup[], rawXML: string }> { console.log('🔍 Получаем группы быстрого поиска с RAW XML для автомобиля:', vehicleId) console.log('📋 Входные параметры - SSD:', ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует') // Для автомобилей найденных через wizard, SSD является обязательным if (!ssd || ssd.trim() === '') { console.log('⚠️ SSD не предоставлен, пробуем альтернативные методы...') // Попробуем общие группы каталога try { const catalogCommand = `ListQuickGroup:Locale=ru_RU|Catalog=${catalogCode}` const catalogHmac = this.createHMAC(catalogCommand) console.log('📝 Catalog command:', catalogCommand) const soapEnvelope = this.createSOAP11Envelope(catalogCommand, this.login, catalogHmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') const groups = this.parseListQuickGroupResponse(xmlText) return { groups, rawXML: xmlText } } catch (catalogError) { console.error('Ошибка получения общих групп каталога:', catalogError) return { groups: [], rawXML: '' } } } try { // Экранируем специальные символы XML в SSD параметре const escapedSsd = this.escapeSsdForXML(ssd) const command = `ListQuickGroup:Locale=ru_RU|Catalog=${catalogCode}|VehicleId=${vehicleId}|ssd=${escapedSsd}` const hmac = this.createHMAC(command) console.log('📝 Исходный SSD:', ssd) console.log('📝 Экранированный SSD:', escapedSsd) console.log('📝 Laximo ListQuickGroup Command:', command) console.log('🔗 HMAC:', hmac) console.log('👤 Login:', this.login) console.log('🏭 SOAP URL:', this.soap11Url) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) console.log('📦 Создан SOAP envelope для ListQuickGroup') const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') console.log('🎯 Начинаем парсинг XML ответа от Laximo...') const groups = this.parseListQuickGroupResponse(xmlText) console.log('✅ Парсинг завершен, получено групп:', groups.length) return { groups, rawXML: xmlText } } catch (error) { console.error('Ошибка получения групп быстрого поиска:', error) return { groups: [], rawXML: '' } } } /** * Базовый SOAP запрос без парсинга каталогов */ protected async makeBasicSOAPRequest(url: string, soapEnvelope: string, soapAction: string): Promise { console.log('🌐 Laximo SOAP запрос к:', url) console.log('🎯 SOAPAction:', soapAction) console.log('📤 SOAP Envelope (первые 800 символов):') console.log(soapEnvelope.substring(0, 800)) const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': `"${soapAction}"` }, body: soapEnvelope }) console.log('📡 HTTP Response Status:', response.status, response.statusText) console.log('📋 Response Headers:', Object.fromEntries(response.headers.entries())) if (!response.ok) { // Пытаемся получить тело ответа для диагностики let errorBody = '' try { errorBody = await response.text() console.error('🚨 Laximo API error body:', errorBody.substring(0, 1000)) } catch (e) { console.error('🚨 Не удалось прочитать тело ошибки:', e) } throw new Error(`Laximo API error: ${response.status} ${response.statusText}`) } const xmlText = await response.text() console.log('📥 RAW XML ответ длиной:', xmlText.length, 'символов') console.log('📄 ПОЛНЫЙ XML ОТВЕТ:') console.log(xmlText) console.log('📄 ======= КОНЕЦ XML =======') console.log('📄 Первые 1000 символов XML:') console.log(xmlText.substring(0, 1000)) // Проверяем на ошибки в ответе if (xmlText.includes('E_ACCESSDENIED')) { console.error('🚨 Access denied error в XML ответе') throw new Error('Access denied to Laximo API') } if (xmlText.includes('soap:Fault') || xmlText.includes('faultstring')) { console.error('🚨 SOAP Fault в ответе:', xmlText.substring(0, 1000)) throw new Error('SOAP Fault in Laximo response') } return xmlText } /** * Парсит ответ GetCatalogInfo */ private parseCatalogInfoResponse(xmlText: string): LaximoCatalogInfo | null { console.log('🔍 Начинаем парсинг ответа о каталоге...') const resultData = this.extractResultData(xmlText) console.log('📋 Извлеченные данные результата:', resultData ? 'найдены' : 'не найдены') if (!resultData) { console.log('❌ resultData is null, возвращаем null') return null } const catalogMatch = resultData.match(/]*)>/); console.log('🎯 Найдено совпадений row:', catalogMatch ? 'да' : 'нет') if (!catalogMatch) { console.log('❌ catalogMatch is null, возвращаем null') return null } const attributes = catalogMatch[1]; console.log('📦 Атрибуты каталога:', attributes) const getAttribute = (name: string): string => { const match = attributes.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } const features = this.parseFeatures(resultData) const permissions = this.parsePermissions(resultData) const result = { brand: getAttribute('brand'), code: getAttribute('code'), icon: getAttribute('icon'), name: getAttribute('name'), supportdetailapplicability: getAttribute('supportdetailapplicability') === 'true', supportparameteridentification2: getAttribute('supportparameteridentification2') === 'true', supportquickgroups: getAttribute('supportquickgroups') === 'true', supportvinsearch: getAttribute('supportvinsearch') === 'true', supportplateidentification: getAttribute('supportplateidentification') === 'true' || undefined, vinexample: getAttribute('vinexample') || undefined, plateexample: getAttribute('plateexample') || undefined, features, permissions } console.log('✅ Результат парсинга каталога:', result) return result } /** * Парсит ответ GetWizard2 */ private parseWizard2Response(xmlText: string): LaximoWizardStep[] { const resultData = this.extractResultData(xmlText) if (!resultData) return [] const rowMatches = resultData.match(/]*>[\s\S]*?<\/row>/g) if (!rowMatches) return [] const steps: LaximoWizardStep[] = [] for (const rowMatch of rowMatches) { const rowTagMatch = rowMatch.match(/]*)>/); if (!rowTagMatch) continue; const attributes = rowTagMatch[1]; const getAttribute = (name: string): string => { const match = attributes.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } const options = this.parseWizardOptions(rowMatch) steps.push({ allowlistvehicles: getAttribute('allowlistvehicles') === 'true', automatic: getAttribute('automatic') === 'true', conditionid: getAttribute('conditionid'), determined: getAttribute('determined') === 'true', name: getAttribute('name'), type: getAttribute('type'), ssd: getAttribute('ssd') || undefined, value: getAttribute('value') || undefined, valueid: getAttribute('valueid') || undefined, options }) } return steps } /** * Парсит ответ поиска автомобилей */ private parseVehicleSearchResponse(xmlText: string): LaximoVehicleSearchResult[] { console.log('🔍 Парсим результаты поиска автомобилей...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return [] } const rowMatches = resultData.match(/]*>[\s\S]*?<\/row>/g) if (!rowMatches) { console.log('❌ Не найдены строки row в ответе') return [] } console.log(`🎯 Найдено ${rowMatches.length} автомобилей`) const vehicles: LaximoVehicleSearchResult[] = [] for (const rowMatch of rowMatches) { const rowTagMatch = rowMatch.match(/]*)>/); if (!rowTagMatch) continue; const attributes = rowTagMatch[1]; console.log('📦 Атрибуты автомобиля:', attributes.substring(0, 200)); const getAttribute = (name: string): string => { const match = attributes.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } // Парсим атрибуты из дочерних элементов const attributeMap = new Map() // Отладочное логирование console.log('🔍 Полный XML контент строки:', rowMatch.substring(0, 500)) const attributeMatches = rowMatch.match(/]*\/?>|]*>[\s\S]*?<\/attribute>/g) if (attributeMatches) { console.log(`📋 Найдено ${attributeMatches.length} дочерних атрибутов`) console.log('🔍 Первые несколько атрибутов:', attributeMatches.slice(0, 3)) for (const attrMatch of attributeMatches) { const attrTagMatch = attrMatch.match(/]*)>/); if (!attrTagMatch) continue; const attrAttributes = attrTagMatch[1]; const getAttrAttribute = (name: string): string => { const match = attrAttributes.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } const key = getAttrAttribute('key') const value = getAttrAttribute('value') if (key && value) { attributeMap.set(key, value) console.log(`🔑 Атрибут: ${key} = ${value}`) } } console.log(`📊 Всего атрибутов в карте: ${attributeMap.size}`) } else { console.log('❌ Дочерние атрибуты не найдены') console.log('🔍 Проверим содержимое rowMatch:') console.log(' - Содержит 0) { console.log('🗝️ Все доступные ключи атрибутов:', Array.from(attributeMap.keys()).sort()) } console.log('🔍 Извлеченные значения:') console.log(` - year: "${year}" (из: getAttribute('year')="${getAttribute('year')}", manufactured="${attributeMap.get('manufactured')}", date="${attributeMap.get('date')}")`) console.log(` - engine: "${engine}" (из: getAttribute('engine')="${getAttribute('engine')}", engine="${attributeMap.get('engine')}", engine_info="${attributeMap.get('engine_info')}")`) console.log(` - modification: "${modification}"`) console.log(` - bodytype: "${bodytype}"`) const vehicle = { vehicleid: getAttribute('vehicleid'), name: vehicleName || undefined, brand: getAttribute('brand'), catalog: getAttribute('catalog') || undefined, model: vehicleName || getAttribute('model'), modification: modification, year: year, bodytype: bodytype, engine: engine, notes: getAttribute('notes') || undefined, ssd: getAttribute('ssd') || undefined, // Дополнительные атрибуты из документации Laximo grade: attributeMap.get('grade') || undefined, transmission: attributeMap.get('transmission') || undefined, creationregion: attributeMap.get('creationregion') || undefined, destinationregion: attributeMap.get('destinationregion') || undefined, date: attributeMap.get('date') || undefined, manufactured: attributeMap.get('manufactured') || undefined, framecolor: attributeMap.get('framecolor') || undefined, trimcolor: attributeMap.get('trimcolor') || undefined, datefrom: attributeMap.get('datefrom') || undefined, dateto: attributeMap.get('dateto') || undefined, engine_info: attributeMap.get('engine_info') || undefined, engineno: attributeMap.get('engineno') || undefined, options: attributeMap.get('options') || undefined, modelyearfrom: attributeMap.get('modelyearfrom') || undefined, modelyearto: attributeMap.get('modelyearto') || undefined, description: attributeMap.get('description') || undefined, market: attributeMap.get('market') || undefined, prodRange: attributeMap.get('prodrange') || undefined, // Используем ключ в нижнем регистре из API prodPeriod: attributeMap.get('prodPeriod') || undefined, carpet_color: attributeMap.get('carpet_color') || undefined, seat_combination_code: attributeMap.get('seat_combination_code') || undefined, attributes: Array.from(attributeMap.entries()).map(([key, value]) => { // Маппинг ключей к человеко-читаемым названиям const keyNameMap: Record = { 'model': 'Модель', 'MVS': 'Код в каталоге', 'MVSDesc': 'Описание', 'specialVersion': 'Версия', 'engine': 'Двигатель', 'variant': 'Исполнение', 'transmission': 'КПП', 'bodytype': 'Тип кузова', 'year': 'Год', 'manufactured': 'Год выпуска', 'date': 'Дата', 'market': 'Рынок', 'grade': 'Класс', 'framecolor': 'Цвет кузова', 'trimcolor': 'Цвет салона', 'engine_info': 'Информация о двигателе', 'engineno': 'Номер двигателя', 'options': 'Опции', 'description': 'Описание', 'notes': 'Примечания', 'creationregion': 'Регион производства', 'destinationregion': 'Регион назначения', 'datefrom': 'Дата с', 'dateto': 'Дата по', 'modelyearfrom': 'Модельный год с', 'modelyearto': 'Модельный год по', 'prodRange': 'Диапазон производства', 'prodPeriod': 'Период производства' }; return { key, name: keyNameMap[key] || key, value }; }) } console.log('🚗 Найден автомобиль:', { vehicleid: vehicle.vehicleid, name: vehicleName || `${vehicle.brand} ${vehicle.model}`, brand: vehicle.brand, catalog: vehicle.catalog, engine: engine, year: year, ssd: vehicle.ssd ? vehicle.ssd.substring(0, 50) + '...' : 'нет SSD', modification: modification, model: vehicle.model, // Дополнительные характеристики для проверки transmission: vehicle.transmission, market: vehicle.market, framecolor: vehicle.framecolor, trimcolor: vehicle.trimcolor, date: vehicle.date, manufactured: vehicle.manufactured, prodRange: vehicle.prodRange, prodPeriod: vehicle.prodPeriod, engine_info: vehicle.engine_info, engineno: vehicle.engineno }) console.log('📊 Финальный объект автомобиля перед возвратом:', JSON.stringify(vehicle, null, 2)) vehicles.push(vehicle) } console.log(`✅ Успешно обработано ${vehicles.length} автомобилей`) return vehicles } /** * Парсит ответ информации об автомобиле */ private parseVehicleInfoResponse(xmlText: string): LaximoVehicleInfo | null { console.log('🔍 parseVehicleInfoResponse - начинаем парсинг...') console.log('📄 XML длина:', xmlText.length) console.log('📄 XML первые 500 символов:', xmlText.substring(0, 500)) const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь resultData') return null } console.log('📋 resultData первые 500 символов:', resultData.substring(0, 500)) const rowMatch = resultData.match(/]*)>([\s\S]*?)<\/row>/) if (!rowMatch) { console.log('❌ Не найден тег ') return null } console.log('✅ Найден тег ') const attributes = rowMatch[1] const content = rowMatch[2] console.log('📋 Атрибуты row:', attributes) const getAttribute = (name: string): string => { const match = attributes.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } const vehicleid = getAttribute('vehicleid') const name = getAttribute('name') const ssd = getAttribute('ssd') const brand = getAttribute('brand') const catalog = getAttribute('catalog') console.log('🔍 Извлеченные атрибуты:', { vehicleid, name, brand, catalog, ssd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует', ssdLength: ssd?.length }) // Парсим атрибуты автомобиля const vehicleAttributes: LaximoVehicleAttribute[] = [] const attributeMatches = content.match(/]*\/?>|]*>[\s\S]*?<\/attribute>/g) if (attributeMatches) { for (const attrMatch of attributeMatches) { const attrTagMatch = attrMatch.match(/]*)>/); if (!attrTagMatch) continue; const attrAttributes = attrTagMatch[1]; const getAttrAttribute = (name: string): string => { const match = attrAttributes.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } vehicleAttributes.push({ key: getAttrAttribute('key'), name: getAttrAttribute('name'), value: getAttrAttribute('value') }) } } return { vehicleid: getAttribute('vehicleid'), name: getAttribute('name'), ssd: getAttribute('ssd'), brand: getAttribute('brand'), catalog: getAttribute('catalog') || '', attributes: vehicleAttributes } } /** * Извлекает данные результата из XML */ protected extractResultData(xmlText: string): string | null { console.log('🔍 Извлекаем данные результата из XML...') console.log('📄 XML длина:', xmlText.length) const soapResultMatch = xmlText.match(/]*>([\s\S]*?)<\/ns:return>/) || xmlText.match(/]*>([\s\S]*?)<\/return>/) const responseMatch = xmlText.match(/]*>([\s\S]*?)<\/response>/) console.log('🎯 soapResultMatch найден:', !!soapResultMatch) console.log('🎯 responseMatch найден:', !!responseMatch) if (soapResultMatch) { const result = soapResultMatch[1] .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/&/g, '&') console.log('📋 Обработанный результат SOAP длина:', result.length) console.log('📋 Первые 500 символов:', result.substring(0, 500)) return result } else if (responseMatch) { console.log('📋 Результат response длина:', responseMatch[1].length) return responseMatch[1] } console.log('❌ Данные результата не найдены') return null } /** * Парсит опции wizard */ private parseWizardOptions(rowXml: string): LaximoWizardOption[] { const options: LaximoWizardOption[] = [] const optionsMatch = rowXml.match(/]*>([\s\S]*?)<\/options>/) if (!optionsMatch) return options const optionsData = optionsMatch[1] const optionMatches = optionsData.match(/]*\/?>|]*>[\s\S]*?<\/row>/g) if (!optionMatches) return options for (const optionMatch of optionMatches) { const getAttribute = (name: string): string => { const match = optionMatch.match(new RegExp(`${name}="([^"]*)"`, 'i')) return match ? match[1] : '' } options.push({ key: getAttribute('key'), value: getAttribute('value') }) } return options } /** * Парсит разрешения */ private parsePermissions(xmlText: string): string[] { const permissions: string[] = [] const permissionsMatch = xmlText.match(/]*>([\s\S]*?)<\/permissions>/) if (!permissionsMatch) return permissions const permissionsData = permissionsMatch[1] const permissionMatches = permissionsData.match(/]*>([\s\S]*?)<\/permission>/g) if (!permissionMatches) return permissions for (const permissionMatch of permissionMatches) { const content = permissionMatch.replace(/<[^>]*>/g, '').trim() if (content) { permissions.push(content) } } return permissions } /** * Парсит ответ ListQuickGroup */ private parseListQuickGroupResponse(xmlText: string): LaximoQuickGroup[] { console.log('🔍 Парсим группы быстрого поиска...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return [] } // Ищем секцию ListQuickGroups const quickGroupsMatch = resultData.match(/]*>([\s\S]*?)<\/ListQuickGroups?>/) || resultData.match(/]*>([\s\S]*?)<\/ListQuickGroup>/) if (!quickGroupsMatch) { console.log('❌ Не найдена секция ListQuickGroups') return [] } console.log('✅ Найдена секция ListQuickGroups') const xmlContent = quickGroupsMatch[1] // Ищем корневую группу с quickgroupid="0" (может называться по-разному) const rootGroupPattern = /]*quickgroupid="0"[^>]*>/ const rootMatch = xmlContent.match(rootGroupPattern) if (!rootMatch) { console.log('❌ Не найдена корневая группа с quickgroupid="0"') console.log('🔍 Контент для поиска (первые 200 символов):') console.log(xmlContent.substring(0, 200)) return [] } console.log('✅ Найдена корневая группа:', rootMatch[0]) // Начинаем парсить содержимое после корневой группы const rootIndex = xmlContent.indexOf(rootMatch[0]) const afterRootTag = rootIndex + rootMatch[0].length // Парсим полную иерархию начиная с корневой группы const allCategories = this.parseRowHierarchy(xmlContent, afterRootTag) console.log(`📊 Найдено основных категорий: ${allCategories.length}`) allCategories.forEach((category, index) => { const childrenCount = this.countAllChildren(category) console.log(`${index + 1}. ${category.name} (ID: ${category.quickgroupid}, link: ${category.link}, подкатегорий: ${childrenCount})`) }) // Возвращаем все категории с их полной иерархией return allCategories } /** * Рекурсивно парсит иерархию row элементов */ private parseRowHierarchy(xmlContent: string, startPos: number): LaximoQuickGroup[] { const categories: LaximoQuickGroup[] = [] let pos = startPos while (pos < xmlContent.length) { // Ищем следующий открывающий тег ', rowStart) if (rowTagEnd === -1) break const rowTag = xmlContent.substring(rowStart, rowTagEnd + 1) // Парсим текущую группу const group = this.parseRowTagToGroup(rowTag) if (!group) { pos = rowTagEnd + 1 continue } console.log(`📦 Найден элемент: ${group.name} (ID: ${group.quickgroupid}, link: ${group.link})`) // Если это самозакрывающийся тег if (rowTag.endsWith('/>')) { categories.push(group) pos = rowTagEnd + 1 continue } // Ищем соответствующий закрывающий тег const closeTagPos = this.findMatchingCloseTag(xmlContent, rowTagEnd + 1) if (closeTagPos > rowTagEnd + 1) { // Есть содержимое между открывающим и закрывающим тегами const innerContent = xmlContent.substring(rowTagEnd + 1, closeTagPos) if (innerContent.includes('" } return categories } /** * Находит позицию соответствующего закрывающего тега */ private findMatchingCloseTag(xmlContent: string, startPos: number): number { let depth = 1 let pos = startPos while (pos < xmlContent.length && depth > 0) { const nextOpen = xmlContent.indexOf('', pos) // Если больше нет тегов if (nextClose === -1) break // Если есть открывающий тег раньше закрывающего if (nextOpen !== -1 && nextOpen < nextClose) { // Проверяем, что это не самозакрывающийся тег const tagEnd = xmlContent.indexOf('>', nextOpen) if (tagEnd !== -1) { const tag = xmlContent.substring(nextOpen, tagEnd + 1) if (!tag.endsWith('/>')) { depth++ } } pos = nextOpen + 4 // Длина "" } } return startPos } /** * Подсчитывает общее количество дочерних элементов рекурсивно */ private countAllChildren(group: LaximoQuickGroup): number { if (!group.children || group.children.length === 0) { return 0 } let count = group.children.length group.children.forEach(child => { count += this.countAllChildren(child) }) return count } /** * Парсит row тег и извлекает атрибуты */ private parseRowTagToGroup(rowTag: string): LaximoQuickGroup | null { try { const quickgroupid = this.extractAttribute(rowTag, 'quickgroupid') const name = this.extractAttribute(rowTag, 'name') const linkStr = this.extractAttribute(rowTag, 'link') const code = this.extractAttribute(rowTag, 'code') const imageurl = this.extractAttribute(rowTag, 'imageurl') const largeimageurl = this.extractAttribute(rowTag, 'largeimageurl') if (!quickgroupid || !name) { return null } return { quickgroupid, name, link: linkStr === 'true', code: code || undefined, imageurl: imageurl || undefined, largeimageurl: largeimageurl || undefined, children: undefined } } catch (error) { console.error('❌ Ошибка парсинга row тега:', error) return null } } /** * Получает список деталей в выбранной группе быстрого поиска */ async getListQuickDetail(catalogCode: string, vehicleId: string, quickGroupId: string, ssd?: string): Promise { try { console.log('🔍 Получаем детали группы быстрого поиска:', quickGroupId) console.log('📋 Параметры:', { catalogCode, vehicleId, quickGroupId, ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует' }) if (!ssd || ssd.trim() === '') { console.log('❌ SSD обязателен для ListQuickDetail') throw new Error('SSD parameter is required for ListQuickDetail') } const command = `ListQuickDetail:Locale=ru_RU|Catalog=${catalogCode}|VehicleId=${vehicleId}|QuickGroupId=${quickGroupId}|ssd=${ssd}|Localized=true|All=1` const hmac = this.createHMAC(command) console.log('📝 ListQuickDetail Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') return this.parseListQuickDetailResponse(xmlText, quickGroupId) } catch (error) { console.error('Ошибка получения деталей группы быстрого поиска:', error) throw error } } /** * Парсит ответ ListQuickDetail */ private parseListQuickDetailResponse(xmlText: string, quickGroupId: string): LaximoQuickDetail | null { console.log('🔍 Парсим детали группы быстрого поиска...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return null } // Декодируем HTML entities в XML для правильного парсинга const decodedXML = resultData .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") // Ищем секцию ListQuickDetail в декодированном XML const quickDetailMatch = decodedXML.match(/]*>([\s\S]*?)<\/ListQuickDetail>/) || decodedXML.match(/]*>([\s\S]*?)<\/response>/) if (!quickDetailMatch) { console.log('❌ Не найдена секция ListQuickDetail') return null } const quickDetail: LaximoQuickDetail = { quickgroupid: quickGroupId, name: '', units: [] } // Ищем категории (Category) const categoryPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/Category>)/g let categoryMatch while ((categoryMatch = categoryPattern.exec(quickDetailMatch[1])) !== null) { const categoryAttributes = categoryMatch[1] const categoryContent = categoryMatch[2] || '' const categoryName = this.extractAttribute(categoryAttributes, 'name') console.log('📂 Найдена категория:', categoryName) // В каждой категории ищем узлы (Unit) const unitPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/Unit>)/g let unitMatch while ((unitMatch = unitPattern.exec(categoryContent)) !== null) { const unitAttributes = unitMatch[1] const unitContent = unitMatch[2] || '' const unitId = this.extractAttribute(unitAttributes, 'unitid') const unitName = this.extractAttribute(unitAttributes, 'name') const unitCode = this.extractAttribute(unitAttributes, 'code') let imageUrl = this.extractAttribute(unitAttributes, 'imageurl') let largeImageUrl = this.extractAttribute(unitAttributes, 'largeimageurl') // Декодируем HTML entities в URL изображений if (imageUrl) { imageUrl = imageUrl.replace(/&/g, '&') } if (largeImageUrl) { largeImageUrl = largeImageUrl.replace(/&/g, '&') } console.log('🔧 Найден узел:', { unitId, unitName, unitCode, imageUrl, largeImageUrl }) const unit: LaximoUnit = { unitid: unitId, name: unitName, code: unitCode, imageurl: imageUrl || undefined, largeimageurl: largeImageUrl || undefined, details: [] } console.log('🔍 Содержимое узла (первые 1000 символов):') console.log(unitContent.substring(0, 1000)) console.log('🔍 Ищем детали в содержимом узла...') // ВАЖНО: Детали могут быть как внутри Unit content, так и в атрибутах самого Unit // Сначала ищем в содержимом узла const detailTagCount = (unitContent.match(/ = [] // ИСПРАВЛЕНО: Ищем все теги Detail (самозакрывающиеся и полные) в одном регулярном выражении const detailPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/Detail>)/g let match while ((match = detailPattern.exec(searchContent)) !== null) { detailMatches.push({ attributes: match[1] || '', content: match[2] || '', fullMatch: match[0] }) } console.log(`🔍 Найдено ${detailMatches.length} элементов Detail в узле`) let detailCount = 0 for (const detailMatch of detailMatches) { detailCount++ const detailAttributes = detailMatch.attributes || '' const detailContent = detailMatch.content || '' console.log(`🔍 Raw detail match #${detailCount}:`) console.log('Attributes:', detailAttributes.substring(0, 200)) if (detailContent) { console.log('Content:', detailContent.substring(0, 200)) } const detailId = this.extractAttribute(detailAttributes, 'detailid') const detailName = this.extractAttribute(detailAttributes, 'name') const oem = this.extractAttribute(detailAttributes, 'oem') const brand = this.extractAttribute(detailAttributes, 'brand') const codeonimage = this.extractAttribute(detailAttributes, 'codeonimage') console.log(`🔩 Найдена деталь #${detailCount}: ${detailName} (${oem})`) const detail: LaximoDetail = { detailid: detailId || codeonimage || oem, name: detailName, oem: oem, brand: brand, attributes: [] } // Парсим атрибуты детали из тега Detail const amount = this.extractAttribute(detailAttributes, 'amount') const dateRange = this.extractAttribute(detailAttributes, 'date_range') const matchAttr = this.extractAttribute(detailAttributes, 'match') const range = this.extractAttribute(detailAttributes, 'range') const ssdAttr = this.extractAttribute(detailAttributes, 'ssd') // Добавляем все найденные атрибуты в деталь if (amount) { detail.amount = amount detail.attributes?.push({ key: 'amount', name: 'Количество', value: amount }) } if (dateRange) { detail.dateRange = dateRange detail.attributes?.push({ key: 'date_range', name: 'date_range', value: dateRange }) } if (matchAttr) { detail.match = matchAttr === 't' || matchAttr === 'true' } if (range) { detail.range = range detail.attributes?.push({ key: 'range', name: 'Диапазон', value: range }) } if (ssdAttr) { detail.ssd = ssdAttr } if (codeonimage) { detail.codeonimage = codeonimage } // Парсим дополнительные атрибуты из содержимого детали const attributePattern = /]*?)(?:\s*\/>)/g let attrMatch while ((attrMatch = attributePattern.exec(detailContent)) !== null) { const attrAttributes = attrMatch[1] const key = this.extractAttribute(attrAttributes, 'key') const name = this.extractAttribute(attrAttributes, 'name') const value = this.extractAttribute(attrAttributes, 'value') if (key === 'applicablemodels') { detail.applicablemodels = value } else if (key === 'note') { detail.note = value } else { detail.attributes?.push({ key, name: name || key, value }) } } unit.details!.push(detail) } console.log(`📊 Найдено деталей в узле "${unitName}": ${detailCount}`) // Если детали не найдены, попробуем альтернативный подход if (detailCount === 0) { console.log('🔍 Детали не найдены стандартным способом, пробуем альтернативный парсинг...') // Поиск деталей в исходном XML блоке (до декодирования) const originalUnitBlock = unitMatch[0] console.log('🔍 Исходный XML блок (первые 1000 символов):') console.log(originalUnitBlock.substring(0, 1000)) // Ищем детали в исходном формате const originalDetailPattern = /]*(?:\s*\/>|>[\s\S]*?<\/Detail>)/g let originalMatch let altDetailCount = 0 while ((originalMatch = originalDetailPattern.exec(originalUnitBlock)) !== null) { altDetailCount++ const detailTag = originalMatch[0] console.log(`🔍 Альтернативный парсинг - найдена деталь #${altDetailCount}:`) console.log(detailTag.substring(0, 300)) // Простой парсинг атрибутов из тега const nameMatch = detailTag.match(/name="([^"]*)"/) const oemMatch = detailTag.match(/oem="([^"]*)"/) const codeMatch = detailTag.match(/codeonimage="([^"]*)"/) const amountMatch = detailTag.match(/amount="([^"]*)"/) if (nameMatch || oemMatch) { const altDetail: LaximoDetail = { detailid: codeMatch?.[1] || oemMatch?.[1] || `alt_${altDetailCount}`, name: nameMatch?.[1] || 'Неизвестная деталь', oem: oemMatch?.[1] || '', attributes: [] } if (amountMatch) { altDetail.attributes?.push({ key: 'amount', name: 'Количество', value: amountMatch[1] }) } unit.details!.push(altDetail) console.log(`🔩 Альтернативный парсинг - добавлена деталь: ${altDetail.name} (${altDetail.oem})`) } } if (altDetailCount > 0) { console.log(`✅ Альтернативный парсинг нашел ${altDetailCount} деталей`) } } quickDetail.units!.push(unit) } } // Если структура не найдена, пробуем альтернативный формат if (quickDetail.units!.length === 0) { console.log('🔄 Пробуем альтернативный формат парсинга...') // Ищем узлы напрямую в декодированном XML const directUnitPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/row>)/g let directUnitMatch while ((directUnitMatch = directUnitPattern.exec(quickDetailMatch[1])) !== null) { const unitAttributes = directUnitMatch[1] const unitContent = directUnitMatch[2] || '' const unitId = this.extractAttribute(unitAttributes, 'unitid') || this.extractAttribute(unitAttributes, 'id') const unitName = this.extractAttribute(unitAttributes, 'name') || this.extractAttribute(unitAttributes, 'description') if (unitId && unitName) { console.log('🔧 Найден узел (прямой формат):', { unitId, unitName }) const unit: LaximoUnit = { unitid: unitId, name: unitName, details: [] } quickDetail.units!.push(unit) } } } console.log(`✅ Обработано ${quickDetail.units!.length} узлов в группе ${quickGroupId}`) // Подсчитываем общее количество деталей const totalDetails = quickDetail.units!.reduce((total, unit) => total + (unit.details?.length || 0), 0) console.log(`📊 Общее количество деталей: ${totalDetails}`) // Выводим детальную информацию о каждом узле quickDetail.units!.forEach((unit, index) => { console.log(`📦 Узел #${index + 1}: ${unit.name} (${unit.details?.length || 0} деталей)`) unit.details?.forEach((detail, detailIndex) => { const amountAttr = detail.attributes?.find(attr => attr.key === 'amount') const amountStr = amountAttr ? ` - ${amountAttr.value}` : '' console.log(` 🔩 Деталь #${detailIndex + 1}: ${detail.name} (${detail.oem})${amountStr}`) }) }) // Дополнительная диагностика if (totalDetails === 0) { console.log('⚠️ НЕ НАЙДЕНО НИ ОДНОЙ ДЕТАЛИ!') console.log('🔍 Оригинальные данные (первые 1000 символов):') console.log(resultData?.substring(0, 1000)) console.log('🔍 Декодированные данные (первые 1000 символов):') console.log(decodedXML.substring(0, 1000)) } else { console.log(`✅ Успешно найдено ${totalDetails} деталей`) } if (quickDetail.units!.length === 0) { return null } // Устанавливаем имя группы если оно не было установлено if (!quickDetail.name && quickDetail.units!.length > 0) { quickDetail.name = `Группа ${quickGroupId}` } return quickDetail } /** * Поиск деталей по OEM номеру для выбранного автомобиля */ async getOEMPartApplicability(catalogCode: string, vehicleId: string, oemNumber: string, ssd?: string): Promise { try { console.log('🔍 Поиск детали по OEM номеру:', oemNumber) console.log('📋 Параметры:', { catalogCode, vehicleId, oemNumber, ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует' }) if (!ssd || ssd.trim() === '') { console.log('❌ SSD обязателен для GetOEMPartApplicability') throw new Error('SSD parameter is required for GetOEMPartApplicability') } const command = `GetOEMPartApplicability:Locale=ru_RU|Catalog=${catalogCode}|VehicleId=${vehicleId}|OEM=${oemNumber}|ssd=${ssd}` const hmac = this.createHMAC(command) console.log('📝 GetOEMPartApplicability Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') return this.parseOEMPartApplicabilityResponse(xmlText, oemNumber) } catch (error) { console.error('Ошибка поиска детали по OEM номеру:', error) throw error } } /** * Поиск деталей по названию для выбранного автомобиля */ async searchVehicleDetails(catalogCode: string, vehicleId: string, searchQuery: string, ssd?: string): Promise { try { console.log('🔍 Поиск деталей по названию:', searchQuery) console.log('📋 Параметры:', { catalogCode, vehicleId, searchQuery, ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует' }) // Для поиска по конкретному автомобилю (vehicleId != 0) SSD обязателен // Для поиска по каталогу (vehicleId = 0) SSD может отсутствовать if (vehicleId !== '0' && (!ssd || ssd.trim() === '')) { console.log('❌ SSD обязателен для поиска по конкретному автомобилю') throw new Error('SSD parameter is required for vehicle-specific search') } // Попробуем разные варианты кодировки поискового запроса const searchQueries = [ searchQuery, // Оригинальный запрос encodeURIComponent(searchQuery), // URL кодирование searchQuery.toLowerCase(), // В нижнем регистре searchQuery.toUpperCase() // В верхнем регистре ] // Добавляем английские переводы для популярных терминов const translations: { [key: string]: string[] } = { 'фильтр': ['filter'], 'масляный': ['oil'], 'воздушный': ['air'], 'топливный': ['fuel'], 'тормозной': ['brake'], 'амортизатор': ['shock', 'absorber'], 'сцепление': ['clutch'], 'ремень': ['belt'], 'свеча': ['spark plug', 'plug'], 'датчик': ['sensor'], 'насос': ['pump'], 'радиатор': ['radiator'], 'термостат': ['thermostat'] } // Добавляем переводы если они есть const lowerQuery = searchQuery.toLowerCase() for (const [russian, english] of Object.entries(translations)) { if (lowerQuery.includes(russian)) { searchQueries.push(...english) searchQueries.push(...english.map(e => e.toUpperCase())) } } console.log('🔄 Попробуем разные варианты запроса:', searchQueries) for (const query of searchQueries) { console.log(`🔍 Пробуем поисковый запрос: "${query}"`) // Формируем команду с SSD или без него let command: string if (ssd && ssd.trim() !== '') { command = `SearchVehicleDetails:Locale=ru_RU|Catalog=${catalogCode}|VehicleId=${vehicleId}|Query=${query}|ssd=${ssd}` } else { command = `SearchVehicleDetails:Locale=ru_RU|Catalog=${catalogCode}|VehicleId=${vehicleId}|Query=${query}` } const hmac = this.createHMAC(command) console.log('📝 SearchVehicleDetails Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') const result = this.parseSearchVehicleDetailsResponse(xmlText, query) if (result && result.details.length > 0) { console.log(`✅ Найдены результаты для запроса "${query}":`, result.details.length) return result } else { console.log(`❌ Нет результатов для запроса "${query}"`) } } // Если ни один запрос не дал результатов, попробуем поиск без SSD (для всего каталога) if (ssd && vehicleId !== '0') { console.log('🔄 Пробуем поиск по всему каталогу без SSD...') const catalogCommand = `SearchVehicleDetails:Locale=ru_RU|Catalog=${catalogCode}|VehicleId=0|Query=${encodeURIComponent(searchQuery)}` const catalogHmac = this.createHMAC(catalogCommand) console.log('📝 Catalog SearchVehicleDetails Command:', catalogCommand) console.log('🔗 Catalog HMAC:', catalogHmac) const catalogSoapEnvelope = this.createSOAP11Envelope(catalogCommand, this.login, catalogHmac) const catalogXmlText = await this.makeBasicSOAPRequest(this.soap11Url, catalogSoapEnvelope, 'urn:QueryDataLogin') const catalogResult = this.parseSearchVehicleDetailsResponse(catalogXmlText, searchQuery) if (catalogResult && catalogResult.details.length > 0) { console.log(`✅ Найдены результаты в каталоге:`, catalogResult.details.length) return catalogResult } } console.log('❌ Поиск не дал результатов') return null } catch (error) { console.error('Ошибка поиска деталей по названию:', error) throw error } } /** * Парсит ответ GetOEMPartApplicability */ private parseOEMPartApplicabilityResponse(xmlText: string, oemNumber: string): LaximoOEMResult | null { console.log('🔍 Парсим результаты поиска по OEM номеру...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return null } // Ищем секцию GetOEMPartApplicability const oemResultMatch = resultData.match(/]*>([\s\S]*?)<\/GetOEMPartApplicability>/) || resultData.match(/]*>([\s\S]*?)<\/response>/) if (!oemResultMatch) { console.log('❌ Не найдена секция GetOEMPartApplicability') return null } const oemResult: LaximoOEMResult = { oemNumber: oemNumber, categories: [] } // Ищем категории (Category) const categoryPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/Category>)/g let categoryMatch while ((categoryMatch = categoryPattern.exec(oemResultMatch[1])) !== null) { const categoryAttributes = categoryMatch[1] const categoryContent = categoryMatch[2] || '' const categoryId = this.extractAttribute(categoryAttributes, 'categoryid') const categoryName = this.extractAttribute(categoryAttributes, 'name') console.log('📂 Найдена категория:', { categoryId, categoryName }) const category: LaximoOEMCategory = { categoryid: categoryId, name: categoryName, units: [] } // В каждой категории ищем узлы (Unit) const unitPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/Unit>)/g let unitMatch while ((unitMatch = unitPattern.exec(categoryContent)) !== null) { const unitAttributes = unitMatch[1] const unitContent = unitMatch[2] || '' const unitId = this.extractAttribute(unitAttributes, 'unitid') const unitName = this.extractAttribute(unitAttributes, 'name') const unitCode = this.extractAttribute(unitAttributes, 'code') const imageUrl = this.extractAttribute(unitAttributes, 'imageurl') console.log('🔧 Найден узел:', { unitId, unitName, unitCode }) const unit: LaximoOEMUnit = { unitid: unitId, name: unitName, code: unitCode, imageurl: imageUrl, details: [] } // В каждом узле ищем детали (Detail) - поддерживаем как самозакрывающиеся, так и полные теги // Используем более простой подход: сначала найдем все вхождения тега Detail console.log('🔍 Поиск деталей в полном блоке Unit (первые 500 символов):') console.log(unitMatch[0].substring(0, 500)) // Ищем все теги Detail внутри текущего Unit const detailPattern = /]*(?:\s*\/>|>[^<]*<\/Detail>)/g let detailMatch let detailCount = 0 // Получаем содержимое Unit для поиска деталей const unitContentForDetails = unitMatch[2] || '' // Ищем все детали в содержимом Unit const allDetailMatches = [...unitContentForDetails.matchAll(/]*?)(?:\s*\/>)/g)] console.log(`🔍 Найдено ${allDetailMatches.length} самозакрывающихся тегов Detail в Unit`) for (const detailMatch of allDetailMatches) { const detailAttributes = detailMatch[1] console.log(`🔍 Raw detail match #${detailCount + 1}:`) console.log('Attributes:', detailAttributes.substring(0, 150)) // Извлекаем основные атрибуты детали const name = this.extractAttribute(detailAttributes, 'name') const oem = this.extractAttribute(detailAttributes, 'oem') const amount = this.extractAttribute(detailAttributes, 'amount') const codeonimage = this.extractAttribute(detailAttributes, 'codeonimage') const match = this.extractAttribute(detailAttributes, 'match') const dateRange = this.extractAttribute(detailAttributes, 'date_range') const ssd = this.extractAttribute(detailAttributes, 'ssd') if (name && oem) { const detail: LaximoDetail = { detailid: `${unitId}_${detailCount}`, name, oem, brand: '', amount: amount || '1pcs', range: dateRange || '', codeonimage: codeonimage || '', match: match === 't', dateRange: dateRange || '', ssd: ssd || '', applicablemodels: '', note: '', attributes: [] } console.log(`🔩 Найдена деталь #${detailCount + 1}: ${detail.name} (${detail.oem})`) unit.details!.push(detail) detailCount++ } } category.units.push(unit) } oemResult.categories.push(category) } // Если категории не найдены, пробуем альтернативный формат if (oemResult.categories.length === 0) { console.log('🔄 Пробуем альтернативный формат парсинга OEM результатов...') // Ищем узлы напрямую const directUnitPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/row>)/g let directUnitMatch while ((directUnitMatch = directUnitPattern.exec(oemResultMatch[1])) !== null) { const unitAttributes = directUnitMatch[1] const unitId = this.extractAttribute(unitAttributes, 'unitid') || this.extractAttribute(unitAttributes, 'id') const unitName = this.extractAttribute(unitAttributes, 'name') || this.extractAttribute(unitAttributes, 'description') const oem = this.extractAttribute(unitAttributes, 'oem') if (unitId && unitName && oem) { console.log('🔧 Найден результат (прямой формат):', { unitId, unitName, oem }) // Создаем категорию по умолчанию if (oemResult.categories.length === 0) { oemResult.categories.push({ categoryid: 'default', name: 'Результаты поиска', units: [] }) } const unit: LaximoOEMUnit = { unitid: unitId, name: unitName, details: [{ detailid: unitId, name: unitName, oem: oem }] } oemResult.categories[0].units.push(unit) } } } console.log(`✅ Найдено ${oemResult.categories.length} категорий для OEM ${oemNumber}`) if (oemResult.categories.length === 0) { return null } return oemResult } /** * Парсит ответ SearchVehicleDetails */ private parseSearchVehicleDetailsResponse(xmlText: string, searchQuery: string): LaximoFulltextSearchResult | null { console.log('🔍 Парсим результаты поиска по названию деталей...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return null } console.log('📄 XML длина:', xmlText.length) console.log('📋 Первые 500 символов результата:', resultData.substring(0, 500)) // Сначала сохраним полный XML для отладки console.log('🔍 Полный XML ответ для анализа:') console.log('===== НАЧАЛО XML =====') console.log(xmlText) console.log('===== КОНЕЦ XML =====') // Ищем секцию SearchVehicleDetails const searchResultMatch = resultData.match(/]*>([\s\S]*?)<\/SearchVehicleDetails>/) if (!searchResultMatch) { console.log('❌ Не найдена секция SearchVehicleDetails') // Попробуем найти альтернативные секции const alternativeMatches = [ resultData.match(/]*>([\s\S]*?)<\/Details>/), resultData.match(/]*>([\s\S]*?)<\/Parts>/), resultData.match(/]*>([\s\S]*?)<\/Items>/), resultData.match(/]*>([\s\S]*?)<\/SearchResult>/) ] for (let i = 0; i < alternativeMatches.length; i++) { const match = alternativeMatches[i] if (match) { console.log(`✅ Найдена альтернативная секция ${i + 1}:`, match[0].substring(0, 100)) const searchContent = match[1].trim() return this.parseSearchContent(searchContent, searchQuery) } } return null } const searchContent = searchResultMatch[1].trim() console.log('📋 Содержимое SearchVehicleDetails:', searchContent) // Проверяем на пустой результат if (searchContent === '') { console.log('⚠️ SearchVehicleDetails пуст - полнотекстовый поиск не дал результатов или не поддерживается каталогом') return null } return this.parseSearchContent(searchContent, searchQuery) } /** * Парсит содержимое поиска в различных форматах */ private parseSearchContent(searchContent: string, searchQuery: string): LaximoFulltextSearchResult | null { const searchResult: LaximoFulltextSearchResult = { searchQuery: searchQuery, details: [] } console.log('🔍 Начинаем парсинг содержимого поиска...') console.log('📋 Содержимое для парсинга (первые 1000 символов):', searchContent.substring(0, 1000)) // Формат 1: согласно документации Laximo - Труба маслоналивная const documentationRowPattern = /]*>(.*?)<\/row>/g let docRowMatch while ((docRowMatch = documentationRowPattern.exec(searchContent)) !== null) { const oem = docRowMatch[1] const name = docRowMatch[2].trim() console.log('🔩 Найдена деталь (формат документации):', { oem, name }) if (oem && name) { searchResult.details.push({ oem: oem, name: name }) } } // Формат 2: Альтернативный формат с атрибутами в row if (searchResult.details.length === 0) { console.log('🔄 Пробуем альтернативный формат с атрибутами...') const rowPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/row>)/g let rowMatch while ((rowMatch = rowPattern.exec(searchContent)) !== null) { const rowAttributes = rowMatch[1] const rowContent = rowMatch[2] || '' console.log('🔍 Найден тег row:', { attributes: rowAttributes, content: rowContent }) const oem = this.extractAttribute(rowAttributes, 'oem') || this.extractAttribute(rowAttributes, 'code') || this.extractAttribute(rowAttributes, 'articul') || this.extractAttribute(rowAttributes, 'article') const name = this.extractAttribute(rowAttributes, 'name') || this.extractAttribute(rowAttributes, 'description') || this.extractAttribute(rowAttributes, 'title') || rowContent.trim() const brand = this.extractAttribute(rowAttributes, 'brand') || this.extractAttribute(rowAttributes, 'manufacturer') console.log('🔩 Найдена деталь (формат атрибутов):', { oem, name, brand }) if (oem && name) { searchResult.details.push({ oem: oem, name: name, brand: brand, description: this.extractAttribute(rowAttributes, 'description') }) } } } // Формат 3: Поиск отдельных атрибутов oem="XXX" name="YYY" if (searchResult.details.length === 0) { console.log('🔄 Пробуем поиск отдельных атрибутов...') const oemPattern = /(?:oem|code|articul|article)="([^"]+)"/gi const namePattern = /(?:name|description|title)="([^"]+)"/gi let oemMatch const oems: string[] = [] while ((oemMatch = oemPattern.exec(searchContent)) !== null) { oems.push(oemMatch[1]) } let nameMatch const names: string[] = [] while ((nameMatch = namePattern.exec(searchContent)) !== null) { names.push(nameMatch[1]) } console.log('🔍 Найдено OEM номеров:', oems.length) console.log('🔍 Найдено названий:', names.length) // Сопоставляем OEM и названия for (let i = 0; i < Math.min(oems.length, names.length); i++) { console.log('🔩 Найдена деталь (отдельные атрибуты):', { oem: oems[i], name: names[i] }) searchResult.details.push({ oem: oems[i], name: names[i] }) } } // Формат 4: Простой текстовый формат или список строк if (searchResult.details.length === 0) { console.log('🔄 Пробуем простой текстовый формат...') // Ищем строки формата "номер - название" или "номер название" const textPattern = /([A-Z0-9]+)[\s\-]+(.+)/g const lines = searchContent.split(/[\r\n]+/) for (const line of lines) { const trimmedLine = line.trim() if (trimmedLine.length > 5) { // Минимальная длина для валидной строки const match = textPattern.exec(trimmedLine) if (match) { const oem = match[1].trim() const name = match[2].trim() console.log('🔩 Найдена деталь (текстовый формат):', { oem, name }) if (oem && name) { searchResult.details.push({ oem: oem, name: name }) } } } textPattern.lastIndex = 0 // Сброс регекса для следующей итерации } } console.log(`✅ Найдено ${searchResult.details.length} деталей по запросу "${searchQuery}"`) if (searchResult.details.length === 0) { console.log('⚠️ Не удалось распарсить детали из ответа Laximo') return null } return searchResult } /** * Поиск автомобиля по государственному номеру (в конкретном каталоге) * @see https://doc.laximo.ru/ru/cat/FindVehicleByPlateNumber */ async findVehicleByPlateNumber(catalogCode: string, plateNumber: string): Promise { try { console.log('🔍 Поиск автомобиля по госномеру в каталоге:', plateNumber, catalogCode) const command = `FindVehicleByPlateNumber:Locale=ru_RU|Catalog=${catalogCode}|PlateNumber=${plateNumber}|CountryCode=ru|Localized=true` const hmac = this.createHMAC(command) console.log('📝 FindVehicleByPlateNumber Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') const vehicles = this.parseVehicleSearchResponse(xmlText) console.log(`✅ Найдено ${vehicles.length} автомобилей по госномеру в каталоге ${catalogCode}`) return vehicles } catch (error) { console.error('❌ Ошибка поиска автомобиля по госномеру:', error) return [] } } /** * Глобальный поиск автомобиля по государственному номеру (без указания каталога) * @see https://doc.laximo.ru/ru/cat/FindVehicleByPlateNumber */ async findVehicleByPlateNumberGlobal(plateNumber: string): Promise { try { console.log('🔍 Глобальный поиск автомобиля по госномеру:', plateNumber) const command = `FindVehicleByPlateNumber:Locale=ru_RU|PlateNumber=${plateNumber}|CountryCode=ru|Localized=true` const hmac = this.createHMAC(command) console.log('📝 FindVehicleByPlateNumber Global Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') const vehicles = this.parseVehicleSearchResponse(xmlText) console.log(`✅ Найдено ${vehicles.length} автомобилей по госномеру глобально`) return vehicles } catch (error) { console.error('❌ Ошибка глобального поиска автомобиля по госномеру:', error) return [] } } /** * Поиск каталогов, содержащих указанный артикул * @see https://doc.laximo.ru/ru/cat/FindPartReferences */ async findPartReferences(partNumber: string): Promise { try { console.log('🔍 Поиск каталогов по артикулу:', partNumber) const command = `FindPartReferences:Locale=ru_RU|OEM=${partNumber}` const hmac = this.createHMAC(command) console.log('📝 FindPartReferences Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') const catalogs = this.parsePartReferencesResponse(xmlText) console.log(`✅ Найдено ${catalogs.length} каталогов с артикулом ${partNumber}`) return catalogs } catch (error) { console.error('❌ Ошибка поиска каталогов по артикулу:', error) return [] } } /** * Поиск автомобилей по артикулу в указанном каталоге * @see https://doc.laximo.ru/ru/cat/FindApplicableVehicles */ async findApplicableVehicles(catalogCode: string, partNumber: string): Promise { try { console.log('🔍 Поиск автомобилей по артикулу:', partNumber, 'в каталоге:', catalogCode) const command = `FindApplicableVehicles:Locale=ru_RU|Catalog=${catalogCode}|OEM=${partNumber}` const hmac = this.createHMAC(command) console.log('📝 FindApplicableVehicles Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') const vehicles = this.parseVehicleSearchResponse(xmlText) console.log(`✅ Найдено ${vehicles.length} автомобилей по артикулу в каталоге ${catalogCode}`) return vehicles } catch (error) { console.error('❌ Ошибка поиска автомобилей по артикулу:', error) return [] } } /** * Комплексный поиск автомобилей по артикулу (двухэтапный процесс) * 1. Поиск каталогов с артикулом через FindPartReferences * 2. Поиск автомобилей в найденных каталогах через FindApplicableVehicles * @see https://doc.laximo.ru/ru/UseCases/SearchString#поиск-автомобиля-по-артикулу */ async findVehiclesByPartNumber(partNumber: string): Promise { try { console.log('🔍 Комплексный поиск автомобилей по артикулу:', partNumber) // Шаг 1: Поиск каталогов с артикулом const catalogs = await this.findPartReferences(partNumber) if (catalogs.length === 0) { console.log('❌ Каталоги с артикулом не найдены') console.log('ℹ️ Возможно, это артикул производителя запчастей, а не оригинальный OEM номер') return { partNumber, catalogs: [], totalVehicles: 0 } } console.log(`📦 Найдено ${catalogs.length} каталогов с артикулом`) // Шаг 2: Поиск автомобилей в каждом каталоге const catalogResults: LaximoCatalogVehicleResult[] = [] for (const catalogCode of catalogs) { console.log(`🔍 Поиск автомобилей в каталоге: ${catalogCode}`) try { const vehicles = await this.findApplicableVehicles(catalogCode, partNumber) if (vehicles.length > 0) { // Получаем информацию о каталоге для отображения бренда const catalogInfo = await this.getCatalogInfo(catalogCode) catalogResults.push({ catalogCode, catalogName: catalogInfo?.name || catalogCode, brand: catalogInfo?.brand || catalogCode, vehicles, vehicleCount: vehicles.length }) console.log(`✅ В каталоге ${catalogCode} найдено ${vehicles.length} автомобилей`) } else { console.log(`⚠️ В каталоге ${catalogCode} автомобили не найдены`) } } catch (error) { console.error(`❌ Ошибка поиска в каталоге ${catalogCode}:`, error) } } const totalVehicles = catalogResults.reduce((sum, catalog) => sum + catalog.vehicleCount, 0) console.log(`✅ Общий результат: найдено ${totalVehicles} автомобилей в ${catalogResults.length} каталогах`) return { partNumber, catalogs: catalogResults, totalVehicles } } catch (error) { console.error('❌ Ошибка комплексного поиска автомобилей по артикулу:', error) return { partNumber, catalogs: [], totalVehicles: 0 } } } /** * Парсит ответ поиска каталогов по артикулу */ private parsePartReferencesResponse(xmlText: string): string[] { console.log('🔍 Парсим результаты поиска каталогов по артикулу...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return [] } console.log('📄 XML длина:', xmlText.length) console.log('📋 Обработанный результат SOAP длина:', resultData.length) console.log('📋 Первые 500 символов:', resultData.substring(0, 500)) const catalogs: string[] = [] // Ищем элементы CatalogReference с атрибутом code const catalogPattern = /]*?code="([^"]*)"[^>]*?>/g let match while ((match = catalogPattern.exec(resultData)) !== null) { const catalogCode = match[1] if (catalogCode && !catalogs.includes(catalogCode)) { catalogs.push(catalogCode) console.log('📦 Найден каталог:', catalogCode) } } console.log(`✅ Обработано ${catalogs.length} каталогов`) return catalogs } /** * Извлекает значение атрибута из строки атрибутов */ protected extractAttribute(attributesString: string, attributeName: string): string { const regex = new RegExp(`${attributeName}="([^"]*)"`, 'i') const match = attributesString.match(regex) return match ? match[1] : '' } } export const laximoService = new LaximoService() export const laximoDocService = new LaximoDocService() // Добавляем методы для работы с деталями узлов export class LaximoUnitService extends LaximoService { /** * Получает информацию об узле */ async getUnitInfo(catalogCode: string, vehicleId: string, unitId: string, ssd?: string): Promise { try { console.log('🔍 Получаем информацию об узле:', unitId) console.log('📋 Параметры:', { catalogCode, vehicleId, unitId, ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует' }) // Используем GetUnitInfo согласно документации Laximo let command = `GetUnitInfo:Locale=ru_RU|Catalog=${catalogCode}|VehicleId=${vehicleId}|UnitId=${unitId}` if (ssd && ssd.trim() !== '') { command += `|ssd=${ssd}` } else { command += `|ssd=` } // Включаем локализацию для получения переведенных названий параметров command += `|Localized=true` const hmac = this.createHMAC(command) console.log('📝 GetUnitInfo Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') return this.parseUnitInfoResponse(xmlText, unitId) } catch (error) { console.error('Ошибка получения информации об узле:', error) return null } } /** * Получает детали узла используя ListDetailByUnit API */ async getUnitDetails(catalogCode: string, vehicleId: string, unitId: string, ssd?: string): Promise { try { console.log('🔍 Получаем детали узла:', unitId) console.log('📋 Параметры:', { catalogCode, vehicleId, unitId, ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует' }) // Используем ListDetailByUnit согласно документации Laximo let command = `ListDetailByUnit:Locale=ru_RU|Catalog=${catalogCode}|VehicleId=${vehicleId}|UnitId=${unitId}` if (ssd && ssd.trim() !== '') { command += `|ssd=${ssd}` } else { command += `|ssd=` } // Включаем локализацию для получения переведенных названий параметров command += `|Localized=true` // Включаем связанные объекты для получения дополнительной информации command += `|WithLinks=true` const hmac = this.createHMAC(command) console.log('📝 ListDetailByUnit Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') return this.parseUnitDetailsResponse(xmlText) } catch (error) { console.error('Ошибка получения деталей узла:', error) return [] } } /** * Получает карту изображений узла с координатами используя ListImageMapByUnit API */ async getUnitImageMap(catalogCode: string, vehicleId: string, unitId: string, ssd?: string): Promise { try { console.log('🔍 Получаем карту изображений узла:', unitId) console.log('📋 Параметры:', { catalogCode, vehicleId, unitId, ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует' }) // Используем ListImageMapByUnit согласно документации Laximo let command = `ListImageMapByUnit:Catalog=${catalogCode}|VehicleId=${vehicleId}|UnitId=${unitId}` if (ssd && ssd.trim() !== '') { command += `|ssd=${ssd}` } else { command += `|ssd=` } // Добавляем WithLinks=true согласно документации command += `|WithLinks=true` const hmac = this.createHMAC(command) console.log('📝 ListImageMapByUnit Command:', command) console.log('🔗 HMAC:', hmac) const soapEnvelope = this.createSOAP11Envelope(command, this.login, hmac) const xmlText = await this.makeBasicSOAPRequest(this.soap11Url, soapEnvelope, 'urn:QueryDataLogin') return this.parseUnitImageMapResponse(xmlText, unitId) } catch (error) { console.error('Ошибка получения карты изображений узла:', error) return null } } /** * Парсит ответ GetUnitInfo с информацией об узле */ private parseUnitInfoResponse(xmlText: string, unitId: string): LaximoUnit | null { console.log('🔍 Парсим информацию об узле...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return null } // Ищем секцию GetUnitInfo const unitInfoMatch = resultData.match(/]*>([\s\S]*?)<\/GetUnitInfo>/) || resultData.match(/]*>([\s\S]*?)<\/response>/) if (!unitInfoMatch) { console.log('❌ Не найдена секция GetUnitInfo') return null } const rowPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/row>)/g const match = rowPattern.exec(unitInfoMatch[1]) if (!match) { console.log('❌ Не найдена строка с данными узла') return null } const attributes = match[1] const content = match[2] || '' // Извлекаем атрибуты согласно документации GetUnitInfo const name = this.extractAttribute(attributes, 'name') const code = this.extractAttribute(attributes, 'code') const imageurl = this.extractAttribute(attributes, 'imageurl') const largeimageurl = this.extractAttribute(attributes, 'largeimageurl') const currentUnitId = this.extractAttribute(attributes, 'unitid') // Извлекаем атрибуты из содержимого const attributePattern = //g const unitAttributes: LaximoDetailAttribute[] = [] let attrMatch while ((attrMatch = attributePattern.exec(content)) !== null) { unitAttributes.push({ key: attrMatch[1], name: attrMatch[2], value: attrMatch[3] }) } // Ищем примечание в атрибутах const noteAttribute = unitAttributes.find(attr => attr.key === 'note') const description = noteAttribute?.value || '' console.log('📦 Найдена информация об узле:', { unitId: currentUnitId, name, code, imageurl }) console.log('📋 Атрибуты узла:', unitAttributes) return { unitid: currentUnitId || unitId, name: name || '', code: code || '', description: description, imageurl: imageurl || undefined, largeimageurl: largeimageurl || undefined, details: [], // Детали загружаются отдельно attributes: unitAttributes } } /** * Парсит ответ ListDetailByUnit с деталями узла */ private parseUnitDetailsResponse(xmlText: string): LaximoDetail[] { console.log('🔍 Парсим детали узла...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return [] } // Ищем секцию ListDetailsByUnit const detailsMatch = resultData.match(/]*>([\s\S]*?)<\/ListDetailsByUnit>/) || resultData.match(/]*>([\s\S]*?)<\/response>/) if (!detailsMatch) { console.log('❌ Не найдена секция ListDetailsByUnit') return [] } const details: LaximoDetail[] = [] const rowPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/row>)/g let match while ((match = rowPattern.exec(detailsMatch[1])) !== null) { const attributes = match[1] const content = match[2] || '' // Извлекаем атрибуты детали согласно документации ListDetailByUnit const codeonimage = this.extractAttribute(attributes, 'codeonimage') const name = this.extractAttribute(attributes, 'name') const oem = this.extractAttribute(attributes, 'oem') const ssd = this.extractAttribute(attributes, 'ssd') // Дополнительные атрибуты const note = this.extractAttribute(attributes, 'note') const filter = this.extractAttribute(attributes, 'filter') const flag = this.extractAttribute(attributes, 'flag') const match_attr = this.extractAttribute(attributes, 'match') const designation = this.extractAttribute(attributes, 'designation') const applicablemodels = this.extractAttribute(attributes, 'applicablemodels') const partspec = this.extractAttribute(attributes, 'partspec') const color = this.extractAttribute(attributes, 'color') const shape = this.extractAttribute(attributes, 'shape') const standard = this.extractAttribute(attributes, 'standard') const material = this.extractAttribute(attributes, 'material') const size = this.extractAttribute(attributes, 'size') const featuredescription = this.extractAttribute(attributes, 'featuredescription') const prodstart = this.extractAttribute(attributes, 'prodstart') const prodend = this.extractAttribute(attributes, 'prodend') // Извлекаем атрибуты из содержимого const attributePattern = //g const detailAttributes: LaximoDetailAttribute[] = [] let attrMatch while ((attrMatch = attributePattern.exec(content)) !== null) { detailAttributes.push({ key: attrMatch[1], name: attrMatch[2], value: attrMatch[3] }) } if (codeonimage && name && oem) { const detail: LaximoDetail = { detailid: codeonimage, // Используем codeonimage как detailid name, oem, brand: '', // Бренд не указан в ListDetailByUnit description: note || '', applicablemodels: applicablemodels || '', note: note || '', attributes: detailAttributes } console.log('📦 Найдена деталь узла:', { codeonimage, name, oem, note }) details.push(detail) } } console.log(`✅ Обработано ${details.length} деталей узла`) return details } /** * Парсит ответ ListImageMapByUnit с картой изображений узла */ private parseUnitImageMapResponse(xmlText: string, unitId: string): LaximoUnitImageMap | null { console.log('🔍 Парсим карту изображений узла...') const resultData = this.extractResultData(xmlText) if (!resultData) { console.log('❌ Не удалось извлечь данные результата') return null } // Ищем секцию ListImageMapByUnit const imageMapMatch = resultData.match(/]*>([\s\S]*?)<\/ListImageMapByUnit>/) || resultData.match(/]*>([\s\S]*?)<\/response>/) if (!imageMapMatch) { console.log('❌ Не найдена секция ListImageMapByUnit') return null } const coordinates: LaximoImageCoordinate[] = [] const rowPattern = /]*?)(?:\s*\/>|>([\s\S]*?)<\/row>)/g let match while ((match = rowPattern.exec(imageMapMatch[1])) !== null) { const attributes = match[1] // Извлекаем атрибуты согласно документации ListImageMapByUnit const code = this.extractAttribute(attributes, 'code') const type = this.extractAttribute(attributes, 'type') const x1 = parseInt(this.extractAttribute(attributes, 'x1') || '0') const y1 = parseInt(this.extractAttribute(attributes, 'y1') || '0') const x2 = parseInt(this.extractAttribute(attributes, 'x2') || '0') const y2 = parseInt(this.extractAttribute(attributes, 'y2') || '0') if (code) { coordinates.push({ detailid: code, // Используем code как detailid codeonimage: code, x: x1, y: y1, width: x2 - x1, height: y2 - y1, shape: type === '0' ? 'rect' : 'circle' // Предполагаем, что type=0 это прямоугольник }) console.log('📦 Найдена координата:', { code, type, x1, y1, x2, y2 }) } } console.log(`✅ Обработано ${coordinates.length} координат изображения`) // Для ListImageMapByUnit изображение получается из GetUnitInfo return { unitid: unitId, imageurl: '', // Изображение берется из GetUnitInfo largeimageurl: '', coordinates } } } // Создаем экземпляр расширенного сервиса export const laximoUnitService = new LaximoUnitService()