Представьте: вы добавили товар в избранное, но цена кусается. Через неделю — скидка 20%, а вы узнали об этом случайно. Это потерянная покупка. Мы решаем эту проблему: автоматическое уведомление возвращает «неопределившихся» покупателей в воронку. Наш стек: Laravel 11 (PHP 8.3) для бэкенда, React 18 на фронтенде, PostgreSQL для хранения подписок. Разработка базовой логики занимает 1–2 рабочих дня, при условии что wishlist уже готов. По нашей статистике, уведомление о снижении цены увеличивает конверсию из отложенных покупок на 15–25%, а каждый пятый подписчик совершает покупку, принося в среднем 2000–5000 рублей выручки.
Какие проблемы решает уведомление о снижении цены?
Проблема 1: пользователь уходит, не купив. По нашим данным, 60–70% посетителей покидают карточку товара без покупки. Уведомление о снижении цены — мягкий триггер возврата. Проблема 2: высокая нагрузка при массовой рассылке. Без очередей и chunkById при 10 000 подписчиков на один товар сервер ляжет. Мы используем Laravel Horizon с Redis — проверено на 50+ проектах. Проблема 3: повторные уведомления не бесконечны. Мы внедряем стратегию: уведомляем только при снижении ещё на 10% относительно предыдущей цены уведомления.
Как мы это делаем?
Схема данных
Подписки хранятся в отдельной таблице с уникальным индексом на пару (user_id, product_id, variant_id). Это предотвращает дубли. Поле price_at_subscription фиксирует цену в момент подписки — уведомление срабатывает при снижении относительно неё, а не последней цены.
CREATE TABLE price_drop_subscriptions (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE,
variant_id BIGINT REFERENCES product_variants(id) ON DELETE CASCADE,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
price_at_subscription NUMERIC(12,2) NOT NULL,
target_price NUMERIC(12,2),
notified_at TIMESTAMP,
notified_price NUMERIC(12,2),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_pds_user_product
ON price_drop_subscriptions(user_id, product_id, COALESCE(variant_id, 0));
Как работает подписка: пошагово
- На карточке товара пользователь видит кнопку «Следить за ценой». Если не авторизован — происходит редирект на логин с возвратом на страницу товара.
- После авторизации нажатие кнопки отправляет POST-запрос к API, который создаёт запись в таблице
price_drop_subscriptionsс текущей ценой товара и email пользователя. - При изменении цены товара (через админку или синхронизацию) срабатывает Observer в Laravel. Если новая цена ниже старой, диспатчится Job
CheckPriceDropSubscriptionsв очередьnotifications. - Job обрабатывает подписки чанками по 100 записей. Для каждой проверяется процент снижения (порог 5%). Если условие выполняется, пользователю отправляется email с уведомлением.
- После отправки подписка помечается как использованная (заполняются поля
notified_atиnotified_price), чтобы избежать повторных уведомлений при той же цене.
Такой подход гарантирует, что сервер не перегружается при массовой рассылке, а пользователи получают актуальные уведомления.
Кнопка подписки на карточке товара
Кнопка «Следить за ценой» показывается рядом с «В корзину». Для неавторизованных — редирект на логин с возвратом. Используем useState + api (Axios) — без сторонних библиотек.
const PriceDropButton = ({ product, variant }: PriceDropProps) => {
const { user } = useAuth();
const [subscribed, setSubscribed] = useState(product.is_price_drop_subscribed ?? false);
const toggle = async () => {
if (!user) {
router.push(`/login?redirect=${encodeURIComponent(window.location.pathname)}`);
return;
}
if (subscribed) {
await api.delete(`/price-drop-subscriptions/${product.id}`);
setSubscribed(false);
toast.success('Подписка отменена');
} else {
await api.post('/price-drop-subscriptions', {
product_id: product.id,
variant_id: variant?.id ?? null,
});
setSubscribed(true);
toast.success('Уведомим при снижении цены');
}
};
return (
<button
onClick={toggle}
className={cn('flex items-center gap-1.5 text-sm', {
'text-blue-600': subscribed,
'text-gray-500 hover:text-gray-700': !subscribed,
})}
>
<BellIcon className={cn('w-4 h-4', { 'fill-blue-600': subscribed })} />
{subscribed ? 'Слежу за ценой' : 'Следить за ценой'}
</button>
);
};
API и Job отправки (объединённый код)
При изменении цены срабатывает Observer, который диспатчит Job. В Job используется chunkById для обработки больших объёмов без перегрузки памяти. Порог снижения настраивается, а также учитывается target_price пользователя, если он указан.
class CheckPriceDropSubscriptions implements ShouldQueue
{
public function __construct(
private Product $product,
private float $newPrice,
) {}
public function handle(): void
{
$threshold = 0.05;
PriceDropSubscription::where('product_id', $this->product->id)
->whereNull('notified_at')
->chunkById(100, function ($subscriptions) use ($threshold) {
foreach ($subscriptions as $sub) {
$dropPercent = ($sub->price_at_subscription - $this->newPrice) / $sub->price_at_subscription;
if ($dropPercent < $threshold) continue;
if ($sub->target_price && $this->newPrice > $sub->target_price) continue;
Mail::to($sub->email)->queue(new PriceDropNotification($sub, $this->product, $this->newPrice));
$sub->update([
'notified_at' => now(),
'notified_price' => $this->newPrice,
]);
}
});
}
}
Как настроить повторные уведомления?
После первого уведомления подписка помечается как использованная. Чтобы отправлять повторные при дальнейшем снижении, добавьте условие: падение ещё на 10% от notified_price. Реализуется легко — расширяем CheckPriceDropSubscriptions, проверяем notified_price и сравниваем. Это повышает ценность уведомления: пользователь не получает спам, но остаётся в курсе выгодных скидок.
Что входит в настройку уведомлений?
- Разработка схемы БД и миграций
- REST API для подписки/отписки
- Фронтенд-компонент кнопки и списка отслеживания
- Job для проверки цен и отправки писем
- Настройка очередей (Redis + Horizon)
- Шаблон письма с UTM и отпиской
- Интеграция с существующим wishlist (если есть)
- Документация API и инструкция по развёртыванию
Сроки ориентировочно
| Этап | Длительность |
|---|---|
| Аналитика и проектирование | 0.5–1 день |
| Разработка схемы и API | 0.5–1 день |
| Фронтенд и интеграция | 0.5–1 день |
| Тестирование и деплой | 0.5–1 день |
| Итого | 2–4 дня |
Стоимость рассчитывается индивидуально — свяжитесь с нами для оценки вашего проекта.
Сравнение стратегий уведомлений
Выбор стратегии уведомлений зависит от бизнес-задач. Одноразовое уведомление — простейший вариант: клиент получает письмо при первом снижении цены ниже порога. Это легко реализовать, но пользователь рискует пропустить более глубокую скидку. Повторное уведомление уведомляет при каждом новом снижении на 10% от предыдущей цены уведомления — такая логика сложнее, но удерживает интерес без спама. Если же клиент сам указывает желаемую цену, уведомление приходит только при её достижении: максимальная релевантность, но конверсия может быть ниже из-за редких срабатываний.
Список отслеживания в личном кабинете
Раздел /account/price-tracking показывает все подписки пользователя: товар, цена при подписке, текущая цена, изменение в процентах. Компонент использует таблицу с кнопкой отписки.
const PriceTrackingList = ({ subscriptions }: { subscriptions: PriceDropSubscription[] }) => (
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2">Товар</th>
<th>Цена при подписке</th>
<th>Текущая цена</th>
<th>Изменение</th>
<th></th>
</tr>
</thead>
<tbody>
{subscriptions.map(sub => {
const change = ((sub.current_price - sub.price_at_subscription) / sub.price_at_subscription) * 100;
return (
<tr key={sub.id} className="border-b">
<td className="py-3">
<a href={`/products/${sub.product.slug}`} className="font-medium hover:underline">
{sub.product.name}
</a>
</td>
<td className="text-center">{formatPrice(sub.price_at_subscription)}</td>
<td className="text-center font-medium">{formatPrice(sub.current_price)}</td>
<td className={cn('text-center text-sm', change < 0 ? 'text-green-600' : 'text-gray-400')}>
{change < 0 ? `−${Math.abs(change).toFixed(1)}%` : `+${change.toFixed(1)}%`}
</td>
<td>
<button onClick={() => unsubscribe(sub.id)} className="text-gray-400 hover:text-red-500">
Отписаться
</button>
</td>
</tr>
);
})}
</tbody>
</table>
);
Как уведомление влияет на конверсию?
Уведомление о снижении цены увеличивает конверсию из отложенных покупок на 15–25% (по данным наших проектов). Пользователь получает персонализированное предложение — это лучше массовой рассылки. Каждый пятый подписчик совершает покупку, принося магазину в среднем 2000–5000 рублей выручки.
Готовы внедрить? Свяжитесь с нами, и мы реализуем уведомление под ключ. Обсудим детали вашего проекта и предложим оптимальное решение.







