Розроблення корзини покупок для інтернет-магазину
Корзина — центральний компонент будь-якого e-commerce проекту. Саме тут відбувається основна частина відмов: незручне управління кількістю, втрата позицій при перезавантаженні сторінки, конфлікт сесій у авторизованих користувачів. Розроблення корзини з нуля займає від 3 до 6 робочих днів залежно від складності бізнес-логіки.
Архітектура збереження корзини
Корзина існує в трьох станах: гостева (анонімна), користувацька (привязана до аккаунту) та об'єднана (merge при логіні). Для гостей дані зберігаються в localStorage чи sessionStorage — вибір залежить від політики сайту. При авторизації клієнт-серверний merge повинен розв'язувати конфлікти: наприклад, якщо один і той же товар є в обох сховищах, підсумувати кількість чи взяти максимальне значення.
На серверній стороні таблиця корзин зазвичай виглядає так:
CREATE TABLE cart_items (
id BIGSERIAL PRIMARY KEY,
cart_id UUID NOT NULL,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
session_id VARCHAR(255),
product_id BIGINT NOT NULL,
variant_id BIGINT,
quantity INT NOT NULL DEFAULT 1,
price_snapshot NUMERIC(12,2) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_cart_items_cart_id ON cart_items(cart_id);
CREATE INDEX idx_cart_items_user_id ON cart_items(user_id);
price_snapshot фіксує ціну у момент додавання — це критично для акцій з таймером та зміни цін в реальному часі.
Логіка оновлення quantity
Змінення кількості повинне бути оптимістичним: UI оновлюється миттєво, запит уходить у фоні. При помилці — відкат з сповіщенням. Реалізація через React Query:
const updateQuantity = useMutation({
mutationFn: ({ itemId, qty }: { itemId: number; qty: number }) =>
api.patch(`/cart/items/${itemId}`, { quantity: qty }),
onMutate: async ({ itemId, qty }) => {
await queryClient.cancelQueries({ queryKey: ['cart'] });
const prev = queryClient.getQueryData(['cart']);
queryClient.setQueryData(['cart'], (old: Cart) => ({
...old,
items: old.items.map(i => i.id === itemId ? { ...i, quantity: qty } : i),
}));
return { prev };
},
onError: (_, __, ctx) => {
queryClient.setQueryData(['cart'], ctx?.prev);
toast.error('Не вдалось оновити кількість');
},
});
Мінімальна та максимальна кількість задається на рівні товару: min_order_qty та max_order_qty. Якщо на складі залишилось 3 штуки, кнопка «+» блокується при досягненні цього значення.
Перевірка наявності та резервування
При додаванні в корзину потрібно вирішити: робити soft reserve (зменшити доступний остаток) чи ні. Soft reserve зменшує конкуренцію за товар, але створює «мертві» резерви від кинутих корзин. Компроміс — резервувати тільки в момент початку оформлення заказу (checkout init), а корзину тримати інформаційною.
При відображенні корзини актуальні остатки перевіряються запитом до складу. Якщо товар закінчився — показуємо попередження прямо у строці позиції, не блокуючи всю корзину.
Розрахунок итоговой суми
Итог корзини включає кілька шарів:
| Компонент | Логіка |
|---|---|
| Subtotal | Сума price_snapshot × quantity по всім позиціям |
| Скидки | Застосовуються за пріоритетом: акційні > купонні > накопицьвні |
| Доставка | Розраховується попередньо, точно — на checkout |
| ПДВ | Включений у ціну чи додається окремо (залежить від конфігурації) |
Розрахунок виконується на сервері при кожній зміні корзини. Клієнт отримує готові суми — жодних обчислень у браузері.
Міні-корзина та повна сторінка
Міні-корзина в хедері (dropdown чи sidebar) показує до 5 останніх позицій, счітчик та кнопку «Оформити». Повна сторінка /cart відображає всі позиції з можливістю редагування. Обидва компоненти підписані на один і той же стан — через React Query чи Zustand.
Важливий момент: счітчик у хедері оновлюється через Server-Sent Events чи polling раз у 30 секунд — це актуально, якщо користувач працює в кількох вкладках одночасно.
Збереження корзини
Корзина гостя зберігається 30 днів у cookie (cart_id). При логіні відбувається merge та корзина переносится в БД. Якщо користувач був авторизований та вийшов — корзина залишається в БД, при повторному логіні відновлюється.
Корзина авторизованого користувача синхронізується між пристроями — це ключова різниця від гостевої.
Аналітика корзини
Всі подієї корзини повинні уходити в аналітику: add_to_cart, remove_from_cart, view_cart. Для Google Analytics 4 це стандартні e-commerce подієї з параметрами item_id, item_name, price, quantity. Для Яндекс.Метрики — аналогічна структура через ym(id, 'reachGoal', 'cart_add', {...}).
Кинуті корзини відслідковуються окремо: якщо користувач додав товари, але не перейшов до checkout за N годин — триггер для email-цепочки.
Типові проблеми реалізації
-
Race condition при одночасному додаванні: вирішується через
SELECT FOR UPDATEна рівні БД чи idempotency key в API - Ціна змінилась після додавання: показувати сповіщення, перераховувати автоматично
- Товар знятий з продажу: блокувати checkout, пропонувати видалити позицію
- ПДВ для різних країн: визначати по IP/адресі доставки, застосовувати відповідну ставку







