Разработка бота для автоматического обновления остатков на маркетплейсах

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка бота для автоматического обновления остатков на маркетплейсах
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка бота для автоматического обновления остатков на маркетплейсах

Актуальный сток на маркетплейсах — это защита от двух болей: продажи отсутствующего товара (ведёт к отменам, штрафам, снижению рейтинга) и заниженного стока, из-за которого маркетплейс занижает позиции карточки. Бот синхронизирует остатки между вашей системой учёта и маркетплейсами без ручного вмешательства.

Источники данных об остатках

Источников может быть несколько, их нужно агрегировать:

  • Складская система (1С, МойСклад, Odoo) — основной источник
  • Поставщики — синхронизируются через импорт
  • Маркетплейсы — нужно читать остаток, зарезервированный платформой
  • Собственный сайт — виртуальный резерв от открытых корзин
1С / МойСклад → (вебхук или polling) → Stock Aggregator → Marketplace API

Схема данных

CREATE TABLE stock_levels (
    id              BIGSERIAL PRIMARY KEY,
    product_id      BIGINT REFERENCES products(id),
    warehouse_id    INT REFERENCES warehouses(id),
    quantity        INT NOT NULL DEFAULT 0,
    reserved        INT NOT NULL DEFAULT 0,    -- зарезервировано платформами
    available       INT GENERATED ALWAYS AS (quantity - reserved) STORED,
    updated_at      TIMESTAMP DEFAULT NOW()
);

CREATE TABLE marketplace_stocks (
    id              BIGSERIAL PRIMARY KEY,
    product_id      BIGINT REFERENCES products(id),
    marketplace     VARCHAR(50) NOT NULL,
    warehouse_code  VARCHAR(100),              -- код склада на маркетплейсе
    synced_quantity INT,
    last_synced_at  TIMESTAMP,
    sync_status     VARCHAR(20) DEFAULT 'ok',  -- 'ok', 'error', 'pending'
    error_message   TEXT,
    UNIQUE(product_id, marketplace, warehouse_code)
);

-- Лог изменений для аудита
CREATE TABLE stock_sync_log (
    id              BIGSERIAL PRIMARY KEY,
    product_id      BIGINT,
    marketplace     VARCHAR(50),
    old_qty         INT,
    new_qty         INT,
    source          VARCHAR(50),               -- '1c', 'webhook', 'manual'
    synced_at       TIMESTAMP DEFAULT NOW()
);

Интеграция с Ozon API

class OzonStockSyncer
{
    public function syncStocks(array $items): SyncResult
    {
        // Ozon принимает до 100 позиций за запрос
        $result  = new SyncResult();
        $batches = array_chunk($items, 100);

        foreach ($batches as $batch) {
            $payload = array_map(fn($item) => [
                'offer_id'          => $item['sku'],
                'stock'             => $item['qty'],
                'warehouse_id'      => $item['warehouse_id'],
            ], $batch);

            $response = Http::withHeaders([
                'Client-Id' => $this->clientId,
                'Api-Key'   => $this->apiKey,
            ])->post('https://api-seller.ozon.ru/v2/products/stocks', [
                'stocks' => $payload,
            ]);

            if (!$response->successful()) {
                $result->errors[] = $response->json('message', 'Unknown error');
                continue;
            }

            foreach ($response->json('result', []) as $item) {
                if ($item['updated']) {
                    $result->updated++;
                } else {
                    $result->errors[] = "SKU {$item['offer_id']}: " . ($item['errors'][0]['message'] ?? 'error');
                }
            }
        }

        return $result;
    }
}

Интеграция с Wildberries

class WildberriesStockSyncer
{
    public function syncStocks(array $items, int $warehouseId): SyncResult
    {
        // WB API v3 — обновление остатков
        $payload = array_map(fn($item) => [
            'sku'    => $item['wb_barcode'],  // штрихкод WB, не артикул
            'amount' => max(0, $item['qty']),
        ], $items);

        $response = Http::withToken($this->apiKey)
            ->put("https://marketplace-api.wildberries.ru/api/v3/warehouses/{$warehouseId}/stocks", [
                'stocks' => $payload,
            ]);

        if (!$response->successful()) {
            throw new WildberriesApiException($response->json('title', 'API Error'));
        }

        return new SyncResult(updated: count($items));
    }
}

Получение остатков из 1С

Через HTTP-сервис 1С (REST):

class OneCStockClient
{
    public function getStocks(?Carbon $changedAfter = null): array
    {
        $params = ['format' => 'json'];
        if ($changedAfter) {
            $params['changedAfter'] = $changedAfter->toISOString();
        }

        $response = Http::withBasicAuth($this->user, $this->password)
            ->timeout(60)
            ->get("{$this->baseUrl}/hs/stocks/list", $params);

        return $response->json('stocks', []);
    }
}

Через вебхук (1С пушит изменения):

// routes/api.php
Route::post('/webhooks/1c/stock', [StockWebhookController::class, 'handle'])
    ->middleware('auth.webhook:1c');

class StockWebhookController extends Controller
{
    public function handle(Request $request): JsonResponse
    {
        $data = $request->validate([
            'stocks' => 'required|array',
            'stocks.*.sku'      => 'required|string',
            'stocks.*.quantity' => 'required|integer|min:0',
        ]);

        foreach ($data['stocks'] as $item) {
            UpdateStockJob::dispatch($item['sku'], $item['quantity'], 'webhook_1c');
        }

        return response()->json(['accepted' => count($data['stocks'])]);
    }
}

Job синхронизации остатков

class SyncMarketplaceStocksJob implements ShouldQueue
{
    public int $timeout = 300;

    public function handle(
        OzonStockSyncer       $ozon,
        WildberriesStockSyncer $wb,
    ): void {
        // Получить товары, у которых сток изменился с последней синхронизации
        $changed = Product::whereHas('stockChanges', fn($q) =>
            $q->where('changed_at', '>', now()->subHour())
        )->with('marketplaceSkus')->get();

        if ($changed->isEmpty()) return;

        // Группировка по маркетплейсу
        $ozonItems = $changed->filter(fn($p) => $p->hasMarketplace('ozon'))
            ->map(fn($p) => [
                'sku'          => $p->ozon_sku,
                'qty'          => $p->available_stock,
                'warehouse_id' => config('ozon.warehouse_id'),
            ])->values()->toArray();

        if ($ozonItems) {
            $result = $ozon->syncStocks($ozonItems);
            Log::info("Ozon stock sync: {$result->updated} updated, " . count($result->errors) . " errors");
        }

        // Аналогично для WB...
    }
}

Буферный сток

Часто нужно держать «буфер» — не выгружать весь остаток на маркетплейс, чтобы резервировать для других каналов или страховаться от ошибок:

class StockCalculator
{
    public function calculateMarketplaceQty(Product $product, string $marketplace): int
    {
        $available = $product->available_stock;

        // Абсолютный буфер
        $buffer = $product->stock_buffer ?? config("marketplaces.{$marketplace}.default_buffer", 2);

        // Процентный буфер (например, 10% для WB)
        $pctBuffer = (int) ceil($available * config("marketplaces.{$marketplace}.buffer_pct", 0) / 100);

        $reserved = max($buffer, $pctBuffer);
        $qty      = max(0, $available - $reserved);

        // Верхний лимит (не выгружать больше N единиц на площадку)
        $maxQty = $product->max_marketplace_stock ?? PHP_INT_MAX;

        return min($qty, $maxQty);
    }
}

Алерты при критических ситуациях

class StockAlertService
{
    public function checkCritical(Product $product): void
    {
        // Остаток на сайте > 0, но на маркетплейсе 0 уже 2+ часа
        $marketplaceZero = MarketplaceStock::where('product_id', $product->id)
            ->where('synced_quantity', 0)
            ->where('last_synced_at', '<', now()->subHours(2))
            ->exists();

        if ($marketplaceZero && $product->available_stock > 0) {
            Notification::send($this->ops, new StockDesyncAlert($product));
        }
    }
}

Расписание

// Синхронизация каждые 15 минут
$schedule->job(new SyncMarketplaceStocksJob)->everyFifteenMinutes();

// Полная принудительная синхронизация раз в ночь
$schedule->job(new FullStockSyncJob)->dailyAt('02:00');

Сроки реализации

  • Ozon API + схема данных + базовый SyncJob: 1–2 дня
  • Wildberries API: +1 день
  • Интеграция с 1С (polling или webhook): 1–2 дня
  • Буферный сток + алерты: 0.5 дня
  • Лог синхронизации + дашборд: 0.5 дня

Итого: 4–5 рабочих дней.