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

700 lines
23 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 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<T>(url: string, options: RequestInit = {}): Promise<T> {
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<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<WildberriesWarehouse[]> {
const url = `${this.baseURL}/api/v2/warehouses`
console.log(`WB API: Getting warehouses from ${url}`)
const response = await this.makeRequest<WildberriesWarehouse[]>(url)
return response || []
}
// Получение карточек товаров
async getCards(options: { limit?: number; offset?: number } = {}): Promise<WildberriesCard[]> {
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<WildberriesCard[]> {
// Для простоты пока используем тот же 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<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 getWarehouses(apiKey: string): Promise<WildberriesWarehouse[]> {
const service = new WildberriesService(apiKey)
return service.getWarehouses()
}
// Статический метод для получения карточек с токеном
static async getAllCards(apiKey: string, limit = 100): Promise<WildberriesCard[]> {
const service = new WildberriesService(apiKey)
return service.getCards({ limit })
}
// Статический метод для поиска карточек с токеном
static async searchCards(apiKey: string, searchTerm: string, limit = 100): Promise<WildberriesCard[]> {
const service = new WildberriesService(apiKey)
return service.searchCards(searchTerm, limit)
}
}
export { WildberriesService }