Реалізація мультиканальних продаж (Omnichannel) на сайті
Omnichannel — це не просто продажі на кількох платформах. Це єдиний досвід клієнта: покупець бачить однакові ціни, історія замовлень доступна в будь-якому каналі, повернення, зроблене на маркетплейсі, можливо через сайт. Технічно — це складна інтеграція кількох систем з єдиним джерелом істини.
Компоненти Omnichannel-системи
┌─────────────────────────────┐
│ Unified Commerce Core │
│ (інвентар, клієнти, заказы) │
└──────────────┬──────────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
┌──────▼──────┐ ┌────────▼───────┐ ┌────────▼────────┐
│ Сайт/App │ │ Маркетплейси │ │ Офлайн / POS │
│ │ │ Ozon WB YM │ │ (якщо є) │
└─────────────┘ └────────────────┘ └─────────────────┘
Єдиний профіль клієнта
class CustomerIdentityResolver
{
// Об'єднуємо клієнтів із різних каналів
public function resolve(array $customerData, string $source): Customer
{
// Шукаємо по email → телефону → імені + адресі
$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;
}
}
Єдина історія замовлень
// Клієнт бачить всі свої замовлення — з сайту та маркетплейсів — у особистому кабінеті
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 InventoryManager
{
// Резерви по каналах дозволяють керувати розподілом запасів
private array $channelReservePercent = [
'site' => 40, // 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
);
}
}
Єдине управління промо
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');
}
}
Аналітика по каналах
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;
Строки
Базова Omnichannel-система (3 канали, єдиний інвентар, єдина історія замовлень): 20–30 робочих днів.







