Владелец спит. Магазин принимает заказ.
Euphoria — семейный магазин арабской парфюмерии на Next.js, который я собрал и технически поддерживаю. Сайт и Telegram Mini App работают из единой базы. Вместо списка технологий здесь одна ночь из жизни магазина: путь настоящего заказа через код, который сейчас в проде.
в кадре: Veneno Bianco · French Avenue · 100 мл · 2650 ₴ · реальный товар из каталога
Сцены смонтированы для рассказа. Код настоящий: каждый шаг подписан функцией из продакшена.
Из инсты на витрину
Покупатель пришёл по ссылке в шапке профиля. Продавец в этот момент не нужен: карточка отвечает на вопросы сама.
Пирамида нот от верхних до базы, объём, цена и живой остаток из базы данных. Всё, что в директе спрашивают по десять раз за вечер, здесь уже написано.
Ждать тоже никого не заставили: 98/100 PageSpeed на десктопе и 100 во всех четырёх категориях Lighthouse.
111 ароматов рендерятся из одной базы: и сайт, и витрина в Telegram.
{
"name": "Veneno Bianco",
"brand": "French Avenue",
"price_uah": 2650,
"volume_ml": 100,
"in_stock": true,
"notes": {
"top": "neroli, bergamot",
"heart": "tiare, ylang-ylang",
"base": "vanilla, coconut, gaiac"
}
}
Карточка не свёрстана руками: она отрендерена из этой записи.
Корзина без лишних вопросов
Гостевой заказ без регистрации: имя, телефон, город, отделение. Оплата картой или наложенным платежом.
Невидимая часть важнее видимой: сервер не доверяет браузеру. Сумма пересчитывается по ценам из базы, а промокод захватывается атомарно: применить одну скидку дважды не получится даже нарочно.
Онлайн-эквайринга здесь осознанно нет: для магазина, который продаёт из инсты, перевод на карту и наложка покрывают всех покупателей, а комиссия платёжки резала бы каждый чек. Ветка под эквайринг в коде при этом уже написана: обработчик заказа знает статус „online“ и ждёт подтверждение оплаты от платёжки. Подключение: вопрос ключей и договора, не переделки магазина.
Депозит с секретом
Для наложки магазин просит депозит: 200 гривен и копейки. Копейки от 01 до 99 не случайны: свободные выбираются так, чтобы сумма в банковской выписке однозначно называла заказ. Выписка сверяет платежи сама, владелец ничего не сличает руками.
// kopiykas 01-99 are the order's code: // the amount in the bank statement // identifies the order const freeKopecks: number[] = []; for (let k = 1; k <= 99; k++) { if (!takenKopecks.has(k)) freeKopecks.push(k); } depositAmount = freeKopecks.length > 0 ? (20000 + freeKopecks[Math.floor( Math.random() * freeKopecks.length)]) / 100 : 200;
Реальный фрагмент из обработчика заказа.
Оплата прошла. Дальше работает код
Пока покупателю грузится страница „Дякуємо“, магазин делает семь дел подряд. Порядок и имена функций: ровно как в коде.
Заказ записан в базу
Статус pending, за заказом закреплён депозит с его уникальными копейками.
orders.insert() → #EU-3B7F19C2Позиции зафиксированы
По ценам, которые проверил сервер, а не прислал браузер.
order_items.insert()Остаток списан
Этого флакона на витрине стало на один меньше: сразу и на сайте, и в Telegram.
rpc("decrement_stock")Владельцу уходит Telegram-сообщение
Если Telegram не ответил: одна повторная попытка через 2 секунды.
sendTelegramToOwner()Клиенту уходит письмо-подтверждение
Не доставилось: запись в failed_emails, cron дошлёт позже.
sendOrderConfirmationEmail()Meta получает Purchase с сервера
event_id равен номеру заказа: браузерный дубль Meta склеит сама.
after(sendMetaPurchaseEvent)Покупателю открывается страница подтверждения
К этому моменту всё выше уже сделано.
201 → { order_id }
Почему шаг 6 непростой: баг, который я поймал в проде
Симптом
В Ads Manager покупки задваивались, а часть Purchase не долетала вовсе. Реклама оптимизировалась по мусорным данным.
Диагноз
Serverless замораживает функцию сразу после ответа. Запрос к Conversions API уходил без ожидания и не успевал улететь. А браузер и сервер слали одно событие без общего event_id.
Фикс
after() из next/server: событие уходит после ответа, но до заморозки функции. Один event_id на браузер и сервер, Meta склеивает дубль сама.
Проверка
Test Events в Events Manager: один заказ = один Purchase. Серверные события доезжают, задвоение исчезло.
- // fire and forget: dropped on freeze- void sendMetaPurchaseEvent({ orderId: order.id })+ after(async () => {+ await sendMetaPurchaseEvent({+ orderId: order.id, // = event_id+ value: verifiedTotal,+ })+ })
Утро владельца
Пока страница „Дякуємо“ дорисовывалась, у владельца в Telegram уже лежало вот это.
Формат сообщения: дословно из кода. Имя и телефон вымышлены.
Депозит уже на карте, копейки в выписке совпали с заказом. Дальше две кнопки прямо под сообщением: подтвердить депозит и отметить отправку. В админку можно не заходить.
Наличие тоже живёт в этом чате: бот перечисляет товары кнопками, одно нажатие, и аромат помечен как распроданный. Витрина обновится сама.
Telegram-вебхук пишет в ту же базу, из которой рендерится сайт.
Из чего сделана эта ночь
Машина
- КаркасNext.js 15, App Router
- ТипизацияTypeScript, strict
- БазаSupabase, 11 таблиц
- Продажисайт + Telegram Mini App
- ОплатаMonobank + наложенный платёж
- ДоставкаNova Poshta
- ХостингVercel
Метод
Магазин собран в паре с ИИ-агентом. Проект описан в CLAUDE.md на 350 строк: карта базы, роуты и правила, агент читает её перед каждой сессией.
332 автотеста держат поведение. Агент может смело менять код: если что-то сломалось, я узнаю об этом раньше покупателя.
Это семейный магазин, который я собрал и технически поддерживаю. Кейс без прикрас: что в проде, то и на странице.
Хотите, чтобы ваш магазин тоже работал ночью?
Соберу такой же механизм под ваш товар: витрина, корзина, оплата, доставка, Telegram. И покажу процесс так же честно, на живом примере.