Digital API

API для управления цифровыми товарами в OZON Seller API.

Количество методов: 4 метода (1 дополнительный для совместимости)

Обзор

Digital API предоставляет функциональность для управления цифровыми товарами:

  • 📦 Получение списка отправлений с цифровыми товарами
  • 🔑 Загрузка кодов цифровых товаров (лицензии, ключи активации)
  • 📊 Обновление остатков цифровых товаров
  • ⏰ Соблюдение дедлайнов загрузки кодов (24 часа)

Основные возможности

🎯 Управление отправлениями

  • Получение списка отправлений, требующих загрузки кодов
  • Фильтрация по дате и статусу
  • Получение аналитических и финансовых данных

🔐 Управление кодами

  • Загрузка цифровых кодов (лицензии, ключи, коды активации)
  • Обработка недоступных кодов
  • Контроль качества загруженных кодов

📦 Управление остатками

  • Обновление информации о количестве доступных цифровых товаров
  • Пакетные обновления остатков
  • Обработка ошибок валидации

⚠️ Ограничения

  • Доступно только продавцам, работающим с цифровыми товарами
  • Коды должны быть загружены в течение 24 часов с момента получения заказа
  • Рекомендуемый размер батча для обновления остатков: 100 товаров

Методы API

getDigitalPostingsList()

Назначение: Получить список отправлений с цифровыми товарами

interface DigitalListPostingCodesRequest {
  dir?: 'ASC' | 'DESC';
  filter?: {
    since?: string;
    to?: string;
    cutoff_from?: string;
    cutoff_to?: string;
  };
  limit?: number;
  offset?: number;
  with?: {
    financial_data?: boolean;
    analytics_data?: boolean;
    legal_info?: boolean;
  };
}

uploadDigitalCodes()

Назначение: Загрузить коды цифровых товаров для отправления

interface DigitalUploadPostingCodesRequest {
  posting_number: string;
  exemplars_by_sku: Array<{
    sku: number;
    exemplar_qty: number;
    not_available_exemplar_qty: number;
    exemplar_keys: string[];
  }>;
}

updateDigitalStocks()

Назначение: Обновить остатки цифровых товаров

interface DigitalStocksImportRequest {
  stocks: Array<{
    offer_id: string;
    stock: number;
  }>;
}

Практические примеры

Базовое использование

import { OzonSellerAPI } from 'bmad-ozon-seller-api';

const api = new OzonSellerAPI({
  clientId: 'your-client-id',
  apiKey: 'your-api-key'
});

// Получить отправления, ожидающие загрузки кодов
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);

const digitalPostings = await api.digital.getDigitalPostingsList({
  filter: {
    since: weekAgo.toISOString().split('T')[0],
    to: new Date().toISOString().split('T')[0]
  },
  limit: 100,
  with: {
    financial_data: true,
    analytics_data: true
  }
});

// Загрузить коды для отправления
await api.digital.uploadDigitalCodes({
  posting_number: '12345-0001-1',
  exemplars_by_sku: [{
    sku: 123456789,
    exemplar_qty: 3,
    not_available_exemplar_qty: 0,
    exemplar_keys: [
      'GAME_KEY_001_ABC123',
      'GAME_KEY_002_DEF456', 
      'GAME_KEY_003_GHI789'
    ]
  }]
});

// Обновить остатки цифровых товаров
await api.digital.updateDigitalStocks({
  stocks: [
    {
      offer_id: 'GAME_DIGITAL_001',
      stock: 50
    },
    {
      offer_id: 'SOFTWARE_LICENSE_002', 
      stock: 25
    }
  ]
});

Продвинутые сценарии

Менеджер цифровых заказов

class DigitalOrderManager {
  constructor(private api: OzonSellerAPI) {}

  async processDigitalOrders(): Promise<void> {
    // Получить все заказы, ожидающие загрузки кодов
    const pendingOrders = await this.api.digital.getDigitalPostingsList({
      filter: {
        since: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
      },
      limit: 1000,
      with: {
        analytics_data: true,
        financial_data: true
      }
    });

    const ordersNeedingCodes = pendingOrders.result?.filter(posting => 
      posting.status === 'awaiting_packaging' &&
      posting.products?.some(product => product.required_qty_for_digital_code > 0)
    ) || [];

    console.log(`Найдено ${ordersNeedingCodes.length} заказов, требующих загрузки кодов`);

    for (const order of ordersNeedingCodes) {
      await this.uploadCodesForOrder(order);
      
      // Задержка между обработкой заказов
      await new Promise(resolve => setTimeout(resolve, 500));
    }
  }

  private async uploadCodesForOrder(order: any): Promise<void> {
    try {
      const codesByProduct = [];
      
      for (const product of order.products || []) {
        if (product.required_qty_for_digital_code > 0) {
          const codes = await this.generateCodesForProduct(
            product.sku, 
            product.required_qty_for_digital_code
          );
          
          codesByProduct.push({
            sku: product.sku,
            exemplar_qty: codes.length,
            not_available_exemplar_qty: 0,
            exemplar_keys: codes
          });
        }
      }

      if (codesByProduct.length > 0) {
        const result = await this.api.digital.uploadDigitalCodes({
          posting_number: order.posting_number,
          exemplars_by_sku: codesByProduct
        });

        this.validateUploadResults(order.posting_number, result);
      }
    } catch (error) {
      console.error(`Ошибка обработки заказа ${order.posting_number}:`, error);
    }
  }

  private async generateCodesForProduct(sku: number, quantity: number): Promise<string[]> {
    // Логика генерации или получения кодов из внешней системы
    const codes: string[] = [];
    
    for (let i = 0; i < quantity; i++) {
      codes.push(`${sku}_KEY_${Date.now()}_${Math.random().toString(36).substring(7).toUpperCase()}`);
    }
    
    return codes;
  }

  private validateUploadResults(postingNumber: string, result: any): void {
    let totalReceived = 0;
    let totalRejected = 0;

    result.exemplars_by_sku?.forEach((skuResult: any) => {
      totalReceived += skuResult.received_qty || 0;
      totalRejected += skuResult.rejected_qty || 0;
      
      if (skuResult.failed_exemplars?.length > 0) {
        console.warn(`Отклоненные коды для SKU ${skuResult.sku} в заказе ${postingNumber}:`);
        skuResult.failed_exemplars.forEach((error: any) => {
          console.warn(`  ${error.code}: ${error.message}`);
        });
      }
    });

    console.log(`Заказ ${postingNumber}: принято кодов ${totalReceived}, отклонено ${totalRejected}`);
  }
}

Система мониторинга остатков

class DigitalStockMonitor {
  constructor(
    private api: OzonSellerAPI,
    private lowStockThreshold: number = 10
  ) {}

  async monitorAndUpdateStocks(): Promise<void> {
    // Получить текущие остатки из внешней системы
    const stockData = await this.getExternalStockData();
    
    // Найти товары с низкими остатками
    const lowStockItems = stockData.filter(item => item.availableStock <= this.lowStockThreshold);
    
    if (lowStockItems.length > 0) {
      console.warn(`Обнаружено ${lowStockItems.length} товаров с низкими остатками`);
      await this.sendLowStockAlert(lowStockItems);
    }
    
    // Обновить остатки в OZON
    await this.updateStocksBatch(stockData);
    
    // Генерация отчета по остаткам
    await this.generateStockReport(stockData);
  }

  private async updateStocksBatch(stockData: any[]): Promise<void> {
    const batchSize = 100;
    let successCount = 0;
    let errorCount = 0;
    
    for (let i = 0; i < stockData.length; i += batchSize) {
      const batch = stockData.slice(i, i + batchSize);
      
      try {
        const result = await this.api.digital.updateDigitalStocks({
          stocks: batch.map(item => ({
            offer_id: item.offerId,
            stock: item.availableStock
          }))
        });
        
        // Подсчет результатов
        result.status?.forEach(status => {
          if (status.updated) {
            successCount++;
          } else {
            errorCount++;
            console.error(`Ошибка обновления ${status.offer_id}:`, status.errors);
          }
        });
        
        console.log(`Обработан батч ${Math.floor(i / batchSize) + 1}/${Math.ceil(stockData.length / batchSize)}`);
        
        // Задержка между батчами
        if (i + batchSize < stockData.length) {
          await new Promise(resolve => setTimeout(resolve, 1000));
        }
      } catch (error) {
        console.error(`Ошибка обновления батча:`, error);
        errorCount += batch.length;
      }
    }
    
    console.log(`Обновление остатков завершено: успешно ${successCount}, ошибок ${errorCount}`);
  }

  private async getExternalStockData(): Promise<any[]> {
    // Эмуляция получения данных из внешней системы
    return [
      { offerId: 'GAME_DIGITAL_001', availableStock: 50 },
      { offerId: 'SOFTWARE_LICENSE_002', availableStock: 5 }, // Низкий остаток
      { offerId: 'DIGITAL_BOOK_003', availableStock: 100 },
      { offerId: 'MUSIC_ALBUM_004', availableStock: 2 }, // Критически низкий остаток
    ];
  }

  private async sendLowStockAlert(lowStockItems: any[]): Promise<void> {
    // Логика отправки уведомлений о низких остатках
    console.log('🚨 Уведомление о низких остатках:');
    lowStockItems.forEach(item => {
      const severity = item.availableStock <= 5 ? '🔴 КРИТИЧНО' : '🟡 ВНИМАНИЕ';
      console.log(`${severity} ${item.offerId}: ${item.availableStock} шт.`);
    });
  }

  private async generateStockReport(stockData: any[]): Promise<void> {
    const totalItems = stockData.length;
    const totalStock = stockData.reduce((sum, item) => sum + item.availableStock, 0);
    const avgStock = Math.round(totalStock / totalItems);
    const lowStockCount = stockData.filter(item => item.availableStock <= this.lowStockThreshold).length;
    
    console.log('\n📊 Отчет по остаткам цифровых товаров:');
    console.log(`Всего товаров: ${totalItems}`);
    console.log(`Общий остаток: ${totalStock} шт.`);
    console.log(`Средний остаток: ${avgStock} шт.`);
    console.log(`Товаров с низким остатком: ${lowStockCount} (${Math.round(lowStockCount / totalItems * 100)}%)`);
  }
}

Аналитическая система отчетности

class DigitalAnalyticsReporter {
  constructor(private api: OzonSellerAPI) {}

  async generateDigitalSalesReport(dateFrom: string, dateTo: string): Promise<void> {
    const digitalPostings = await this.api.digital.getDigitalPostingsList({
      filter: {
        since: dateFrom,
        to: dateTo
      },
      limit: 1000,
      with: {
        financial_data: true,
        analytics_data: true
      }
    });

    const analytics = this.analyzeDigitalSales(digitalPostings.result || []);
    this.generateReport(analytics, dateFrom, dateTo);
  }

  private analyzeDigitalSales(postings: any[]): any {
    const analytics = {
      totalOrders: postings.length,
      totalRevenue: 0,
      totalCommission: 0,
      productStats: new Map(),
      regionStats: new Map(),
      statusStats: new Map(),
      codeUploadStats: {
        totalRequired: 0,
        uploaded: 0,
        pending: 0
      }
    };

    postings.forEach(posting => {
      // Финансовая аналитика
      if (posting.financial_data) {
        analytics.totalRevenue += posting.financial_data.order_amount || 0;
        analytics.totalCommission += posting.financial_data.commission || 0;
      }

      // Статистика статусов
      const currentCount = analytics.statusStats.get(posting.status) || 0;
      analytics.statusStats.set(posting.status, currentCount + 1);

      // Региональная аналитика
      if (posting.analytics_data?.region) {
        const regionCount = analytics.regionStats.get(posting.analytics_data.region) || 0;
        analytics.regionStats.set(posting.analytics_data.region, regionCount + 1);
      }

      // Аналитика по продуктам и кодам
      posting.products?.forEach((product: any) => {
        const sku = product.sku.toString();
        if (!analytics.productStats.has(sku)) {
          analytics.productStats.set(sku, {
            name: product.name,
            totalSold: 0,
            totalRevenue: 0,
            codesRequired: 0
          });
        }

        const productStat = analytics.productStats.get(sku);
        productStat.totalSold += product.quantity;
        productStat.totalRevenue += (product.price || 0) * product.quantity;
        productStat.codesRequired += product.required_qty_for_digital_code || 0;

        analytics.codeUploadStats.totalRequired += product.required_qty_for_digital_code || 0;
        
        if (product.required_qty_for_digital_code > 0 && posting.status === 'awaiting_packaging') {
          analytics.codeUploadStats.pending += product.required_qty_for_digital_code;
        } else if (product.required_qty_for_digital_code > 0) {
          analytics.codeUploadStats.uploaded += product.required_qty_for_digital_code;
        }
      });
    });

    return analytics;
  }

  private generateReport(analytics: any, dateFrom: string, dateTo: string): void {
    console.log(`\n📈 Отчет по цифровым товарам (${dateFrom} - ${dateTo})`);
    console.log('='.repeat(60));
    
    // Общая статистика
    console.log('\n📊 Общая статистика:');
    console.log(`Всего заказов: ${analytics.totalOrders}`);
    console.log(`Общая выручка: ${analytics.totalRevenue.toFixed(2)} руб.`);
    console.log(`Общая комиссия: ${analytics.totalCommission.toFixed(2)} руб.`);
    console.log(`Чистая прибыль: ${(analytics.totalRevenue - analytics.totalCommission).toFixed(2)} руб.`);
    console.log(`Средний чек: ${(analytics.totalRevenue / analytics.totalOrders).toFixed(2)} руб.`);

    // Статистика по кодам
    console.log('\n🔑 Статистика по кодам:');
    console.log(`Всего требуется кодов: ${analytics.codeUploadStats.totalRequired}`);
    console.log(`Загружено кодов: ${analytics.codeUploadStats.uploaded}`);
    console.log(`Ожидает загрузки: ${analytics.codeUploadStats.pending}`);
    
    if (analytics.codeUploadStats.totalRequired > 0) {
      const uploadRate = (analytics.codeUploadStats.uploaded / analytics.codeUploadStats.totalRequired * 100).toFixed(1);
      console.log(`Процент загрузки: ${uploadRate}%`);
    }

    // Топ товары
    console.log('\n🏆 Топ-5 товаров по продажам:');
    const topProducts = Array.from(analytics.productStats.entries())
      .sort(([,a], [,b]) => b.totalRevenue - a.totalRevenue)
      .slice(0, 5);
      
    topProducts.forEach(([sku, stats], index) => {
      console.log(`${index + 1}. ${stats.name} (SKU: ${sku})`);
      console.log(`   Продано: ${stats.totalSold} шт., Выручка: ${stats.totalRevenue.toFixed(2)} руб.`);
    });

    // Статистика по регионам
    if (analytics.regionStats.size > 0) {
      console.log('\n🌍 Топ-5 регионов:');
      const topRegions = Array.from(analytics.regionStats.entries())
        .sort(([,a], [,b]) => b - a)
        .slice(0, 5);
        
      topRegions.forEach(([region, count], index) => {
        const percentage = (count / analytics.totalOrders * 100).toFixed(1);
        console.log(`${index + 1}. ${region}: ${count} заказов (${percentage}%)`);
      });
    }

    // Статистика по статусам
    console.log('\n📦 Статусы заказов:');
    Array.from(analytics.statusStats.entries()).forEach(([status, count]) => {
      const percentage = (count / analytics.totalOrders * 100).toFixed(1);
      console.log(`${status}: ${count} заказов (${percentage}%)`);
    });
  }
}

Обработка ошибок

try {
  await api.digital.uploadDigitalCodes({
    posting_number: '12345-0001-1',
    exemplars_by_sku: [/* ... */]
  });
} catch (error) {
  if (error.response?.status === 400) {
    console.error('Ошибка валидации данных:', error.response.data);
  } else if (error.response?.status === 403) {
    console.error('Нет доступа к цифровым товарам');
  } else if (error.response?.status === 404) {
    console.error('Отправление не найдено');
  } else {
    console.error('Неожиданная ошибка:', error.message);
  }
}

Рекомендации по использованию

🎯 Производительность

  • Используйте пакетные обновления остатков (до 100 товаров за запрос)
  • Добавляйте задержки между запросами для избежания rate limiting
  • Кешируйте результаты получения списка отправлений

🔒 Безопасность

  • Храните цифровые коды в зашифрованном виде
  • Логируйте все операции с кодами для аудита
  • Используйте уникальные и сложные коды для предотвращения мошенничества

📊 Мониторинг

  • Отслеживайте дедлайны загрузки кодов (24 часа)
  • Мониторьте низкие остатки цифровых товаров
  • Анализируйте статистику отклоненных кодов

🚀 Автоматизация

  • Автоматизируйте процесс генерации и загрузки кодов
  • Настройте уведомления о низких остатках
  • Интегрируйте с внешними системами управления лицензиями

Digital API предоставляет полный контроль над цифровыми товарами, от управления отправлениями до обновления остатков, обеспечивая эффективную работу с цифровым контентом на платформе OZON.