first commit

This commit is contained in:
Bivekich
2025-06-26 06:59:19 +03:00
commit 18e1f3ffb1
124 changed files with 52448 additions and 0 deletions

3346
src/lib/laximo-service.ts Normal file
View File

@ -0,0 +1,3346 @@
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
}
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
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 `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://Aftermarket.Kito.ec">
<soap:Body>
<ns:QueryDataLogin>
<ns:request>${command}</ns:request>
<ns:login>${login}</ns:login>
<ns:hmac>${hmac}</ns:hmac>
</ns:QueryDataLogin>
</soap:Body>
</soap:Envelope>`
}
/**
* Выполняет SOAP запрос
*/
private async makeSOAPRequest(url: string, soapEnvelope: string, soapAction: string): Promise<string> {
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 '<soap:Envelope><soap:Body><ns:return></ns:return></soap:Body></soap:Envelope>'
}
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<LaximoDocFindOEMResult | null> {
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(/<ns:return[^>]*>([\s\S]*?)<\/ns:return>/) ||
xmlText.match(/<return[^>]*>([\s\S]*?)<\/return>/)
if (!resultMatch) {
console.log('❌ Не найден return в ответе')
return null
}
let resultData = resultMatch[1]
// Декодируем HTML entities если данные экранированы
resultData = resultData
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&amp;/g, '&')
console.log('📋 Данные результата (первые 1000 символов):', resultData.substring(0, 1000))
console.log('📋 Полные данные результата:', resultData)
// Ищем блок FindOEM
const findOemMatch = resultData.match(/<FindOEM>([\s\S]*?)<\/FindOEM>/) ||
resultData.match(/<findOem>([\s\S]*?)<\/findOem>/) ||
resultData.match(/<response>([\s\S]*?)<\/response>/)
if (!findOemMatch) {
console.log('❌ Не найден блок FindOEM в ответе')
return null
}
const findOemData = findOemMatch[1]
// Парсим детали
const details: LaximoDocDetail[] = []
const detailPattern = /<detail([^>]*)>(.*?)<\/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([^>]*)>(.*?)<\/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(/<detail([^>]*)/)
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
/**
* Создает SOAP 1.1 конверт согласно WSDL схеме
*/
protected createSOAP11Envelope(command: string, login: string, hmac: string): string {
return `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://WebCatalog.Kito.ec">
<soap:Body>
<ns:QueryDataLogin>
<ns:request>${command}</ns:request>
<ns:login>${login}</ns:login>
<ns:hmac>${hmac}</ns:hmac>
</ns:QueryDataLogin>
</soap:Body>
</soap:Envelope>`
}
/**
* Создает SOAP 1.2 конверт согласно WSDL схеме
*/
private createSOAP12Envelope(command: string, login: string, hmac: string): string {
return `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
xmlns:ns="http://WebCatalog.Kito.ec">
<soap:Body>
<ns:QueryDataLogin>
<ns:request>${command}</ns:request>
<ns:login>${login}</ns:login>
<ns:hmac>${hmac}</ns:hmac>
</ns:QueryDataLogin>
</soap:Body>
</soap:Envelope>`
}
/**
* Парсит XML ответ согласно официальной документации Laximo
*/
private parseListCatalogsResponse(xmlText: string): LaximoBrand[] {
const brands: LaximoBrand[] = []
// Извлекаем данные между тегами QueryDataLoginResponse/return или response
let resultData = ''
// Пытаемся найти данные в разных форматах ответа
const soapResultMatch = xmlText.match(/<ns:return[^>]*>([\s\S]*?)<\/ns:return>/) ||
xmlText.match(/<return[^>]*>([\s\S]*?)<\/return>/)
const responseMatch = xmlText.match(/<response[^>]*>([\s\S]*?)<\/response>/)
if (soapResultMatch) {
resultData = soapResultMatch[1]
// Декодируем HTML entities если данные экранированы
resultData = resultData
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&amp;/g, '&')
} else if (responseMatch) {
resultData = responseMatch[1]
} else {
console.log('🔍 Не найден результат в XML ответе')
return brands
}
// Ищем секцию ListCatalogs
const catalogsMatch = resultData.match(/<ListCatalogs[^>]*>([\s\S]*?)<\/ListCatalogs>/)
if (!catalogsMatch) {
console.log('🔍 Не найдена секция ListCatalogs')
return brands
}
const catalogsData = catalogsMatch[1]
// Ищем все row элементы с их содержимым
const rowMatches = catalogsData.match(/<row[^>]*>[\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(/<row([^>]*)>/);
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(/<features[^>]*>([\s\S]*?)<\/features>/)
if (!featuresMatch) {
return features
}
const featuresData = featuresMatch[1]
const featureMatches = featuresData.match(/<feature[^>]*\/?>|<feature[^>]*>[\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(/<extensions[^>]*>([\s\S]*?)<\/extensions>/)
if (!extensionsMatch) {
return undefined
}
const extensionsData = extensionsMatch[1]
const operationsMatch = extensionsData.match(/<operations[^>]*>([\s\S]*?)<\/operations>/)
if (!operationsMatch) {
return undefined
}
const operationsData = operationsMatch[1]
const operationMatches = operationsData.match(/<operation[^>]*>[\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(/<field[^>]*\/?>|<field[^>]*>[\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<LaximoBrand[]> {
// Проверяем наличие учетных данных
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<LaximoBrand[]> {
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<LaximoCatalogInfo | null> {
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<LaximoWizardStep[]> {
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<LaximoVehicleSearchResult[]> {
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<LaximoVehicleSearchResult[]> {
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<LaximoVehicleSearchResult[]> {
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<LaximoVehicleInfo | null> {
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<LaximoQuickGroup[]> {
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}`
}
if (ssd && ssd.trim() !== '') {
const escapedSsd = this.escapeSsdForXML(ssd)
command += `|ssd=${escapedSsd}`
}
if (categoryId) {
command += `|CategoryId=${categoryId}`
}
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(/<ListUnits?[^>]*>([\s\S]*?)<\/ListUnits?>/) ||
resultData.match(/<response[^>]*>([\s\S]*?)<\/response>/)
if (!unitsMatch) {
console.log('❌ Не найдена секция ListUnits')
return []
}
const groups: LaximoQuickGroup[] = []
const rowPattern = /<row([^>]*?)(?:\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<LaximoQuickGroup[]> {
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(/<ListCategories?[^>]*>([\s\S]*?)<\/ListCategories?>/) ||
resultData.match(/<response[^>]*>([\s\S]*?)<\/response>/)
if (!categoriesMatch) {
console.log('❌ Не найдена секция ListCategories')
console.log('📋 Доступные данные результата (первые 500 символов):', resultData.substring(0, 500))
return []
}
const groups: LaximoQuickGroup[] = []
const rowPattern = /<row([^>]*?)(?:\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<LaximoQuickGroup[]> {
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<string> {
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('📄 Первые 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(/<row([^>]*)>/);
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(/<row[^>]*>[\s\S]*?<\/row>/g)
if (!rowMatches) return []
const steps: LaximoWizardStep[] = []
for (const rowMatch of rowMatches) {
const rowTagMatch = rowMatch.match(/<row([^>]*)>/);
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(/<row[^>]*>[\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(/<row([^>]*)>/);
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] : ''
}
// Парсим атрибуты из дочерних элементов <attribute>
const attributeMap = new Map<string, string>()
// Отладочное логирование
console.log('🔍 Полный XML контент строки:', rowMatch.substring(0, 500))
const attributeMatches = rowMatch.match(/<attribute[^>]*\/?>|<attribute[^>]*>[\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(/<attribute([^>]*)>/);
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(' - Содержит <attribute:', rowMatch.includes('<attribute'))
console.log(' - Длина rowMatch:', rowMatch.length)
}
// Получаем данные из атрибутов row и дочерних элементов attribute
const vehicleName = getAttribute('name')
// Ищем год в разных атрибутах
const year = getAttribute('year') ||
attributeMap.get('manufactured') ||
attributeMap.get('date')?.split('.').pop() ||
attributeMap.get('modelyear') ||
attributeMap.get('productionyear') || ''
// Ищем двигатель в разных атрибутах
const engine = getAttribute('engine') ||
attributeMap.get('engine') ||
attributeMap.get('engine_info') ||
attributeMap.get('enginecode') ||
attributeMap.get('enginetype') || ''
const modification = getAttribute('modification') || attributeMap.get('modification') || ''
const bodytype = getAttribute('bodytype') || attributeMap.get('bodytype') || ''
// Логируем все доступные ключи для отладки
if (attributeMap.size > 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,
doors: attributeMap.get('doors') || 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,
frame: attributeMap.get('frame') || undefined,
frames: attributeMap.get('frames') || undefined,
framefrom: attributeMap.get('framefrom') || undefined,
frameto: attributeMap.get('frameto') || undefined,
engine1: attributeMap.get('engine1') || undefined,
engine2: attributeMap.get('engine2') || 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,
}
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 {
const resultData = this.extractResultData(xmlText)
if (!resultData) return null
const rowMatch = resultData.match(/<row([^>]*)>([\s\S]*?)<\/row>/)
if (!rowMatch) return null
const attributes = rowMatch[1]
const content = rowMatch[2]
const getAttribute = (name: string): string => {
const match = attributes.match(new RegExp(`${name}="([^"]*)"`, 'i'))
return match ? match[1] : ''
}
// Парсим атрибуты автомобиля
const vehicleAttributes: LaximoVehicleAttribute[] = []
const attributeMatches = content.match(/<attribute[^>]*\/?>|<attribute[^>]*>[\s\S]*?<\/attribute>/g)
if (attributeMatches) {
for (const attrMatch of attributeMatches) {
const attrTagMatch = attrMatch.match(/<attribute([^>]*)>/);
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(/<ns:return[^>]*>([\s\S]*?)<\/ns:return>/) ||
xmlText.match(/<return[^>]*>([\s\S]*?)<\/return>/)
const responseMatch = xmlText.match(/<response[^>]*>([\s\S]*?)<\/response>/)
console.log('🎯 soapResultMatch найден:', !!soapResultMatch)
console.log('🎯 responseMatch найден:', !!responseMatch)
if (soapResultMatch) {
const result = soapResultMatch[1]
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&amp;/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(/<options[^>]*>([\s\S]*?)<\/options>/)
if (!optionsMatch) return options
const optionsData = optionsMatch[1]
const optionMatches = optionsData.match(/<row[^>]*\/?>|<row[^>]*>[\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(/<permissions[^>]*>([\s\S]*?)<\/permissions>/)
if (!permissionsMatch) return permissions
const permissionsData = permissionsMatch[1]
const permissionMatches = permissionsData.match(/<permission[^>]*>([\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(/<ListQuickGroups?[^>]*>([\s\S]*?)<\/ListQuickGroups?>/) ||
resultData.match(/<ListQuickGroup[^>]*>([\s\S]*?)<\/ListQuickGroup>/)
if (!quickGroupsMatch) {
console.log('❌ Не найдена секция ListQuickGroups')
return []
}
console.log('✅ Найдена секция ListQuickGroups')
const xmlContent = quickGroupsMatch[1]
// Ищем корневую группу с quickgroupid="0" (может называться по-разному)
const rootGroupPattern = /<row[^>]*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) {
// Ищем следующий открывающий тег <row
const rowStart = xmlContent.indexOf('<row', pos)
if (rowStart === -1) break
const rowTagEnd = xmlContent.indexOf('>', 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
}
// Ищем соответствующий закрывающий тег </row>
const closeTagPos = this.findMatchingCloseTag(xmlContent, rowTagEnd + 1)
if (closeTagPos > rowTagEnd + 1) {
// Есть содержимое между открывающим и закрывающим тегами
const innerContent = xmlContent.substring(rowTagEnd + 1, closeTagPos)
if (innerContent.includes('<row')) {
console.log(`🔍 Парсим детей для ${group.name}...`)
group.children = this.parseRowHierarchy(innerContent, 0)
}
}
categories.push(group)
// Переходим к позиции после закрывающего тега
pos = closeTagPos + 6 // Длина "</row>"
}
return categories
}
/**
* Находит позицию соответствующего закрывающего тега </row>
*/
private findMatchingCloseTag(xmlContent: string, startPos: number): number {
let depth = 1
let pos = startPos
while (pos < xmlContent.length && depth > 0) {
const nextOpen = xmlContent.indexOf('<row', pos)
const nextClose = xmlContent.indexOf('</row>', 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 // Длина "<row"
} else {
depth--
if (depth === 0) {
return nextClose
}
pos = nextClose + 6 // Длина "</row>"
}
}
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<LaximoQuickDetail | null> {
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}`
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
}
// Ищем секцию ListQuickDetail
const quickDetailMatch = resultData.match(/<ListQuickDetail[^>]*>([\s\S]*?)<\/ListQuickDetail>/) ||
resultData.match(/<response[^>]*>([\s\S]*?)<\/response>/)
if (!quickDetailMatch) {
console.log('❌ Не найдена секция ListQuickDetail')
return null
}
const quickDetail: LaximoQuickDetail = {
quickgroupid: quickGroupId,
name: '',
units: []
}
// Ищем категории (Category)
const categoryPattern = /<Category([^>]*?)(?:\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 = /<Unit([^>]*?)(?:\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(/&amp;/g, '&')
}
if (largeImageUrl) {
largeImageUrl = largeImageUrl.replace(/&amp;/g, '&')
}
console.log('🔧 Найден узел:', { unitId, unitName, unitCode, imageUrl, largeImageUrl })
const unit: LaximoUnit = {
unitid: unitId,
name: unitName,
code: unitCode,
imageurl: imageUrl || undefined,
largeimageurl: largeImageUrl || undefined,
details: []
}
// В каждом узле ищем детали (Detail)
const detailPattern = /<Detail([^>]*?)(?:\s*\/>|>([\s\S]*?)<\/Detail>)/g
let detailMatch
while ((detailMatch = detailPattern.exec(unitContent)) !== null) {
const detailAttributes = detailMatch[1]
const detailContent = detailMatch[2] || ''
const detailId = this.extractAttribute(detailAttributes, 'detailid')
const detailName = this.extractAttribute(detailAttributes, 'name')
const oem = this.extractAttribute(detailAttributes, 'oem')
const brand = this.extractAttribute(detailAttributes, 'brand')
console.log('🔩 Найдена деталь:', { detailId, detailName, oem, brand })
const detail: LaximoDetail = {
detailid: detailId,
name: detailName,
oem: oem,
brand: brand,
attributes: []
}
// Парсим атрибуты детали
const attributePattern = /<attribute([^>]*?)(?:\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)
}
quickDetail.units!.push(unit)
}
}
// Если структура не найдена, пробуем альтернативный формат
if (quickDetail.units!.length === 0) {
console.log('🔄 Пробуем альтернативный формат парсинга...')
// Ищем узлы напрямую
const directUnitPattern = /<row([^>]*?)(?:\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}`)
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<LaximoOEMResult | null> {
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<LaximoFulltextSearchResult | null> {
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(/<GetOEMPartApplicability[^>]*>([\s\S]*?)<\/GetOEMPartApplicability>/) ||
resultData.match(/<response[^>]*>([\s\S]*?)<\/response>/)
if (!oemResultMatch) {
console.log('❌ Не найдена секция GetOEMPartApplicability')
return null
}
const oemResult: LaximoOEMResult = {
oemNumber: oemNumber,
categories: []
}
// Ищем категории (Category)
const categoryPattern = /<Category([^>]*?)(?:\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 = /<Unit([^>]*?)(?:\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)
const detailPattern = /<Detail([^>]*?)(?:\s*\/>|>([\s\S]*?)<\/Detail>)/g
let detailMatch
while ((detailMatch = detailPattern.exec(unitContent)) !== null) {
const detailAttributes = detailMatch[1]
const detailContent = detailMatch[2] || ''
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 amount = this.extractAttribute(detailAttributes, 'amount')
const range = this.extractAttribute(detailAttributes, 'range')
console.log('🔩 Найдена деталь:', { detailId, detailName, oem, brand })
const detail: LaximoOEMDetail = {
detailid: detailId,
name: detailName,
oem: oem,
brand: brand,
amount: amount,
range: range,
attributes: []
}
// Парсим атрибуты детали
const attributePattern = /<attribute([^>]*?)(?:\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')
detail.attributes?.push({
key,
name: name || key,
value
})
}
unit.details.push(detail)
}
category.units.push(unit)
}
oemResult.categories.push(category)
}
// Если категории не найдены, пробуем альтернативный формат
if (oemResult.categories.length === 0) {
console.log('🔄 Пробуем альтернативный формат парсинга OEM результатов...')
// Ищем узлы напрямую
const directUnitPattern = /<row([^>]*?)(?:\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(/<SearchVehicleDetails[^>]*>([\s\S]*?)<\/SearchVehicleDetails>/)
if (!searchResultMatch) {
console.log('❌ Не найдена секция SearchVehicleDetails')
// Попробуем найти альтернативные секции
const alternativeMatches = [
resultData.match(/<Details[^>]*>([\s\S]*?)<\/Details>/),
resultData.match(/<Parts[^>]*>([\s\S]*?)<\/Parts>/),
resultData.match(/<Items[^>]*>([\s\S]*?)<\/Items>/),
resultData.match(/<SearchResult[^>]*>([\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 - <row oem="4M0115301H">Труба маслоналивная</row>
const documentationRowPattern = /<row\s+oem="([^"]+)"[^>]*>(.*?)<\/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 = /<row([^>]*?)(?:\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<LaximoVehicleSearchResult[]> {
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<LaximoVehicleSearchResult[]> {
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<string[]> {
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<LaximoVehicleSearchResult[]> {
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<LaximoVehiclesByPartResult> {
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 = /<CatalogReference[^>]*?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<LaximoUnit | null> {
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}|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<LaximoDetail[]> {
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}|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<LaximoUnitImageMap | null> {
try {
console.log('🔍 Получаем карту изображений узла:', unitId)
console.log('📋 Параметры:', { catalogCode, vehicleId, unitId, ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует' })
// Используем ListImageMapByUnit согласно документации Laximo
let command = `ListImageMapByUnit:Catalog=${catalogCode}|UnitId=${unitId}`
if (ssd && ssd.trim() !== '') {
command += `|ssd=${ssd}`
} else {
command += `|ssd=`
}
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(/<GetUnitInfo[^>]*>([\s\S]*?)<\/GetUnitInfo>/) ||
resultData.match(/<response[^>]*>([\s\S]*?)<\/response>/)
if (!unitInfoMatch) {
console.log('❌ Не найдена секция GetUnitInfo')
return null
}
const rowPattern = /<row([^>]*?)(?:\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 = /<attribute\s+key="([^"]*?)"\s+name="([^"]*?)"\s+value="([^"]*?)"\s*\/?>/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(/<ListDetailsByUnit[^>]*>([\s\S]*?)<\/ListDetailsByUnit>/) ||
resultData.match(/<response[^>]*>([\s\S]*?)<\/response>/)
if (!detailsMatch) {
console.log('❌ Не найдена секция ListDetailsByUnit')
return []
}
const details: LaximoDetail[] = []
const rowPattern = /<row([^>]*?)(?:\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 = /<attribute\s+key="([^"]*?)"\s+name="([^"]*?)"\s+value="([^"]*?)"\s*\/?>/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(/<ListImageMapByUnit[^>]*>([\s\S]*?)<\/ListImageMapByUnit>/) ||
resultData.match(/<response[^>]*>([\s\S]*?)<\/response>/)
if (!imageMapMatch) {
console.log('❌ Не найдена секция ListImageMapByUnit')
return null
}
const coordinates: LaximoImageCoordinate[] = []
const rowPattern = /<row([^>]*?)(?:\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()