← Проекты

Freedom Marine · CPQ

952 правила, которые никто никогда не записывал.

Мультибрендовая платформа конфигурации и расчёта катеров. Отдел продаж собирает катер, считает цену в CAD и отправляет предложение, и каждый шаг фиксируется в CRM. Она заменила расчёты в таблицах, превратив опции, правила зависимостей и логику цен каждого производителя в данные.

Роль
Product lead и разработка
Стек
Next.js 16 · React 19 · TypeScript strict · Supabase Postgres · Vercel
Задача

Правила существовали. Их просто никогда не записывали как правила.

Список опций производителя выглядит как каталог. На деле это свод правил в маскировке: сноска о том, что комплект подушек подходит лишь к двум из пяти версий дивана; цветовая маркировка, означающая, что две компоновки палубы несовместимы; «см. примечание 4», за которым стоит обязательная фурнитура. Продавцы держали эти правила в голове и в таблицах, и расчёты уходили с ошибками.

Значит, работа была не в том, чтобы построить форму. Она была в том, чтобы превратить неявное знание в данные, которые система может применять на каждом клике, — и сделать это для брендов, чья коммерческая логика не имеет ничего общего.

Что внутри системы

1 016Позиций оборудования, 3933 строк цен
952Правил зависимостей, только Axopar
10 660Эталонных тестов ценообразования
220Миграций базы с контрольной суммой
Архитектура

Одно приложение, одна база, ничего не зашито в код.

Каталог, правила, цены и порядок шагов живут в Postgres, а не в коде, поэтому новый модельный год — это изменение данных, а новый бренд — настройка, а не релиз.

СлойЧто делает
ФронтендКонфигуратор, панель продаж и админ-панель. 14 основных шагов, порядок и видимость настраиваются по бренду.
СерверПредложения, серверный рендер PDF, синхронизация с CRM, плановые изменения цен, жизненный цикл лида, три cron-задачи.
БазаКаталог, цены, правила, поток шагов, предложения и журнал аудита, с row-level security и ролями.
ЦенообразованиеОдин чистый модуль со всеми формулами и подключаемая схема для каждой модели.
ИнтеграцииCRM и транзакционная почта, изолированные за собственным слоем, с идемпотентной записью.
Решения в условиях ограничений

Шесть раз, когда очевидный ответ был неверным.

Интересное в этой работе — не список функций. Интересно то, где первый подход не сработал и что пришло ему на смену.

  1. 01

    Четыре подхода с ИИ не смогли прочитать списки производителя

    Ограничение · Правила закодированы в сносках, цветах и перекрёстных ссылках

    Я пробовал разобрать списки автоматически четырьмя разными способами. Ни один не был достаточно надёжным, чтобы доверить ему цену. Списки неявны по своей природе — они рассчитаны на человека, знающего продукт. Поэтому я перестал автоматизировать чтение и перенёс усилие туда, где оно держалось: типизированная модель правил в базе, генераторы, превращающие проверенный список в правила, и переопределения по модели поверх глобальных. Интерпретация осталась человеческой; всё после неё стало данными.

  2. 02

    Два бренда, чьё ценообразование не имеет ничего общего

    Ограничение · Один движок, две несвязанные коммерческие логики

    Один бренд считает цену от европейского прайса через настраиваемые шаги формулы — маржа, наценка, пошлины, фрахт, локальные нормы. Другой собирает себестоимость от розничной цены корпуса, с переносом и форвардной покупкой, дельтой версии, фиксированными затратами и MSRP двигателя. Вместо ветвления кода ценообразование стало схемой, привязанной к модели, со всеми формулами в одном чистом модуле. База отвергает поля цен одного бренда на модели другого, чтобы их нельзя было смешать по ошибке.

  3. 03

    Опции, которые появлялись и исчезали сами

    Ограничение · Каскадные зависимости, непредсказуемые для продавца

    Правильно применять правила недостаточно, если экран меняется под руками человека. Каждый клик проходит через планировщик, который сначала вычисляет все последствия и показывает их: что добавится, что уйдёт, почему и какой будет новая сумма. Там, где правило допускает несколько вариантов, выбирает продавец, и кнопка подтверждения остаётся неактивной, пока он не выберет. Каскады считаются относительно конечного состояния, поэтому замена одной версии на другую сохраняет то, что требуется обеим. Отмена возвращает всё к состоянию до клика.

  4. 04

    Нет курса — нет цены, а не догадка

    Ограничение · Единый источник истины по курсу валют

    Действующий курс задаёт администратор, и живёт он в одной таблице. Если активного курса нет, конфигуратор отказывается считать цену, а не откатывается к устаревшему числу. Тихо неверная цена хуже, чем её отсутствие, а отделу продаж нужно было доверять цифре без перепроверки.

  5. 05

    Модель CRM менялась трижды за три месяца

    Ограничение · Движущаяся цель интеграции

    Схема прошла путь от одного лида на предложение к сделкам, затем к лидам с конфигуратором как источником истины и наконец к одному лиду на контакт. Поскольку интеграция с самого начала строилась как изолированный слой, каждое изменение оставалось внутри него и не доходило до конфигуратора. Отправки несут ключ идемпотентности, поэтому повтор никогда не создаёт дубль лида или письма, а внешние записи заблокированы вне продакшена.

  6. 06

    Цены меняются, пока предложения открыты

    Ограничение · Живая система без отдельной staging-базы

    Администратор планирует изменение цены с датой вступления в силу. Триггеры базы помечают затронутые открытые предложения недействительными, PDF перегенерируется с водяным знаком о старой цене, а отправка и редактирование для них отключаются. Более широкое ограничение — менять живую систему без staging-копии — закрывается feature-флагами по брендам, миграциями с контрольной суммой и резервной копией плюс скриптом отката для любого изменения боевых данных.

Как это построено

Большую часть кода написал ИИ. Работой было суждение.

Около 70 тысяч строк TypeScript и примерно 630 часов разработки за одиннадцать календарных месяцев — порядка шестнадцати рабочих недель одного разработчика на полной занятости. Важна именно эта пропорция: код генерируется и проверяется с ИИ под человеческим руководством, и это сделало такой объём достижимым в таком бюджете.

Не делегируется знание о том, где модель уверенно ошибётся. Ценообразование — самый ясный случай: формулы живут в одном чистом модуле, а 10 660 эталонных случаев сверяют каждый результат с сохранёнными снимками, включая набор, построенный так, чтобы строка в строку воспроизвести собственный калькулятор цен клиента. Сквозной тест переигрывает все сочетания карточки и клика для одной модели, чтобы обещание карточки в точности совпадало с действием клика.

Скорость удерживается контролем релиза, а не процессом. Каждое изменение — это pull request со своим preview-деплоем. Изменения данных идут через нумерованные миграции с контрольной суммой. Новые бренды выходят за feature-флагами.

Что бы я сделал иначе

Часть, которой я недоволен

Главный компонент конфигуратора вырос примерно до 8500 строк. Он работает и покрыт тестами, но замедляет любого, кто впервые читает код, и этот долг создал я сам, двигаясь быстро в компоненте, который вбирал шаг за шагом. Его разбивают постепенно, по мере работы с каждым шагом, а не рискованным переписыванием разом.

Есть продукт с такими же правилами?

Конфигурация, цена и коммерческое предложение там, где логика реальна, но ещё никем не записана.