Уявіть: ви додали товар до обраного, але ціна кусається. Через тиждень — знижка 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 гривень виручки.
Готові впровадити? Зв'яжіться з нами, і ми реалізуємо сповіщення під ключ. Обговоримо деталі вашого проєкту та запропонуємо оптимальне рішення.







