Promos API

Promos API для управления акциями и скидками с 8 методами для участия в промо-кампаниях Ozon.

Обзор

Promos API предоставляет инструменты для участия в акциях Ozon, управления скидками и обработки заявок покупателей на льготные цены.

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

  • 🎯 Просмотр доступных акций Ozon
  • 📋 Получение списка товаров-кандидатов для акций
  • 🚀 Добавление и удаление товаров из акций
  • 💰 Обработка заявок покупателей на скидки
  • 📊 Мониторинг участвующих товаров
  • ⚡ Автоматизация промо-кампаний

Доступные методы

Управление акциями

getActions() - Список доступных акций

const actions = await promosApi.getActions();

getCandidates(request) - Товары-кандидаты для акции

const candidates = await promosApi.getCandidates({
  action_id: 12345,
  limit: 100,
  last_id: 0
});

getParticipatingProducts(request) - Участвующие в акции товары

const participants = await promosApi.getParticipatingProducts({
  action_id: 12345,
  limit: 100
});

Добавление и удаление товаров

activateProducts(request) - Добавить товары в акцию

const activation = await promosApi.activateProducts({
  action_id: 12345,
  products: [{
    product_id: 67890,
    action_price: '999',
    stock: 50
  }]
});

deactivateProducts(request) - Удалить товары из акции

const deactivation = await promosApi.deactivateProducts({
  action_id: 12345,
  product_ids: [67890, 11111]
});

Заявки на скидки

getDiscountTasks(request) - Список заявок на скидку

const tasks = await promosApi.getDiscountTasks({
  status: 'NEW',
  limit: 50,
  page: 1
});

approveDiscountTasks(request) - Согласовать заявки на скидку

const approval = await promosApi.approveDiscountTasks({
  tasks: [{
    task_id: 'task_123',
    product_id: 67890,
    discount_percentage: 15
  }]
});

declineDiscountTasks(request) - Отклонить заявки на скидку

const decline = await promosApi.declineDiscountTasks({
  tasks: [{
    task_id: 'task_456',
    product_id: 67890,
    decline_reason: 'Слишком большая скидка'
  }]
});

TypeScript интерфейсы

// Основные запросы
interface PromosGetProductsRequest {
  action_id: number;
  limit?: number;
  last_id?: number;
  page?: number;
}

interface PromosGetDiscountTasksRequest {
  status?: "NEW" | "SEEN" | "APPROVED" | "DECLINED";
  limit?: number;
  page?: number;
  product_id?: number;
}

interface PromosApproveDiscountTasksRequest {
  tasks: Array<{
    task_id: string;
    product_id: number;
    discount_percentage: number;
    action_price?: string;
  }>;
}

interface PromosDeclineDiscountTasksRequest {
  tasks: Array<{
    task_id: string;
    product_id: number;
    decline_reason: string;
  }>;
}

interface PromosActivateProductsRequest {
  action_id: number;
  products: Array<{
    product_id: number;
    action_price: string;
    stock?: number;
  }>;
}

interface PromosDeactivateProductsRequest {
  action_id: number;
  product_ids: number[];
}

// Ответы
interface PromosGetActionsResponse {
  result: Array<{
    id: number;
    title: string;
    description: string;
    date_start: string;
    date_end: string;
    status: "ACTIVE" | "UPCOMING" | "FINISHED" | "CANCELLED";
    is_participating_available: boolean;
    action_type: string;
    conditions: {
      min_discount_percentage?: number;
      max_discount_percentage?: number;
      min_action_price?: string;
      max_action_price?: string;
      categories?: number[];
      brands?: string[];
    };
    participation_info: {
      total_products: number;
      participating_products: number;
      max_products_allowed?: number;
    };
  }>;
}

interface PromosGetProductsResponse {
  result: {
    products: Array<{
      product_id: number;
      name: string;
      offer_id: string;
      sku: number;
      price: string;
      old_price?: string;
      currency_code: string;
      action_price?: string;
      stock?: number;
      is_available: boolean;
      restrictions?: Array<{
        type: string;
        message: string;
      }>;
    }>;
    has_next: boolean;
    last_id: number;
    total: number;
  };
}

interface PromosGetDiscountTasksResponse {
  result: Array<{
    task_id: string;
    product_id: number;
    product_name: string;
    offer_id: string;
    sku: number;
    current_price: string;
    desired_price: string;
    discount_percentage: number;
    status: "NEW" | "SEEN" | "APPROVED" | "DECLINED";
    customer_count: number;
    created_at: string;
    expires_at: string;
  }>;
  total: number;
  has_next: boolean;
}

interface PromosProcessDiscountTasksResponse {
  result: {
    processed_count: number;
    success_count: number;
    error_count: number;
    errors?: Array<{
      task_id: string;
      error_message: string;
    }>;
  };
}

interface PromosActivateProductsResponse {
  result: {
    results: Array<{
      product_id: number;
      is_updated: boolean;
      errors?: string[];
    }>;
  };
}

interface PromosDeactivateProductsResponse {
  result: {
    results: Array<{
      product_id: number;
      is_updated: boolean;
      errors?: string[];
    }>;
  };
}

Примеры использования

Участие в акции Ozon

// 1. Получение списка доступных акций
const actions = await promosApi.getActions();

const suitableActions = actions.result.filter(action => 
  action.status === "ACTIVE" && 
  action.is_participating_available &&
  action.conditions.min_discount_percentage <= 20
);

// 2. Анализ товаров-кандидатов
for (const action of suitableActions) {
  console.log(`\nАнализ акции: ${action.title}`);
  console.log(`Период: ${action.date_start} - ${action.date_end}`);
  
  const candidates = await promosApi.getCandidates({
    action_id: action.id,
    limit: 100
  });

  const suitableCandidates = candidates.result.products.filter(product => 
    product.is_available && 
    parseInt(product.price) > 500 // минимальная цена
  );

  console.log(`Подходящих товаров: ${suitableCandidates.length}`);

  // 3. Добавление товаров в акцию
  if (suitableCandidates.length > 0) {
    const productsToActivate = suitableCandidates.slice(0, 10).map(product => ({
      product_id: product.product_id,
      action_price: (parseInt(product.price) * 0.85).toString(), // скидка 15%
      stock: 50
    }));

    const activationResult = await promosApi.activateProducts({
      action_id: action.id,
      products: productsToActivate
    });

    console.log(`Успешно добавлено: ${activationResult.result.results.filter(r => r.is_updated).length} товаров`);
  }
}

Обработка заявок на скидки

// Получение новых заявок на скидки
const newTasks = await promosApi.getDiscountTasks({
  status: 'NEW',
  limit: 50
});

console.log(`Новых заявок на скидки: ${newTasks.result.length}`);

// Анализ и обработка заявок
const tasksToApprove: PromosApproveDiscountTasksRequest['tasks'] = [];
const tasksToDecline: PromosDeclineDiscountTasksRequest['tasks'] = [];

newTasks.result.forEach(task => {
  const currentPrice = parseInt(task.current_price);
  const desiredPrice = parseInt(task.desired_price);
  const discountPercent = ((currentPrice - desiredPrice) / currentPrice) * 100;

  console.log(`\nЗаявка на товар: ${task.product_name}`);
  console.log(`Текущая цена: ${task.current_price}, желаемая: ${task.desired_price}`);
  console.log(`Скидка: ${discountPercent.toFixed(1)}%`);
  console.log(`Количество покупателей: ${task.customer_count}`);

  // Автоматическое принятие решений
  if (discountPercent <= 15 && task.customer_count >= 5) {
    // Одобряем скидки до 15% при наличии 5+ покупателей
    tasksToApprove.push({
      task_id: task.task_id,
      product_id: task.product_id,
      discount_percentage: Math.min(discountPercent, 15)
    });
  } else if (discountPercent > 25) {
    // Отклоняем слишком большие скидки
    tasksToDecline.push({
      task_id: task.task_id,
      product_id: task.product_id,
      decline_reason: `Слишком большая скидка: ${discountPercent.toFixed(1)}%`
    });
  }
});

// Пакетное одобрение заявок
if (tasksToApprove.length > 0) {
  const approvalResult = await promosApi.approveDiscountTasks({
    tasks: tasksToApprove
  });
  console.log(`\nОдобрено заявок: ${approvalResult.result.success_count}`);
}

// Пакетное отклонение заявок
if (tasksToDecline.length > 0) {
  const declineResult = await promosApi.declineDiscountTasks({
    tasks: tasksToDecline
  });
  console.log(`Отклонено заявок: ${declineResult.result.success_count}`);
}

Мониторинг активных промо-кампаний

// Проверка участвующих товаров во всех акциях
const actions = await promosApi.getActions();
const activeActions = actions.result.filter(a => a.status === "ACTIVE");

for (const action of activeActions) {
  const participants = await promosApi.getParticipatingProducts({
    action_id: action.id,
    limit: 1000
  });

  console.log(`\nАкция: ${action.title}`);
  console.log(`Участвует товаров: ${participants.result.products.length}`);
  
  // Анализ производительности товаров в акции
  const lowStockProducts = participants.result.products.filter(p => 
    p.stock !== undefined && p.stock < 10
  );
  
  const expensiveProducts = participants.result.products.filter(p => 
    parseFloat(p.action_price || "0") > parseFloat(p.price) * 1.1
  );

  if (lowStockProducts.length > 0) {
    console.log(`⚠️ Товары с низким остатком: ${lowStockProducts.length}`);
    
    // Можно автоматически удалить товары с нулевым остатком
    const zeroStockProducts = lowStockProducts
      .filter(p => p.stock === 0)
      .map(p => p.product_id);

    if (zeroStockProducts.length > 0) {
      await promosApi.deactivateProducts({
        action_id: action.id,
        product_ids: zeroStockProducts
      });
      console.log(`Удалено товаров без остатка: ${zeroStockProducts.length}`);
    }
  }

  if (expensiveProducts.length > 0) {
    console.log(`💰 Товары с высокой ценой в акции: ${expensiveProducts.length}`);
  }
}

Сложные сценарии

PromoCampaignManager - Автоматизированная система промо-кампаний

class PromoCampaignManager {
  constructor(private api: PromosApi) {}

  async runAutomatedPromoStrategy(): Promise<PromoStrategyResult> {
    // 1. Анализ доступных акций
    const availableActions = await this.getOptimalActions();
    
    // 2. Подбор товаров для каждой акции
    const campaignPlan = await this.createCampaignPlan(availableActions);
    
    // 3. Исполнение плана
    const executionResults = await this.executeCampaignPlan(campaignPlan);
    
    // 4. Обработка заявок на скидки
    const discountResults = await this.processDiscountRequests();
    
    return {
      campaigns_executed: executionResults.length,
      total_products_activated: executionResults.reduce((sum, r) => sum + r.activated_count, 0),
      discount_tasks_processed: discountResults.processed_count,
      estimated_revenue_boost: this.calculateRevenueImpact(executionResults),
      recommendations: this.generateRecommendations(executionResults)
    };
  }

  private async getOptimalActions(): Promise<OptimalAction[]> {
    const actions = await this.api.getActions();
    
    return actions.result
      .filter(action => 
        action.status === "ACTIVE" && 
        action.is_participating_available &&
        new Date(action.date_end) > new Date(Date.now() + 24 * 60 * 60 * 1000) // минимум сутки до окончания
      )
      .map(action => ({
        ...action,
        priority: this.calculateActionPriority(action)
      }))
      .sort((a, b) => b.priority - a.priority)
      .slice(0, 5); // топ-5 акций
  }

  private calculateActionPriority(action: any): number {
    let score = 0;
    
    // Длительность акции (чем дольше, тем лучше)
    const daysLeft = Math.ceil((new Date(action.date_end).getTime() - Date.now()) / (24 * 60 * 60 * 1000));
    score += Math.min(daysLeft * 2, 20);
    
    // Тип акции (некоторые типы более выгодные)
    if (action.action_type === "FLASH_SALE") score += 15;
    if (action.action_type === "CATEGORY_PROMOTION") score += 10;
    
    // Ограничения на скидку (чем меньше минимальная скидка, тем лучше)
    if (action.conditions.min_discount_percentage) {
      score += Math.max(0, 20 - action.conditions.min_discount_percentage);
    }
    
    // Уже участвующие товары (не переполненная акция лучше)
    const participationRate = action.participation_info.participating_products / 
                             (action.participation_info.max_products_allowed || 1000);
    if (participationRate < 0.5) score += 10;
    
    return score;
  }

  private async createCampaignPlan(actions: OptimalAction[]): Promise<CampaignPlan[]> {
    const plans: CampaignPlan[] = [];
    
    for (const action of actions) {
      const candidates = await this.api.getCandidates({
        action_id: action.id,
        limit: 500
      });

      const selectedProducts = this.selectOptimalProducts(candidates.result.products, action);
      
      if (selectedProducts.length > 0) {
        plans.push({
          action_id: action.id,
          action_title: action.title,
          products: selectedProducts,
          expected_sales_boost: this.estimateSalesBoost(selectedProducts, action)
        });
      }
    }
    
    return plans;
  }

  private selectOptimalProducts(candidates: any[], action: any): PromoProduct[] {
    return candidates
      .filter(candidate => candidate.is_available)
      .map(candidate => ({
        product_id: candidate.product_id,
        name: candidate.name,
        current_price: parseFloat(candidate.price),
        suggested_action_price: this.calculateOptimalActionPrice(candidate, action),
        expected_margin: this.calculateExpectedMargin(candidate, action),
        priority_score: this.calculateProductPriority(candidate, action)
      }))
      .sort((a, b) => b.priority_score - a.priority_score)
      .slice(0, 50); // максимум 50 товаров на акцию
  }

  private async processDiscountRequests(): Promise<DiscountProcessResult> {
    const tasks = await this.api.getDiscountTasks({
      status: 'NEW',
      limit: 100
    });

    const decisions = this.analyzeDiscountRequests(tasks.result);
    
    // Пакетное одобрение
    if (decisions.approve.length > 0) {
      await this.api.approveDiscountTasks({ tasks: decisions.approve });
    }
    
    // Пакетное отклонение
    if (decisions.decline.length > 0) {
      await this.api.declineDiscountTasks({ tasks: decisions.decline });
    }

    return {
      processed_count: decisions.approve.length + decisions.decline.length,
      approved_count: decisions.approve.length,
      declined_count: decisions.decline.length,
      total_potential_revenue: decisions.approve.reduce((sum, task) => 
        sum + (parseFloat(task.discount_percentage.toString()) * task.product_id), 0)
    };
  }
}

interface OptimalAction {
  id: number;
  title: string;
  description: string;
  date_start: string;
  date_end: string;
  status: string;
  priority: number;
}

interface CampaignPlan {
  action_id: number;
  action_title: string;
  products: PromoProduct[];
  expected_sales_boost: number;
}

interface PromoProduct {
  product_id: number;
  name: string;
  current_price: number;
  suggested_action_price: number;
  expected_margin: number;
  priority_score: number;
}

interface PromoStrategyResult {
  campaigns_executed: number;
  total_products_activated: number;
  discount_tasks_processed: number;
  estimated_revenue_boost: number;
  recommendations: string[];
}

SmartDiscountAnalyzer - Интеллектуальный анализатор скидок

class SmartDiscountAnalyzer {
  constructor(private api: PromosApi) {}

  async analyzeDiscountOpportunities(): Promise<DiscountAnalysisReport> {
    // Получение всех заявок на скидки
    const allTasks = await this.getAllDiscountTasks();
    
    // Группировка по товарам и анализ трендов
    const productAnalysis = this.groupTasksByProduct(allTasks);
    
    // Анализ сезонности и трендов
    const trends = this.analyzeTrends(allTasks);
    
    // Конкурентный анализ
    const competitiveInsights = this.analyzeCompetitivePosition(allTasks);
    
    return {
      total_discount_requests: allTasks.length,
      unique_products: Object.keys(productAnalysis).length,
      average_discount_requested: this.calculateAverageDiscount(allTasks),
      trend_analysis: trends,
      competitive_insights: competitiveInsights,
      high_demand_products: this.identifyHighDemandProducts(productAnalysis),
      pricing_recommendations: this.generatePricingRecommendations(productAnalysis)
    };
  }

  private async getAllDiscountTasks(): Promise<DiscountTask[]> {
    const tasks: DiscountTask[] = [];
    let page = 1;
    let hasMore = true;

    while (hasMore) {
      const response = await this.api.getDiscountTasks({
        limit: 100,
        page: page
      });

      tasks.push(...response.result);
      hasMore = response.has_next;
      page++;

      // Rate limiting
      await new Promise(resolve => setTimeout(resolve, 100));
    }

    return tasks;
  }

  private analyzeTrends(tasks: DiscountTask[]): TrendAnalysis {
    const dailyRequests = new Map<string, number>();
    const weeklyDiscounts = new Map<number, number[]>();

    tasks.forEach(task => {
      const date = task.created_at.split('T')[0];
      dailyRequests.set(date, (dailyRequests.get(date) || 0) + 1);

      const week = Math.floor(Date.now() / (7 * 24 * 60 * 60 * 1000));
      if (!weeklyDiscounts.has(week)) {
        weeklyDiscounts.set(week, []);
      }
      weeklyDiscounts.get(week)!.push(task.discount_percentage);
    });

    return {
      peak_request_days: this.findPeakDays(dailyRequests),
      average_weekly_discount: this.calculateWeeklyAverages(weeklyDiscounts),
      seasonal_patterns: this.identifySeasonalPatterns(dailyRequests),
      growth_rate: this.calculateGrowthRate(dailyRequests)
    };
  }

  private generatePricingRecommendations(productAnalysis: Map<number, ProductDiscountInfo>): PricingRecommendation[] {
    const recommendations: PricingRecommendation[] = [];

    productAnalysis.forEach((info, productId) => {
      const avgRequestedDiscount = info.requests.reduce((sum, r) => sum + r.discount_percentage, 0) / info.requests.length;
      const totalCustomerInterest = info.requests.reduce((sum, r) => sum + r.customer_count, 0);

      if (totalCustomerInterest >= 20 && avgRequestedDiscount <= 15) {
        recommendations.push({
          product_id: productId,
          recommendation_type: "PROACTIVE_DISCOUNT",
          suggested_discount: Math.floor(avgRequestedDiscount * 0.8), // 80% от запрашиваемой скидки
          rationale: `Высокий интерес (${totalCustomerInterest} покупателей), разумный размер скидки`,
          priority: "HIGH",
          estimated_sales_increase: Math.floor(totalCustomerInterest * 1.5)
        });
      } else if (avgRequestedDiscount > 25) {
        recommendations.push({
          product_id: productId,
          recommendation_type: "PRICE_REVIEW",
          suggested_discount: 0,
          rationale: `Слишком высокие запросы на скидку (${avgRequestedDiscount.toFixed(1)}%) - возможно, цена завышена`,
          priority: "MEDIUM",
          estimated_sales_increase: 0
        });
      }
    });

    return recommendations.sort((a, b) => {
      const priorityOrder = { "HIGH": 3, "MEDIUM": 2, "LOW": 1 };
      return priorityOrder[b.priority] - priorityOrder[a.priority];
    });
  }
}

interface DiscountTask {
  task_id: string;
  product_id: number;
  product_name: string;
  current_price: string;
  desired_price: string;
  discount_percentage: number;
  customer_count: number;
  created_at: string;
  status: string;
}

interface DiscountAnalysisReport {
  total_discount_requests: number;
  unique_products: number;
  average_discount_requested: number;
  trend_analysis: TrendAnalysis;
  competitive_insights: CompetitiveInsights;
  high_demand_products: HighDemandProduct[];
  pricing_recommendations: PricingRecommendation[];
}

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

try {
  const activationResult = await promosApi.activateProducts({
    action_id: 12345,
    products: [{
      product_id: 67890,
      action_price: '999',
      stock: 50
    }]
  });

  activationResult.result.results.forEach(result => {
    if (!result.is_updated && result.errors) {
      console.error(`Ошибки для товара ${result.product_id}:`, result.errors);
    }
  });
} catch (error) {
  if (error.response?.status === 400) {
    console.error("Ошибка валидации:", error.response.data);
  } else if (error.response?.status === 403) {
    console.error("Недостаточно прав для участия в акции");
  } else {
    console.error("Неожиданная ошибка:", error.message);
  }
}

Лучшие практики

Стратегия участия в акциях

// Анализ ROI перед участием в акции
function calculatePromoROI(product: any, actionPrice: string): number {
  const currentMargin = parseFloat(product.price) * 0.3; // предполагаемая маржа 30%
  const actionMargin = parseFloat(actionPrice) * 0.3;
  const expectedSalesIncrease = 2.5; // ожидаемый рост продаж в 2.5 раза
  
  const currentProfit = currentMargin * 10; // текущая прибыль с 10 продаж
  const actionProfit = actionMargin * 10 * expectedSalesIncrease;
  
  return (actionProfit - currentProfit) / currentProfit;
}

// Автоматическое управление остатками в акциях
async function managePromoStock(
  promosApi: PromosApi, 
  actionId: number, 
  targetStockLevel: number
): Promise<void> {
  const participants = await promosApi.getParticipatingProducts({
    action_id: actionId,
    limit: 1000
  });

  const lowStockProducts = participants.result.products.filter(p => 
    p.stock !== undefined && p.stock < targetStockLevel
  );

  if (lowStockProducts.length > 0) {
    console.log(`Товары с низким остатком в акции ${actionId}: ${lowStockProducts.length}`);
    
    // Здесь можно интегрироваться с системой пополнения остатков
    // или временно удалить товары из акции
  }
}

Оптимизация обработки заявок

// Умная система принятия решений по заявкам на скидки
class DiscountDecisionEngine {
  private readonly rules = [
    {
      condition: (task: any) => task.discount_percentage <= 10 && task.customer_count >= 3,
      action: "APPROVE",
      priority: 1
    },
    {
      condition: (task: any) => task.discount_percentage <= 20 && task.customer_count >= 10,
      action: "APPROVE", 
      priority: 2
    },
    {
      condition: (task: any) => task.discount_percentage > 30,
      action: "DECLINE",
      priority: 1,
      reason: "Слишком большая скидка"
    }
  ];

  processTask(task: any): { action: string; reason?: string } {
    for (const rule of this.rules.sort((a, b) => a.priority - b.priority)) {
      if (rule.condition(task)) {
        return {
          action: rule.action,
          reason: rule.reason
        };
      }
    }
    return { action: "MANUAL_REVIEW" };
  }
}

Интеграционные заметки

  • Rate Limiting: API поддерживает до 500 запросов в минуту для акций
  • Batch Operations: Рекомендуется обрабатывать товары и заявки пакетами
  • Real-time Updates: Статусы акций обновляются в режиме реального времени
  • Stock Synchronization: Остатки товаров в акциях должны синхронизироваться с основными остатками
  • Price Validation: Цены в акциях проверяются на соответствие минимальным требованиям
  • Action Limits: Существуют лимиты на количество товаров в одной акции
  • Geographic Restrictions: Некоторые акции могут быть доступны только в определенных регионах