interface WildberriesWarehouse { id: number name: string address: string cargoType: number latitude: number longitude: number } interface WildberriesWarehousesResponse { data: WildberriesWarehouse[] } interface WildberriesCard { nmID: number vendorCode: string sizes: Array<{ chrtID: number techSize: string wbSize: string price: number discountedPrice: number quantity: number }> mediaFiles: string[] object: string parent: string countryProduction: string supplierVendorCode: string brand: string title: string description: 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' constructor(apiKey: string) { this.apiKey = apiKey } private async makeRequest(url: string, options: RequestInit = {}): Promise { const response = await fetch(url, { ...options, headers: { 'Authorization': this.apiKey, '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 { const url = `${this.baseURL}/api/v2/warehouses` console.log(`WB API: Getting warehouses from ${url}`) const response = await this.makeRequest(url) return response || [] } // Получение карточек товаров async getCards(options: { limit?: number; offset?: number } = {}): Promise { const { limit = 100, offset = 0 } = options const url = `${this.baseURL}/content/v1/cards/cursor/list?sort=updateAt&limit=${limit}&cursor=${offset}` console.log(`WB API: Getting cards from ${url}`) try { const response = await this.makeRequest<{ cards: WildberriesCard[] }>(url) return response?.cards || [] } catch (error) { console.error(`WB API: Error getting cards:`, error) return [] } } // Поиск карточек товаров async searchCards(searchTerm: string, limit = 100): Promise { // Для простоты пока используем тот же API что и getCards // В реальности может потребоваться другой endpoint для поиска const cards = await this.getCards({ limit }) // Фильтруем результаты по поисковому запросу const filteredCards = cards.filter(card => { const searchLower = searchTerm.toLowerCase() return ( card.vendorCode?.toLowerCase().includes(searchLower) || card.object?.toLowerCase().includes(searchLower) || card.brand?.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 getWarehouses(apiKey: string): Promise { const service = new WildberriesService(apiKey) return service.getWarehouses() } // Статический метод для получения карточек с токеном static async getAllCards(apiKey: string, limit = 100): Promise { const service = new WildberriesService(apiKey) return service.getCards({ limit }) } // Статический метод для поиска карточек с токеном static async searchCards(apiKey: string, searchTerm: string, limit = 100): Promise { const service = new WildberriesService(apiKey) return service.searchCards(searchTerm, limit) } } export { WildberriesService }