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 в месяц.