Реалізація мультиканальних продажів (Omnichannel) на сайті
Уявіть: клієнт замовляє стілець на сайті, через годину забирає його в магазині, але на вітрині стілець ще висить — уже проданий. Або промокод працює тільки на сайті, а в додатку — ні. Це класичні наслідки розрізнених каналів. Ми інтегруємо мультиканальні продажі так, що клієнт бачить єдину картину: однакові ціни на сайті та маркетплейсах, історію замовлень в особистому кабінеті, можливість повернути товар з Ozon через магазин. Технічно — це зв'язка API, message broker (RabbitMQ) та єдиного профілю в PostgreSQL.
Чому Omnichannel — це не просто декілька магазинів?
Багато хто думає: «Підключили API маркетплейсів — ось вам омніканальність». Насправді виникає хаос: на сайті товар є, на Ozon закінчився, клієнт скаржиться. Або знижка діє лише в офлайн-магазині — користувач роздратований. Корінь проблеми — розрізнені джерела даних: ціни, залишки, замовлення живуть у різних системах без спільного ключа.
Ми вирішуємо це через Unified Commerce Core — мікросервіс, який тримає інвентар, клієнтів і промо в одному місці. Всі канали звертаються до нього, а не один до одного. Це усуває N+1 запитів і конфлікти. Для глибокого розуміння — механізм мультиканальності в рітейлі.
Як ми забезпечуємо єдиний профіль клієнта?
Клієнт може зареєструватися на сайті, купити через WB, а повернути в магазині — і система повинна зрозуміти, що це одна людина. Без цього неможлива персоналізація та історія замовлень.
Ми реалізуємо Customer Identity Resolution через ланцюжок: email → телефон → ім'я+адреса. Приклад коду:
class CustomerIdentityResolver
{
public function resolve(array $customerData, string $source): Customer
{
$customer = null;
if (!empty($customerData['email'])) {
$customer = Customer::where('email', $customerData['email'])->first();
}
if (!$customer && !empty($customerData['phone'])) {
$normalized = $this->normalizePhone($customerData['phone']);
$customer = Customer::where('phone_normalized', $normalized)->first();
}
if (!$customer) {
$customer = Customer::create([
'name' => $customerData['name'],
'email' => $customerData['email'] ?? null,
'phone_normalized' => $normalized ?? null,
'source_first' => $source,
]);
}
$customer->channelIds()->updateOrCreate(
['channel' => $source],
['external_id' => $customerData['id'] ?? null]
);
return $customer;
}
}
Як ми вирішуємо проблеми синхронізації?
Єдина історія замовлень. Клієнт в особистому кабінеті бачить всі замовлення — з сайту, Ozon, WB, POS — відсортовані за датою. Технічно це означає об'єднання замовлень з різних джерел в одну таблицю з прив'язкою до customer_id. Ми використовуємо патерн Repository для абстракції:
public function getOrderHistory(Customer $customer): Collection
{
return Order::where('customer_id', $customer->id)
->with(['items.product', 'source_details'])
->orderByDesc('created_at')
->get()
->map(fn($order) => [
'id' => $order->id,
'source' => $order->source,
'source_label' => $this->sourceLabel($order->source),
'status' => $order->status,
'total' => $order->total,
'items' => $order->items->count(),
'date' => $order->created_at->format('d.m.Y'),
]);
}
Єдине управління промо. Промокод, створений для сайту, повинен працювати і на маркетплейсі, але тільки якщо канал дозволений.
class OmnichannelPromotion
{
public function apply(string $promoCode, Order $order): void
{
$promo = Promotion::where('code', $promoCode)->first();
if (!in_array($order->source, $promo->applicable_channels)) {
throw new PromoNotApplicableException("Промокод недійсний для {$order->source}");
}
$discount = $promo->calculateDiscount($order->total);
$order->applyDiscount($discount, $promoCode);
$promo->increment('used_count');
}
}
Як ми резервуємо товари за каналами?
Без загальної картини залишків ви або втрачаєте продажі (товар є в системі, але не викладений на маркетплейс), або отримуєте овербукінг (два канали продають одну одиницю).
Реалізуємо це через InventoryManager з відсоткуванням за каналами:
class InventoryManager
{
private array $channelReservePercent = [
'site' => 40,
'ozon' => 30,
'wb' => 20,
'buffer' => 10,
];
public function getChannelAllocation(int $productId): array
{
$total = WarehouseItem::where('product_id', $productId)->sum('quantity');
return array_map(
fn($pct) => (int)floor($total * $pct / 100),
$this->channelReservePercent
);
}
}
Аналітика за каналами
SELECT
source,
COUNT(*) AS orders_count,
SUM(total) AS revenue,
AVG(total) AS aov,
COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY source
ORDER BY revenue DESC;
Порівняння підходів до інтеграції
| Підхід | Час впровадження | Складність | Консистентність |
|---|---|---|---|
| ETL (періодичне завантаження) | 2-4 тижні | Низька | Низька (затримки) |
| API Gateway з синхронними запитами | 4-8 тижнів | Середня | Середня (ризик таймаутів) |
| Event-driven (CDC + черги) | 6-12 тижнів | Висока | Висока (реальний час) |
Ми зазвичай обираємо третій варіант — він дає найменші затримки і масштабується. Event-driven архітектура в 2-3 рази швидша за ETL при синхронізації залишків. Детальніше про CDC — в документації Debezium.
Порівняння стратегій резервування
| Стратегія | Ризик овербукінгу | Гнучкість розподілу | Складність реалізації |
|---|---|---|---|
| Фіксований відсоток | Низький | Низька | Низька |
| Динамічний за продажами | Середній | Висока | Середня |
| Повний сток (пул) | Високий | Середня | Висока |
Процес роботи
- Аналітика — вивчаємо поточну архітектуру, список каналів, обсяги даних, бізнес-правила.
- Проектування — малюємо модель даних, API-специфікації (OpenAPI), схему обміну.
- Реалізація — пишемо Core, інтеграційні адаптери, тести (unit + integration).
- Тестування — наскрізні сценарії: замовлення на сайті → скасування в мобільному додатку → повернення на маркетплейсі.
- Деплой — roll-out за каналами, моніторинг через Grafana та алерти.
Ми гарантуємо консистентність даних у реальному часі. Наші інженери мають 5+ років досвіду в побудові розподілених систем. Для реального проекту — наприклад, для мережі магазинів меблів ми інтегрували 5 каналів, що дозволило скоротити кількість повернень на 25% за рахунок точного відображення залишків.
Що входить в роботу
- Розробка Unified Commerce Core (інвентар, клієнти, замовлення, промо).
- Інтеграція з 3+ каналами (сайт, маркетплейси, мобільний додаток).
- Документація API та схеми даних.
- Навантажувальне тестування та оптимізація.
- Навчання команди замовника.
Терміни
Базова система на 3 канали — від 20 до 30 робочих днів. Для 5+ каналів — до 50 днів. Вартість розраховується індивідуально після аудиту існуючої інфраструктури. Отримайте консультацію для оцінки вашого проекту.
Наш досвід
Ми реалізували 15+ omnichannel-проектів для e-commerce. Серед клієнтів — магазини зі значним оборотом. Інженери мають 5+ років досвіду в побудові розподілених систем. Зв'яжіться з нами — ми допоможемо визначити оптимальну архітектуру для вашого бізнесу.







