Files
sfera/src/services/wildberries-service.ts

1266 lines
43 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<T>(url: string, options: RequestInit = {}): Promise<T> {
// Определяем правильный заголовок авторизации в зависимости от 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<T>
}
// Получение данных о продажах
async getSales(dateFrom: string, flag = 0): Promise<WBSalesData[]> {
const url = `${this.baseURL}/api/v1/supplier/sales?dateFrom=${dateFrom}&flag=${flag}`
return this.makeRequest<WBSalesData[]>(url)
}
// Получение данных о заказах
async getOrders(dateFrom: string, flag = 0): Promise<WBOrdersData[]> {
const url = `${this.baseURL}/api/v1/supplier/orders?dateFrom=${dateFrom}&flag=${flag}`
return this.makeRequest<WBOrdersData[]>(url)
}
// Получение списка всех кампаний с группировкой
async getCampaignsList(): Promise<WBCampaignsListResponse> {
const url = `${this.advertURL}/adv/v1/promotion/count`
console.log(`WB API: Getting campaigns list from ${url}`)
return this.makeRequest<WBCampaignsListResponse>(url)
}
// Получение кампаний за определенный период по changeTime
async getCampaignsForPeriod(dateFrom: string, dateTo: string): Promise<number[]> {
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<WBAdvertData[]> {
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<WBAdvertStatsResponse[]> {
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<WBAdvertStatsResponse[]>(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<Array<{ id: number; name: string; cargoType: number; deliveryType: number }>> {
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<Array<{
id: number
name: string
officeId?: number
cargoType?: number
deliveryType?: number
}>>(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<WildberriesCardsResponse> {
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<WildberriesCardsResponse>(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<WildberriesCard[]> {
// Сначала получаем все карточки
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<WBAdvertStatsResponse[]> {
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<WBAdvertStatsResponse[]>(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<Record<string, unknown>[]> {
const url = `${this.baseURL}/api/v1/supplier/reportDetailByPeriod?dateFrom=${dateFrom}&dateTo=${dateTo}&limit=${limit}&rrdid=${rrdid}`
return this.makeRequest<Record<string, unknown>[]>(url)
}
// Агрегированная статистика для дашборда
async getStatistics(dateFrom: string, dateTo: string): Promise<WBStatisticsData[]> {
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<string, WBStatisticsData>()
// Обрабатываем продажи
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<unknown[]> {
const service = new WildberriesService(apiKey)
return service.getStocks()
}
// Статический метод для получения складов с токеном
static async getWarehouses(apiKey: string): Promise<Array<{ id: number; name: string; cargoType: number; deliveryType: number }>> {
const service = new WildberriesService(apiKey)
return service.getWarehouses()
}
// Получение всех карточек с пагинацией
async getAllCardsWithPagination(maxCards = 1000): Promise<WildberriesCard[]> {
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<WildberriesCard[]> {
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<WildberriesCard[]> {
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<unknown[]> {
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<string, WildberriesCard>()
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<string, unknown>).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<StocksReportOfficesResponse> {
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<StocksReportOfficesResponse>(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 }