Разработка системы предзаказов для интернет-магазина
Предзаказ — это обязательство покупателя купить товар до его появления на складе. Технически это пересечение нескольких систем: инвентаря, платежей, коммуникаций и управления ожиданиями. Плохо реализованный предзаказ приводит к конфликтам — покупатель заплатил, а магазин не может выполнить обязательство или не помнит, когда обещал.
Сценарии предзаказа
Предзаказы бывают принципиально разными по бизнес-логике:
Полная оплата сейчас — покупатель платит сразу, товар отгружается когда поступит. Подходит для товаров с известной датой поставки (новые модели электроники, книги).
Частичная предоплата — депозит 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 недели.







