Freedom Marine · CPQ
952 правила, которые никто никогда не записывал.
Мультибрендовая платформа конфигурации и расчёта катеров. Отдел продаж собирает катер, считает цену в CAD и отправляет предложение, и каждый шаг фиксируется в CRM. Она заменила расчёты в таблицах, превратив опции, правила зависимостей и логику цен каждого производителя в данные.
- Роль
- Product lead и разработка
- Стек
- Next.js 16 · React 19 · TypeScript strict · Supabase Postgres · Vercel
Правила существовали. Их просто никогда не записывали как правила.
Список опций производителя выглядит как каталог. На деле это свод правил в маскировке: сноска о том, что комплект подушек подходит лишь к двум из пяти версий дивана; цветовая маркировка, означающая, что две компоновки палубы несовместимы; «см. примечание 4», за которым стоит обязательная фурнитура. Продавцы держали эти правила в голове и в таблицах, и расчёты уходили с ошибками.
Значит, работа была не в том, чтобы построить форму. Она была в том, чтобы превратить неявное знание в данные, которые система может применять на каждом клике, — и сделать это для брендов, чья коммерческая логика не имеет ничего общего.
Что внутри системы
Одно приложение, одна база, ничего не зашито в код.
Каталог, правила, цены и порядок шагов живут в Postgres, а не в коде, поэтому новый модельный год — это изменение данных, а новый бренд — настройка, а не релиз.
| Слой | Что делает |
|---|---|
| Фронтенд | Конфигуратор, панель продаж и админ-панель. 14 основных шагов, порядок и видимость настраиваются по бренду. |
| Сервер | Предложения, серверный рендер PDF, синхронизация с CRM, плановые изменения цен, жизненный цикл лида, три cron-задачи. |
| База | Каталог, цены, правила, поток шагов, предложения и журнал аудита, с row-level security и ролями. |
| Ценообразование | Один чистый модуль со всеми формулами и подключаемая схема для каждой модели. |
| Интеграции | CRM и транзакционная почта, изолированные за собственным слоем, с идемпотентной записью. |
Шесть раз, когда очевидный ответ был неверным.
Интересное в этой работе — не список функций. Интересно то, где первый подход не сработал и что пришло ему на смену.
- 01
Четыре подхода с ИИ не смогли прочитать списки производителя
Ограничение · Правила закодированы в сносках, цветах и перекрёстных ссылках
Я пробовал разобрать списки автоматически четырьмя разными способами. Ни один не был достаточно надёжным, чтобы доверить ему цену. Списки неявны по своей природе — они рассчитаны на человека, знающего продукт. Поэтому я перестал автоматизировать чтение и перенёс усилие туда, где оно держалось: типизированная модель правил в базе, генераторы, превращающие проверенный список в правила, и переопределения по модели поверх глобальных. Интерпретация осталась человеческой; всё после неё стало данными.
- 02
Два бренда, чьё ценообразование не имеет ничего общего
Ограничение · Один движок, две несвязанные коммерческие логики
Один бренд считает цену от европейского прайса через настраиваемые шаги формулы — маржа, наценка, пошлины, фрахт, локальные нормы. Другой собирает себестоимость от розничной цены корпуса, с переносом и форвардной покупкой, дельтой версии, фиксированными затратами и MSRP двигателя. Вместо ветвления кода ценообразование стало схемой, привязанной к модели, со всеми формулами в одном чистом модуле. База отвергает поля цен одного бренда на модели другого, чтобы их нельзя было смешать по ошибке.
- 03
Опции, которые появлялись и исчезали сами
Ограничение · Каскадные зависимости, непредсказуемые для продавца
Правильно применять правила недостаточно, если экран меняется под руками человека. Каждый клик проходит через планировщик, который сначала вычисляет все последствия и показывает их: что добавится, что уйдёт, почему и какой будет новая сумма. Там, где правило допускает несколько вариантов, выбирает продавец, и кнопка подтверждения остаётся неактивной, пока он не выберет. Каскады считаются относительно конечного состояния, поэтому замена одной версии на другую сохраняет то, что требуется обеим. Отмена возвращает всё к состоянию до клика.
- 04
Нет курса — нет цены, а не догадка
Ограничение · Единый источник истины по курсу валют
Действующий курс задаёт администратор, и живёт он в одной таблице. Если активного курса нет, конфигуратор отказывается считать цену, а не откатывается к устаревшему числу. Тихо неверная цена хуже, чем её отсутствие, а отделу продаж нужно было доверять цифре без перепроверки.
- 05
Модель CRM менялась трижды за три месяца
Ограничение · Движущаяся цель интеграции
Схема прошла путь от одного лида на предложение к сделкам, затем к лидам с конфигуратором как источником истины и наконец к одному лиду на контакт. Поскольку интеграция с самого начала строилась как изолированный слой, каждое изменение оставалось внутри него и не доходило до конфигуратора. Отправки несут ключ идемпотентности, поэтому повтор никогда не создаёт дубль лида или письма, а внешние записи заблокированы вне продакшена.
- 06
Цены меняются, пока предложения открыты
Ограничение · Живая система без отдельной staging-базы
Администратор планирует изменение цены с датой вступления в силу. Триггеры базы помечают затронутые открытые предложения недействительными, PDF перегенерируется с водяным знаком о старой цене, а отправка и редактирование для них отключаются. Более широкое ограничение — менять живую систему без staging-копии — закрывается feature-флагами по брендам, миграциями с контрольной суммой и резервной копией плюс скриптом отката для любого изменения боевых данных.
Большую часть кода написал ИИ. Работой было суждение.
Около 70 тысяч строк TypeScript и примерно 630 часов разработки за одиннадцать календарных месяцев — порядка шестнадцати рабочих недель одного разработчика на полной занятости. Важна именно эта пропорция: код генерируется и проверяется с ИИ под человеческим руководством, и это сделало такой объём достижимым в таком бюджете.
Не делегируется знание о том, где модель уверенно ошибётся. Ценообразование — самый ясный случай: формулы живут в одном чистом модуле, а 10 660 эталонных случаев сверяют каждый результат с сохранёнными снимками, включая набор, построенный так, чтобы строка в строку воспроизвести собственный калькулятор цен клиента. Сквозной тест переигрывает все сочетания карточки и клика для одной модели, чтобы обещание карточки в точности совпадало с действием клика.
Скорость удерживается контролем релиза, а не процессом. Каждое изменение — это pull request со своим preview-деплоем. Изменения данных идут через нумерованные миграции с контрольной суммой. Новые бренды выходят за feature-флагами.
Часть, которой я недоволен
Главный компонент конфигуратора вырос примерно до 8500 строк. Он работает и покрыт тестами, но замедляет любого, кто впервые читает код, и этот долг создал я сам, двигаясь быстро в компоненте, который вбирал шаг за шагом. Его разбивают постепенно, по мере работы с каждым шагом, а не рискованным переписыванием разом.
Есть продукт с такими же правилами?
Конфигурация, цена и коммерческое предложение там, где логика реальна, но ещё никем не записана.