Розробка системи передзаказів для E-Commerce
Предзаказ — це зобов'язання покупця купити товар до його появи на складі. Технічно це перетин кількох систем: інвентарю, платежів, комунікацій та управління очікуванням. Погано реалізований передзаказ приводить до конфліктів — покупатель заплатив, а магазин не може виконати зобов'язання або забуває, коли обіцяв.
Сценарії передзаказу
Передзакази бувають принципово різними за бізнес-логіці:
Повна оплата сейчас — покупатель платить відразу, товар відправляється коли поступить. Підходить для товарів з відомою датою поставки (нові моделі електроніки, книги).
Частична передоплата — депозит 20–50%, залишок — при відправці. Потребує двохетапного платежу. Складніше технічно, знижує барʼєр для покупця.
Без оплати (бронювання місця) — покупатель залишає заявку, гроші списуються при поступленні або менеджер зв'язується вручну. Найслабший варіант за конверсією.
Груповий передзаказ — товар виробляється/замовляється тільки при досягненні мінімальної кількості. Crowdfunding-механіка.
Схема даних
CREATE TABLE preorder_campaigns (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(id),
name VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'active',
-- active | paused | fulfilled | cancelled
payment_mode VARCHAR(50) NOT NULL DEFAULT 'full',
-- full | deposit | free
deposit_percent NUMERIC(5,2),
expected_date DATE,
max_quantity INTEGER, -- NULL = без обмеження
min_quantity INTEGER, -- для групового передзаказу
current_count INTEGER NOT NULL DEFAULT 0,
closes_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE preorders (
id BIGSERIAL PRIMARY KEY,
campaign_id BIGINT NOT NULL REFERENCES preorder_campaigns(id),
user_id BIGINT REFERENCES users(id),
email VARCHAR(255) NOT NULL,
variant_id BIGINT NOT NULL REFERENCES product_variants(id),
qty INTEGER NOT NULL DEFAULT 1,
unit_price NUMERIC(12,2) NOT NULL,
deposit_paid NUMERIC(12,2) NOT NULL DEFAULT 0,
total_paid NUMERIC(12,2) NOT NULL DEFAULT 0,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
-- pending | deposit_paid | fully_paid | fulfilled | cancelled | refunded
expected_date DATE,
notified_at TIMESTAMP,
order_id BIGINT REFERENCES orders(id), -- створюється при виконанні
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
Прием оплаты: двухэтапний сценарій
При режимі deposit — платіж у два кроки:
Крок 1: Депозит при оформленні передзаказу
class PreorderCheckout
{
public function processDeposit(Preorder $preorder, PaymentMethod $method): Payment
{
$depositAmount = $preorder->unit_price * $preorder->qty
* ($preorder->campaign->deposit_percent / 100);
$payment = $this->gateway->charge([
'amount' => $depositAmount,
'currency' => 'RUB',
'description' => "Передзаказ #{$preorder->id}: депозит",
'metadata' => ['preorder_id' => $preorder->id, 'type' => 'deposit'],
]);
$preorder->update([
'deposit_paid' => $depositAmount,
'status' => 'deposit_paid',
]);
return $payment;
}
}
Крок 2: Залишок при поступленні товару
Коли товар поступає на склад, запускається автоматична спроба списання залишку. Якщо карта вже не актуальна — покупателю йде лист з новою ссилкою для оплати.
Обмеження кількості та черга
Для лімітованих передзаказів потрібна атомарна перевірка ліміту:
UPDATE preorder_campaigns
SET current_count = current_count + :qty
WHERE id = :campaign_id
AND (max_quantity IS NULL OR current_count + :qty <= max_quantity)
AND status = 'active'
RETURNING id, current_count;
Якщо рядок не повернувся — ліміт вичерпан. Користувачу пропонується стати у лист ожидання.
Лист ожідання
Окрема таблиця для тих, хто не встиг:
CREATE TABLE waitlist_entries (
id BIGSERIAL PRIMARY KEY,
campaign_id BIGINT NOT NULL REFERENCES preorder_campaigns(id),
email VARCHAR(255) NOT NULL,
variant_id BIGINT REFERENCES product_variants(id),
notified BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (campaign_id, email, variant_id)
);
При звільненні місця (скасування передзаказу) — автоматичне сповіщення першим N у черзі з обмеженою по часу пропозицією.
Виконання передзаказів при поступленні товару
Коли товар приходить на склад, менеджер запускає процес виконання:
class PreorderFulfillmentService
{
public function fulfill(PreorderCampaign $campaign): FulfillmentResult
{
$preorders = $campaign->preorders()
->where('status', 'deposit_paid')
->orWhere('status', 'fully_paid')
->orderBy('created_at') // FIFO
->get();
$fulfilled = 0;
foreach ($preorders as $preorder) {
DB::transaction(function () use ($preorder, &$fulfilled) {
// Перевіряємо наявність остатка
$reserved = $this->stockService->reserve(
$preorder->variant_id,
$preorder->qty
);
if (!$reserved) {
return; // Остатка не хватає, пропускаємо
}
// Створюємо реальний заказ
$order = $this->orderFactory->fromPreorder($preorder);
// Списуємо остаток по доплаті якщо потрібно
if ($preorder->needsBalancePayment()) {
$this->chargeBalance($preorder);
}
$preorder->update(['status' => 'fulfilled', 'order_id' => $order->id]);
$fulfilled++;
PreorderFulfilled::dispatch($preorder, $order);
});
}
return new FulfillmentResult($fulfilled, $preorders->count());
}
}
Комунікації з покупцями
Передзаказ — це обіцянка, та покупатель має постійно розуміти статус:
| Подія | Канал | Вміст |
|---|---|---|
| Оформлення | Підтвердження, деталі, очікувана дата | |
| Зміна дати | Email + SMS | Нова дата, причина затримки |
| Товар поступив | Email + Push | Сповіщення про початок відправки |
| Заказ створений | Номер замовлення, трек | |
| Скасування кампанії | Інструкція по поверненню |
Шаблони сповіщень — окрема сутність, редагована в адмінці. Дата поставки в листах завжди відображається у форматі "орієнтовно: квітень 2025" а не точною датою, якщо впевненості немає.
Відображення на вітрині
Карточка товару у режимі передзаказу:
- Кнопка "Передзаказати" замість "В кошик"
- Очікувана дата поставки під кнопкою
- Лічильник займаних місць (якщо ліміт): "Забронировано 47 з 100"
- Індикатор дефіциту: "Залишилось 12 місць"
- Інформаційний блок про політику передзаказів та повернень
Сроки реалізації
- Базова система: оформлення + повна оплата + сповіщення: 4–6 днів
- Двохетапна оплата (депозит + залишок): +2–3 дня
- Груповий передзаказ з мінімальним порогом: +2 дня
- Лист очидання з автосповіщеннями: +1–2 дня
- Панель управління кампаніями в адмінці: +2–3 дня
Повна система з кількома режимами та аналітикою: 2–3 тижня.







