чат с агентомспроситьПроекты2 папки 4 файлаAI и агенты2 папки 2 файлаИнфраструктура3 папкиРазработка5 файловОбо мне5 файлов
Caerus - прокат автомобилей в Черногории, от отделения до продвижения
  • Next.js
  • платежи
  • SEO

Caerus - прокат автомобилей в Черногории, от отделения до продвижения

С 2024 года сайт проката работал на движке агрегатора GlobalRental: на проде оба продукта крутили один бинарник на одной базе и отличались только переменными окружения, разделения данных в схеме не было. GlobalRental закрыли, а прокат на этом движке продолжал принимать брони. Сайт нужно было отделить и не уронить по дороге работающий бизнес.

Разделение

Начал с аудита прода: карта процессов, разница конфигураций, что чем управляется. Снял снимок как точку отката - и только потом разделял. В такой конструкции опасны не столько данные, сколько неявные связи: общий процесс, общие переменные окружения, общий деплой. Пока они не выписаны на бумагу, любое разделение делается вслепую.

Дальше вел продукт один: все 92 коммита этого этапа мои, сейчас это багфиксы и поддержка. Стек - Next.js 14 с React 18 и TypeScript, Express с Prisma поверх PostgreSQL, три языка через префикс в адресе, JWT в куках, деплой через GitHub Actions на PM2.

Что делает продукт

Каталог с фильтрами и расчётом цены, бронирование с проверкой доступности, кабинет партнёра, админка. Договоры собираются в PDF, письма уходят через отдельный сервис. Онлайн-оплату из бронирования убрали по решению владельца. Интеграция Stripe осталась в коде, ее проверяли боевым платежом еще на GlobalRental.

Отдельная часть работы - органика: адреса под людей и поисковые системы, страницы под запросы вроде «car rental in Montenegro», разметка и метатеги, редактируемые из админки без деплоя.

Три бага, по которым видно продакшн

Поисковый робот уронил каталог. TypeError: e.data.map is not a function приходил только с Baiduspider. Корень был не в парсере: перехватчик axios возвращал error.response вместо того, чтобы бросить исключение, поэтому при ошибке в data оказывалось что угодно, и следующий .map() падал. Починил в двух местах сразу - защита на каждом из трёх параллельных запросов и проверка типа перед обходом, - потому что этот антипаттерн задевает всех потребителей интерсептора, а не одну страницу.

Бесконечный цикл в фильтрах на мобильном. RangeError при выборе автоматической коробки в каталоге Будвы, воспроизводился на iPhone. Каскад: setValue внутри эффекта дёргал наблюдателя формы, тот вызывал отправку, отправка снова писала значения. Лечится не переписыванием формы, а защитой от повторного входа и флагом инициализации со сбросом таймера в очистке эффекта.

Ваучер с чужим временем. Клиент бронирует на 10:00, в PDF и письме приходит 08:00. Прод-процесс живёт в UTC, а PDF и письма рендерятся на бэкенде и, в отличие от остальных путей, не переводили дату в таймзону города. Добавил конвертацию и тест на переход летнего времени, применил в ваучере, письмах подтверждения и отмены. Фронт при этом не трогал намеренно: он уже показывает верное время через сдвиг на бэкенде, и «исправление» там дало бы двойную конвертацию.

Что вышло

Кабинет стал рабочим календарем проката: около 1400 записей за два года. Прямых броней с сайта становится больше: 13 за 2025 год, 31 за 2026-й, в сезон до 10 в месяц.