interface WildberriesWarehouse { id: number name: string address: string cargoType: number latitude: number longitude: number } interface WildberriesWarehousesResponse { data: WildberriesWarehouse[] } // Интерфейс для совместимости с компонентом склада interface WBStock { nmId: number vendorCode: string title: string brand: string price: number stocks: Array<{ warehouseId: number warehouseName: string quantity: number quantityFull: number inWayToClient: number inWayFromClient: number }> totalQuantity: number totalReserved: number photos?: Array<{ big?: string c246x328?: string c516x688?: string square?: string tm?: string }> mediaFiles?: string[] characteristics?: Array<{ id: number name: string value: string[] | string }> subjectName?: string description?: string } // Analytics API interfaces for stocks report by offices interface StocksReportOfficesRequest { nmIDs?: number[] subjectIDs?: number[] brandNames?: string[] tagIDs?: number[] currentPeriod: { start: string end: string } stockType: '' | 'wb' | 'mp' skipDeletedNm: boolean } interface StocksReportOfficesResponse { data: { regions: Array<{ regionName: string metrics: { stockCount?: number stockSum?: number saleRate?: { days: number hours: number } toClientCount?: number fromClientCount?: number } offices: Array<{ officeID: number officeName: string metrics: { stockCount: number stockSum: number saleRate: { days: number hours: number } toClientCount: number fromClientCount: number } }> }> } } interface WildberriesCard { nmID: number imtID?: number nmUUID?: string subjectID?: number subjectName?: string vendorCode: string brand: string title: string description: string needKiz?: boolean photos?: Array<{ big: string c246x328: string c516x688: string square: string tm: string }> video?: string dimensions?: { length: number width: number height: number weightBrutto: number isValid: boolean } characteristics?: Array<{ id: number name: string value: string[] }> sizes: Array<{ chrtID: number techSize: string skus?: string[] // Legacy fields for backward compatibility wbSize: string price: number discountedPrice: number quantity: number }> tags?: Array<{ id: number name: string color: string }> createdAt?: string updatedAt?: string // Legacy fields for backward compatibility mediaFiles: string[] object?: string parent?: string countryProduction?: string supplierVendorCode?: string } interface WildberriesCardsResponse { cursor: { total: number updatedAt: string limit: number nmID: number } cards: WildberriesCard[] } interface WildberriesCardFilter { settings?: { cursor?: { limit?: number nmID?: number updatedAt?: string } filter?: { textSearch?: string withPhoto?: number objectIDs?: number[] tagIDs?: number[] brandIDs?: number[] colorIDs?: number[] sizeIDs?: number[] } } } export interface WBSalesData { date: string saleID: string gNumber: string supplierArticle: string techSize: string barcode: string totalPrice: number discountPercent: number isSupply: boolean isRealization: boolean promoCodeDiscount: number warehouseName: string countryName: string oblastOkrugName: string regionName: string incomeID: number isCancel: boolean cancelDate: string orderType: string sticker: string srp: number } export interface WBOrdersData { date: string lastChangeDate: string supplierArticle: string techSize: string barcode: string totalPrice: number discountPercent: number warehouseName: string oblast: string incomeID: number odid: number nmId: number subject: string category: string brand: string isCancel: boolean cancel_dt: string } export interface WBAdvertData { advertId: number name: string status: number type: number createTime: string changeTime: string startTime: string endTime: string dailyBudget: number budget: number } export interface WBAdvertStatsRequest { id?: number dates?: string[] interval?: { begin: string end: string } } // Интерфейсы для API /adv/v1/promotion/count export interface WBCampaignListItem { advertId: number changeTime: string // date-time } export interface WBAdvertGroup { type: number status: number count: number advert_list: WBCampaignListItem[] } export interface WBCampaignsListResponse { adverts: WBAdvertGroup[] | null all: number } export interface WBAdvertStatsResponse { dates: string[] views: number clicks: number ctr: number cpc: number sum: number atbs: number orders: number cr: number shks: number sum_price: number days: Array<{ date: string views: number clicks: number ctr: number cpc: number sum: number atbs: number orders: number cr: number shks: number sum_price: number }> boosterStats: Array<{ date: string views: number clicks: number ctr: number cpc: number sum: number atbs: number orders: number cr: number shks: number sum_price: number }> advertId: number } // Новые интерфейсы для метода getCampaignStats export interface WBCampaignStatsRequestWithDate { id: number dates: string[] } export interface WBCampaignStatsRequestWithInterval { id: number interval: { begin: string end: string } } export interface WBCampaignStatsRequestWithCampaignID { id: number } export type WBCampaignStatsRequest = | WBCampaignStatsRequestWithDate | WBCampaignStatsRequestWithInterval | WBCampaignStatsRequestWithCampaignID export interface WBStatisticsData { date: string sales: number orders: number advertising: number refusals: number returns: number revenue: number buyoutPercentage: number } class WildberriesService { private apiKey: string private baseURL = 'https://statistics-api.wildberries.ru' private advertURL = 'https://advert-api.wildberries.ru' private contentURL = 'https://content-api.wildberries.ru' constructor(apiKey: string) { this.apiKey = apiKey } private async makeRequest(url: string, options: RequestInit = {}): Promise { // Определяем правильный заголовок авторизации в зависимости от API const authHeader = url.includes('marketplace-api.wildberries.ru') || url.includes('content-api.wildberries.ru') ? { 'Authorization': `Bearer ${this.apiKey}` } // Marketplace и Content API используют Bearer : { 'Authorization': this.apiKey } // Statistics и Advert API используют прямой токен const response = await fetch(url, { ...options, headers: { ...authHeader, 'Content-Type': 'application/json', ...options.headers, }, }) if (!response.ok) { throw new Error(`WB API Error: ${response.status} ${response.statusText}`) } return response.json() as Promise } // Получение данных о продажах async getSales(dateFrom: string, flag = 0): Promise { const url = `${this.baseURL}/api/v1/supplier/sales?dateFrom=${dateFrom}&flag=${flag}` return this.makeRequest(url) } // Получение данных о заказах async getOrders(dateFrom: string, flag = 0): Promise { const url = `${this.baseURL}/api/v1/supplier/orders?dateFrom=${dateFrom}&flag=${flag}` return this.makeRequest(url) } // Получение списка всех кампаний с группировкой async getCampaignsList(): Promise { const url = `${this.advertURL}/adv/v1/promotion/count` console.log(`WB API: Getting campaigns list from ${url}`) return this.makeRequest(url) } // Получение кампаний за определенный период по changeTime async getCampaignsForPeriod(dateFrom: string, dateTo: string): Promise { const campaignsList = await this.getCampaignsList() const fromDate = new Date(dateFrom) const toDate = new Date(dateTo) const campaignIds: number[] = [] if (campaignsList.adverts) { for (const advertGroup of campaignsList.adverts) { if (advertGroup.advert_list) { for (const campaign of advertGroup.advert_list) { const changeTime = new Date(campaign.changeTime) // Проверяем что кампания была изменена в нужном периоде if (changeTime >= fromDate && changeTime <= toDate) { campaignIds.push(campaign.advertId) } } } } } console.log(`WB API: Found ${campaignIds.length} campaigns for period ${dateFrom} - ${dateTo}`) return campaignIds } // Старый метод для совместимости (используем новый API) async getAdverts(status?: number, type?: number, limit = 100, offset = 0): Promise { const campaignsList = await this.getCampaignsList() const campaigns: WBAdvertData[] = [] if (campaignsList.adverts) { for (const advertGroup of campaignsList.adverts) { // Фильтрация по статусу и типу если указаны if (status && advertGroup.status !== status) continue if (type && advertGroup.type !== type) continue if (advertGroup.advert_list) { for (const campaign of advertGroup.advert_list) { campaigns.push({ advertId: campaign.advertId, type: advertGroup.type, status: advertGroup.status, name: `Campaign ${campaign.advertId}`, endTime: campaign.changeTime, createTime: campaign.changeTime, changeTime: campaign.changeTime, startTime: campaign.changeTime, // Используем changeTime как заглушку dailyBudget: 0, // Неизвестно из этого API budget: 0 // Неизвестно из этого API }) // Применяем лимит if (campaigns.length >= limit) break } } if (campaigns.length >= limit) break } } return campaigns.slice(offset, offset + limit) } // Получение статистики конкретных кампаний (новый метод) async getCampaignStats(requests: WBCampaignStatsRequest[]): Promise { if (!requests || requests.length === 0) { throw new Error('Requests array cannot be empty') } if (requests.length > 100) { throw new Error('Maximum 100 campaigns can be requested at once') } const url = `${this.advertURL}/adv/v2/fullstats` console.log(`WB API: Requesting campaign stats for ${requests.length} campaigns`) console.log(`WB API: Request body:`, JSON.stringify(requests, null, 2)) try { const response = await this.makeRequest(url, { method: 'POST', body: JSON.stringify(requests) }) console.log(`WB API: Campaign stats response:`, JSON.stringify(response, null, 2)) return response } catch (error) { console.error(`WB API: Campaign stats error:`, error) throw error } } // Получение списка складов async getWarehouses(): Promise> { try { // Используем правильный API endpoint для получения складов продавца const url = `https://marketplace-api.wildberries.ru/api/v3/warehouses` console.log(`WB API: Getting seller warehouses from ${url}`) const response = await this.makeRequest>(url) console.log(`WB API: Got ${response.length} warehouses`) return response.map(w => ({ id: w.id, name: w.name, cargoType: w.cargoType || 1, deliveryType: w.deliveryType || 1 })) } catch (error) { console.error(`WB API: Error getting warehouses:`, error) // При ошибке возвращаем пустой массив вместо статических данных console.log(`WB API: Returning empty warehouses array due to API error`) return [] } } // Получение карточек товаров async getCards(options: { limit?: number; cursor?: { updatedAt?: string; nmID?: number } } = {}): Promise { const { limit = 100, cursor } = options const url = `${this.contentURL}/content/v2/get/cards/list` const body = { settings: { cursor: { limit, ...(cursor?.updatedAt && { updatedAt: cursor.updatedAt }), ...(cursor?.nmID && { nmID: cursor.nmID }) }, filter: { withPhoto: -1 } } } console.log(`WB API: Getting cards from ${url}`, body) try { const response = await this.makeRequest(url, { method: 'POST', body: JSON.stringify(body) }) // Преобразуем карточки для обратной совместимости const processedCards = response.cards.map(this.processCard) return { ...response, cards: processedCards } } catch (error) { console.error(`WB API: Error getting cards:`, error) return { cards: [], cursor: { total: 0, updatedAt: '', limit: 0, nmID: 0 } } } } // Обработка карточки для обратной совместимости private processCard(card: WildberriesCard): WildberriesCard { // Создаем массив URL изображений для совместимости с mediaFiles const mediaFiles: string[] = [] console.log(`WB API: Processing card ${card.nmID}, photos:`, card.photos) if (card.photos && card.photos.length > 0) { card.photos.forEach((photo, index) => { // Для каждого фото берем лучший доступный размер const bestImage = photo.c516x688 || photo.big || photo.c246x328 || photo.square || photo.tm if (bestImage) { mediaFiles.push(bestImage) console.log(`WB API: Added image ${index + 1} for card ${card.nmID}:`, bestImage) } }) } // Если нет photos, пытаемся сгенерировать fallback изображения if (mediaFiles.length === 0) { const vol = Math.floor(card.nmID / 100000) const part = Math.floor(card.nmID / 1000) const fallbackUrl = `https://basket-${String(vol).padStart(2, '0')}.wbbasket.ru/vol${vol}/part${part}/${card.nmID}/images/c246x328/1.webp` mediaFiles.push(fallbackUrl) console.log(`WB API: Added fallback image for card ${card.nmID}:`, fallbackUrl) } console.log(`WB API: Final mediaFiles for card ${card.nmID}:`, mediaFiles) // Заполняем размеры с ценами и количеством для совместимости const processedSizes = card.sizes.map(size => ({ ...size, wbSize: size.wbSize || size.techSize || '', price: size.price || 0, discountedPrice: size.discountedPrice || size.price || 0, quantity: size.quantity || 0 })) return { ...card, // Добавляем mediaFiles для обратной совместимости mediaFiles, // Заполняем legacy поля если они отсутствуют object: card.object || card.subjectName || '', parent: card.parent || '', countryProduction: card.countryProduction || '', supplierVendorCode: card.supplierVendorCode || card.vendorCode, // Обработанные размеры sizes: processedSizes } } // Поиск карточек товаров async searchCards(searchTerm: string, limit = 100): Promise { // Сначала получаем все карточки const response = await this.getCards({ limit }) // Фильтруем результаты по поисковому запросу const filteredCards = response.cards.filter((card: WildberriesCard) => { const searchLower = searchTerm.toLowerCase() return ( card.vendorCode?.toLowerCase().includes(searchLower) || card.object?.toLowerCase().includes(searchLower) || card.brand?.toLowerCase().includes(searchLower) || card.title?.toLowerCase().includes(searchLower) ) }) console.log(`WB API: Search "${searchTerm}" found ${filteredCards.length} cards`) return filteredCards } // Получение статистики всех рекламных кампаний (v2) async getAdvertStats(dateFrom: string, dateTo: string): Promise { const url = `${this.advertURL}/adv/v2/fullstats` // Попробуем через interval - это может работать лучше const request: WBAdvertStatsRequest[] = [{ interval: { begin: dateFrom, end: dateTo } }] console.log(`WB API: Requesting campaign stats with interval: ${dateFrom} to ${dateTo}`) console.log(`WB API: Request body:`, JSON.stringify(request, null, 2)) try { const response = await this.makeRequest(url, { method: 'POST', body: JSON.stringify(request) }) console.log(`WB API: Advert response:`, JSON.stringify(response, null, 2)) return response } catch (error) { console.error(`WB API: Advert stats error:`, error) throw error } } // Получение детального отчета по периоду async getDetailReport(dateFrom: string, dateTo: string, limit = 10000, rrdid = 0): Promise[]> { const url = `${this.baseURL}/api/v1/supplier/reportDetailByPeriod?dateFrom=${dateFrom}&dateTo=${dateTo}&limit=${limit}&rrdid=${rrdid}` return this.makeRequest[]>(url) } // Агрегированная статистика для дашборда async getStatistics(dateFrom: string, dateTo: string): Promise { try { console.log(`WB API: Getting statistics from ${dateFrom} to ${dateTo}`) // Получаем продажи и заказы const [salesData, ordersData] = await Promise.all([ this.getSales(dateFrom, 0), // flag=0 для получения данных за период от dateFrom до сегодня this.getOrders(dateFrom, 0) ]) console.log(`WB API: Got ${salesData.length} sales, ${ordersData.length} orders`) // Получаем статистику рекламы через правильный API let advertStatsData: WBAdvertStatsResponse[] = [] try { console.log(`WB API: Getting campaign stats for interval: ${dateFrom} to ${dateTo}`) try { // Получаем ID кампаний, которые были изменены в указанном периоде const campaignIds = await this.getCampaignsForPeriod(dateFrom, dateTo) if (campaignIds.length > 0) { // Создаем запросы для /adv/v2/fullstats с интервалом дат const campaignRequests: WBCampaignStatsRequest[] = campaignIds.map(id => ({ id, interval: { begin: dateFrom, end: dateTo } })) // Получаем статистику кампаний advertStatsData = await this.getCampaignStats(campaignRequests) console.log(`WB API: Got advertising stats for ${advertStatsData.length} campaigns`) } else { console.log(`WB API: No campaigns found for the specified period`) } } catch (error) { console.error(`WB API: Failed to get campaign stats:`, error) console.log(`WB API: Skipping advertising stats due to API error`) } console.log(`WB API: Got advertising stats for ${advertStatsData.length} campaigns total`) // Логируем детали рекламных затрат advertStatsData.forEach(stat => { const totalSpend = stat.days?.reduce((sum, day) => sum + (day.sum || 0), 0) || stat.sum || 0 console.log(`WB API: Campaign ${stat.advertId} spent ${totalSpend} rubles over ${stat.days?.length || 0} days`) }) } catch (error) { console.warn('WB API: Failed to get advertising stats:', error) } // Группируем данные по датам const statsMap = new Map() // Обрабатываем продажи console.log(`WB API: Processing ${salesData.length} sales records`) salesData.forEach(sale => { const originalDate = sale.date const date = sale.date.split('T')[0] // Берем только дату без времени console.log(`WB API: Processing sale - original date: ${originalDate}, normalized: ${date}`) // Строгая фильтрация по диапазону дат if (date < dateFrom || date > dateTo) { console.log(`WB API: Skipping sale ${date} - outside range ${dateFrom} to ${dateTo}`) return } if (!statsMap.has(date)) { console.log(`WB API: Creating new stats entry for date ${date}`) statsMap.set(date, { date, sales: 0, orders: 0, advertising: 0, refusals: 0, returns: 0, revenue: 0, buyoutPercentage: 0 }) } const stats = statsMap.get(date)! if (!sale.isCancel) { stats.sales += 1 stats.revenue += sale.totalPrice * (1 - sale.discountPercent / 100) console.log(`WB API: Added sale to ${date}, total sales now: ${stats.sales}`) } else { stats.returns += 1 console.log(`WB API: Added return to ${date}, total returns now: ${stats.returns}`) } }) // Обрабатываем заказы console.log(`WB API: Processing ${ordersData.length} orders records`) ordersData.forEach(order => { const originalDate = order.date const date = order.date.split('T')[0] console.log(`WB API: Processing order - original date: ${originalDate}, normalized: ${date}`) // Строгая фильтрация по диапазону дат if (date < dateFrom || date > dateTo) { console.log(`WB API: Skipping order ${date} - outside range ${dateFrom} to ${dateTo}`) return } if (!statsMap.has(date)) { console.log(`WB API: Creating new stats entry for date ${date} (from orders)`) statsMap.set(date, { date, sales: 0, orders: 0, advertising: 0, refusals: 0, returns: 0, revenue: 0, buyoutPercentage: 0 }) } const stats = statsMap.get(date)! if (!order.isCancel) { stats.orders += 1 console.log(`WB API: Added order to ${date}, total orders now: ${stats.orders}`) } else { stats.refusals += 1 console.log(`WB API: Added refusal to ${date}, total refusals now: ${stats.refusals}`) } }) // Обрабатываем данные рекламы advertStatsData.forEach(advertStat => { console.log(`WB API: Processing advert ${advertStat.advertId}, total sum: ${advertStat.sum}`) // Обрабатываем статистику по дням для каждой кампании if (advertStat.days && advertStat.days.length > 0) { advertStat.days.forEach(day => { const date = day.date console.log(`WB API: Day ${date} - spent ${day.sum} rubles (campaign ${advertStat.advertId})`) // Строгая фильтрация по диапазону дат if (date < dateFrom || date > dateTo) { console.log(`WB API: Skipping ${date} - outside range ${dateFrom} to ${dateTo}`) return } if (!statsMap.has(date)) { statsMap.set(date, { date, sales: 0, orders: 0, advertising: 0, refusals: 0, returns: 0, revenue: 0, buyoutPercentage: 0 }) } const stats = statsMap.get(date)! const adSpend = day.sum || 0 stats.advertising += adSpend console.log(`WB API: Added ${adSpend} rubles to ${date}, total now: ${stats.advertising}`) }) } else { console.log(`WB API: No daily data for campaign ${advertStat.advertId}`) } }) // Вычисляем процент выкупов statsMap.forEach(stats => { if (stats.orders > 0) { // Ограничиваем процент выкупов до 100% максимум const percentage = Math.min(100, Math.round((stats.sales / stats.orders) * 100)) stats.buyoutPercentage = percentage } }) // Получаем данные по рекламе (это более сложно, так как нужна отдельная статистика) // Пока используем заглушку, в реальности нужно вызвать отдельный API const finalResults = Array.from(statsMap.values()).sort((a, b) => a.date.localeCompare(b.date)) console.log(`WB API: Final aggregated results:`, finalResults) console.log(`WB API: Unique dates count: ${finalResults.length}`) return finalResults } catch (error) { console.error('Error fetching WB statistics:', error) throw error } } // Получение статистики рекламы (заглушка) async getAdvertStatistics(dateFrom: string, dateTo: string): Promise<{ date: string; spend: number }[]> { // В реальности здесь нужно вызвать соответствующий API для статистики рекламы // Пока возвращаем заглушку return [] } // Генерирует массив дат между dateFrom и dateTo private generateDateRange(dateFrom: string, dateTo: string): string[] { const dates: string[] = [] const start = new Date(dateFrom) const end = new Date(dateTo) const current = new Date(start) while (current <= end) { dates.push(WildberriesService.formatDate(current)) current.setDate(current.getDate() + 1) } return dates } // Форматирование даты для WB API (RFC3339) static formatDate(date: Date): string { return date.toISOString().split('T')[0] // YYYY-MM-DD } // Получение даты период назад static getDatePeriodAgo(period: 'week' | 'month' | 'quarter'): string { const now = new Date() const date = new Date(now) switch (period) { case 'week': date.setDate(date.getDate() - 7) break case 'month': date.setMonth(date.getMonth() - 1) break case 'quarter': date.setMonth(date.getMonth() - 3) break } return this.formatDate(date) } // Статический метод для получения остатков с токеном static async getStocks(apiKey: string): Promise { const service = new WildberriesService(apiKey) return service.getStocks() } // Статический метод для получения складов с токеном static async getWarehouses(apiKey: string): Promise> { const service = new WildberriesService(apiKey) return service.getWarehouses() } // Получение всех карточек с пагинацией async getAllCardsWithPagination(maxCards = 1000): Promise { const allCards: WildberriesCard[] = [] let cursor: { updatedAt?: string; nmID?: number } | undefined while (allCards.length < maxCards) { const response = await this.getCards({ limit: Math.min(100, maxCards - allCards.length), cursor }) if (!response.cards || response.cards.length === 0) { break } allCards.push(...response.cards) // Если получили меньше чем запрашивали, значит это последняя страница if (response.cards.length < 100) { break } // Обновляем курсор для следующего запроса const lastCard = response.cards[response.cards.length - 1] cursor = { updatedAt: response.cursor.updatedAt, nmID: lastCard.nmID } } return allCards } // Статический метод для получения карточек с токеном static async getAllCards(apiKey: string, limit = 100): Promise { const service = new WildberriesService(apiKey) // Если запрашивается больше 100 карточек, используем пагинацию if (limit > 100) { return service.getAllCardsWithPagination(limit) } const response = await service.getCards({ limit }) return response.cards } // Статический метод для поиска карточек с токеном static async searchCards(apiKey: string, searchTerm: string, limit = 100): Promise { const service = new WildberriesService(apiKey) return service.searchCards(searchTerm, limit) } // Утилитные методы для работы с изображениями static getCardImage(card: WildberriesCard, size: 'big' | 'c516x688' | 'c246x328' | 'square' | 'tm' = 'c516x688'): string { if (card.photos && card.photos.length > 0) { return card.photos[0][size] || card.photos[0].big || '' } // Fallback на mediaFiles для старых данных if (card.mediaFiles && card.mediaFiles.length > 0) { return card.mediaFiles[0] } return '' } static getCardImages(card: WildberriesCard): string[] { if (card.photos && card.photos.length > 0) { return card.photos.map(photo => photo.big || photo.c516x688 || photo.c246x328) } // Fallback на mediaFiles для старых данных return card.mediaFiles || [] } // Получение остатков товаров на складах async getStocks(): Promise { try { console.log('WB API: Getting stocks using marketplace API') // 1. Сначала получаем список складов продавца const warehouses = await this.getWarehouses() console.log(`WB API: Got ${warehouses.length} warehouses`) if (warehouses.length === 0) { console.log('WB API: No warehouses found') return [] } // 2. Получаем карточки товаров для получения SKU/баркодов const cardsResponse = await this.getCards({ limit: 100 }) const cards = cardsResponse.cards console.log(`WB API: Got ${cards.length} cards`) console.log(`WB API: Sample card photos:`, cards[0]?.photos) if (cards.length === 0) { console.log('WB API: No cards found') return [] } // 3. Собираем все SKU из карточек товаров const allSkus: string[] = [] const cardSkuMap = new Map() cards.forEach(card => { if (card.sizes && card.sizes.length > 0) { card.sizes.forEach(size => { if (size.skus && size.skus.length > 0) { size.skus.forEach(sku => { if (sku) { allSkus.push(sku) cardSkuMap.set(sku, card) } }) } }) } }) console.log(`WB API: Collected ${allSkus.length} SKUs from cards`) if (allSkus.length === 0) { console.log('WB API: No SKUs found in cards') return [] } // 4. Для каждого склада получаем остатки const allStocks: unknown[] = [] for (const warehouse of warehouses) { try { const stocksUrl = `https://marketplace-api.wildberries.ru/api/v3/stocks/${warehouse.id}` console.log(`WB API: Getting stocks for warehouse ${warehouse.id} (${warehouse.name})`) // Разбиваем SKUs на порции по 1000 (лимит API) const chunkSize = 1000 for (let i = 0; i < allSkus.length; i += chunkSize) { const skuChunk = allSkus.slice(i, i + chunkSize) try { const stocksResponse = await this.makeRequest<{ stocks: Array<{ sku: string amount: number }> }>(stocksUrl, { method: 'POST', body: JSON.stringify({ skus: skuChunk }) }) console.log(`WB API: Got ${stocksResponse.stocks?.length || 0} stock records for warehouse ${warehouse.id}`) // Преобразуем данные в нужный формат if (stocksResponse.stocks) { stocksResponse.stocks.forEach(stock => { const card = cardSkuMap.get(stock.sku) if (card) { console.log(`WB API: Creating stock entry for card ${card.nmID}`) console.log(`WB API: Card photos:`, card.photos) console.log(`WB API: Card mediaFiles:`, card.mediaFiles) allStocks.push({ nmId: card.nmID, vendorCode: card.vendorCode, title: card.title, brand: card.brand, subject: card.object || card.subjectName, subjectName: card.subjectName, category: card.subjectName, description: card.description, warehouseId: warehouse.id, warehouseName: warehouse.name, quantity: stock.amount, quantityFull: stock.amount, inWayToClient: 0, // Эти данные недоступны через marketplace API inWayFromClient: 0, price: 0, // Цены получаются отдельно sku: stock.sku, photos: card.photos || [], mediaFiles: card.mediaFiles || [], // ЗДЕСЬ ДОЛЖНЫ БЫТЬ ОБРАБОТАННЫЕ ИЗОБРАЖЕНИЯ! characteristics: card.characteristics || [] }) } }) } } catch (chunkError) { console.error(`WB API: Error getting stocks chunk for warehouse ${warehouse.id}:`, chunkError) } } } catch (warehouseError) { console.error(`WB API: Error getting stocks for warehouse ${warehouse.id}:`, warehouseError) } } // 5. Добавляем карточки, для которых не найдено остатков (показываем их с нулевыми остатками) const stockedCardIds = new Set(allStocks.map(stock => (stock as Record).nmId)) cards.forEach(card => { if (!stockedCardIds.has(card.nmID)) { console.log(`WB API: Adding zero-stock entry for card ${card.nmID}`) console.log(`WB API: Card photos:`, card.photos) console.log(`WB API: Card mediaFiles:`, card.mediaFiles) // Для каждого склада создаем запись с нулевыми остатками warehouses.forEach(warehouse => { allStocks.push({ nmId: card.nmID, vendorCode: card.vendorCode, title: card.title, brand: card.brand, subject: card.object || card.subjectName, subjectName: card.subjectName, category: card.subjectName, description: card.description, warehouseId: warehouse.id, warehouseName: warehouse.name, quantity: 0, quantityFull: 0, inWayToClient: 0, inWayFromClient: 0, price: 0, sku: '', photos: card.photos || [], mediaFiles: card.mediaFiles || [], // ВАЖНО: ОБРАБОТАННЫЕ ИЗОБРАЖЕНИЯ! characteristics: card.characteristics || [] }) }) } }) console.log(`WB API: Total collected ${allStocks.length} stock records (including zero stocks)`) return allStocks } catch (error) { console.error(`WB API: Error getting stocks:`, error) console.log('WB API: Returning empty stocks array due to API error') return [] } } // Метод для получения даты N дней назад private getDateDaysAgo(days: number): string { const date = new Date() date.setDate(date.getDate() - days) return date.toISOString().split('T')[0] } // Новый метод для получения данных по складам через Analytics API async getStocksReportByOffices(params: { nmIds?: number[] subjectIds?: number[] brandNames?: string[] tagIds?: number[] dateFrom?: string dateTo?: string stockType?: '' | 'wb' | 'mp' } = {}): Promise { try { console.log('WB Analytics API: Getting stocks report by offices...') const today = new Date().toISOString().split('T')[0] const dateFrom = params.dateFrom || today const dateTo = params.dateTo || today const requestBody: StocksReportOfficesRequest = { nmIDs: params.nmIds, subjectIDs: params.subjectIds, brandNames: params.brandNames, tagIDs: params.tagIds, currentPeriod: { start: dateFrom, end: dateTo }, stockType: params.stockType || '', // все склады skipDeletedNm: true } console.log('WB Analytics API: Request parameters:') console.log('- nmIDs:', params.nmIds) console.log('- subjectIDs:', params.subjectIds) console.log('- brandNames:', params.brandNames) console.log('- tagIDs:', params.tagIds) console.log('- currentPeriod:', { start: dateFrom, end: dateTo }) console.log('- stockType:', params.stockType || 'all') console.log('- skipDeletedNm:', true) console.log('WB Analytics API: Request body:', JSON.stringify(requestBody, null, 2)) // Используем Analytics API const analyticsURL = 'https://seller-analytics-api.wildberries.ru' const url = `${analyticsURL}/api/v2/stocks-report/offices` const response = await this.makeRequest(url, { method: 'POST', body: JSON.stringify(requestBody) }) console.log('WB Analytics API: Response:', JSON.stringify(response, null, 2)) // Детальный анализ структуры ответа console.log('\n=== ДЕТАЛЬНЫЙ АНАЛИЗ ОТВЕТА API ===') if (response.data) { console.log('✅ response.data существует') if (response.data.regions) { console.log('✅ response.data.regions существует, длина:', response.data.regions.length) response.data.regions.forEach((region, regionIndex) => { console.log(`\n📍 РЕГИОН ${regionIndex + 1}:`) console.log(' - regionName:', region.regionName) console.log(' - metrics:', region.metrics) console.log(' - offices.length:', region.offices?.length || 0) if (region.offices && region.offices.length > 0) { region.offices.forEach((office, officeIndex) => { console.log(`\n 🏢 СКЛАД ${officeIndex + 1}:`) console.log(' - officeID:', office.officeID) console.log(' - officeName:', office.officeName) console.log(' - metrics:', office.metrics) // Проверяем наличие метрик if (office.metrics) { console.log(' - stockCount:', office.metrics.stockCount || 0) console.log(' - toClientCount:', office.metrics.toClientCount || 0) console.log(' - fromClientCount:', office.metrics.fromClientCount || 0) } }) } else { console.log(' ⚠️ Нет складов в этом регионе') } }) } else { console.log('❌ response.data.regions отсутствует') } } else { console.log('❌ response.data отсутствует') } console.log('=== КОНЕЦ АНАЛИЗА ===\n') console.log(`WB Analytics API: Returning raw response for processing in component`) return response } catch (error) { console.error('WB Analytics API: Error getting stocks report:', error) return { data: { regions: [] } } } } } export { WildberriesService }