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

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, 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 робочих днів.