B2B-кабинет часто начинают проектировать с привычной фразы: «клиент заходит в личный кабинет и оформляет заказ». Проблема в том, что в B2B слово «клиент» почти всегда означает сразу несколько разных объектов.
Есть человек с логином. Есть организация, от имени которой он работает. Есть юридическое лицо с реквизитами. Есть договор с условиями оплаты. Есть плательщик, на которого выставляют счёт. В простом проекте всё это может совпадать, но в зрелом B2B-сценарии эти роли быстро расходятся.
Если смешать их в одну таблицу, кабинет некоторое время будет работать. А потом появится второй сотрудник, несколько юрлиц у одного клиента, счёт на головную компанию, разные договоры, частичная отгрузка, интеграция с 1С — и модель данных начнёт мешать бизнесу.
Главный принцип: аккаунт, заказчик, юрлицо и плательщик — это разные сущности. Их можно осознанно схлопнуть в простом MVP, но нельзя проектировать так, будто различий не существует.
Пять слоёв B2B-клиента
Главная причина путаницы — слово «клиент». В B2B оно может описывать минимум пять уровней.
| Слой | Что это | За что отвечает |
|---|---|---|
| Аккаунт | Учётная запись человека: телефон, email, пароль, ФИО | Авторизация и персональная идентификация |
| Членство | Связь «человек работает от имени заказчика» | Роли, права, доступ к юрлицам и адресам |
| Заказчик | Бизнес-субъект: компания, дилер, лаборатория, клиника, филиальная группа | Коммерческие условия, сегмент, менеджер, история отношений |
| Юрлицо | ООО, ИП или другое лицо с реквизитами | Договоры, счета, закрывающие документы |
| Плательщик | Юрлицо, на которое выставлен счёт по конкретному заказу | Финансовая ответственность в заказе |
В простейшем варианте один пользователь привязан к одному партнёру, партнёр одновременно является юрлицом, а плательщик всегда совпадает с ним. Это нормальная модель для старта, если бизнес действительно так работает.
Но как только появляются сотрудники, филиалы, разные договоры или оплата через управляющую компанию, прямой user.customer_id превращается в ограничение. Правильнее мыслить через связь account ↔ customer, то есть через членство.
Базовая схема сущностей
Универсальная модель B2B-кабинета строится вокруг заказа. Заказ фиксирует, кто оформил действие, от имени какого заказчика оно выполнено, какое юрлицо участвует, по какому договору и кто платит.
Минимальный набор сущностей:
| Сущность | Назначение |
|---|---|
Account |
Человек и его учётная запись |
Membership |
Роль человека у конкретного заказчика |
Customer |
Заказчик как коммерческая единица |
LegalEntity |
Юрлицо с реквизитами |
Contract |
Договор и условия оплаты |
DeliveryAddress |
Адрес доставки и контакт на точке |
Order |
Заказ, его состав, статусы и снапшоты |
Invoice |
Счёт на оплату |
Payment |
Поступившие оплаты |
Document |
УПД, накладные, акты, счета-фактуры |
PriceType |
Тип цены и правила расчёта |
В реальном проекте часть сущностей может быть не нужна. Например, если договоры не показываются в кабинете, их можно не заводить как отдельный объект. Но решение должно быть принято после бизнес-вопросов, а не по умолчанию.
Где должны жить роли
Роль сотрудника нельзя надёжно хранить только в аккаунте. Один и тот же человек может быть администратором в одной организации и наблюдателем в другой. Поэтому роль живёт на связи между аккаунтом и заказчиком.
Типовая матрица ролей:
| Роль | Что может делать |
|---|---|
| Владелец или админ | Управляет сотрудниками, ролями, доступами, настройками |
| Закупщик | Собирает корзину, оформляет заказы, видит цены и остатки |
| Бухгалтер | Видит счета, оплаты, закрывающие документы, акты сверки |
| Получатель или наблюдатель | Видит заказы, доставки, статусы, но не меняет коммерческие данные |
Даже если в MVP есть только один владелец, таблицу членств стоит заложить сразу. Добавить роли позже намного проще, если авторизация уже понимает связь account ↔ customer.
Если бизнес говорит «у нас пока один пользователь на компанию», это не всегда означает, что можно делать прямой FK из пользователя в компанию. Часто «пока» заканчивается в первой же итерации после запуска, когда клиент просит добавить бухгалтера или закупщика.
Заказчик и юрлицо — не одно и то же
Заказчик — это носитель коммерческих отношений. На нём удобно хранить сегмент, персонального менеджера, общую скидку, лимиты, настройки видимости ассортимента и историю работы.
Юрлицо — это носитель реквизитов. У него есть ИНН, КПП, ОГРН, юридический адрес, банковские реквизиты, договоры и документы.
Иногда заказчик и юрлицо совпадают. Например, небольшой партнёр работает от одного ООО, платит сам за себя и не использует филиальную структуру. Но у сетевых клиентов, дилеров, групп компаний и B2B-порталов для сложной дистрибуции это быстро перестаёт быть правдой.
Полезное правило:
| Вопрос | Если ответ «нет» | Если ответ «да» |
|---|---|---|
| У клиента бывает несколько юрлиц? | Можно схлопнуть заказчика и юрлицо | Нужна связь Customer 1:N LegalEntity |
| Платит иногда не то юрлицо, которое заказывает? | Плательщик равен юрлицу заказа | Нужен payer_entity_id в заказе |
| С кабинетом работает несколько людей? | Можно начать с одного владельца | Нужны Membership и роли |
| Один человек представляет несколько клиентов? | Достаточно одного активного заказчика | Нужен переключатель заказчика и M:N-связь |
| У юрлица бывает несколько договоров? | Условия можно хранить на заказчике | Нужен Contract и выбор договора |
Что фиксировать в заказе
Заказ — это исторический документ. Он не должен меняться задним числом из-за правки адреса, реквизитов или прайса.
Поэтому в заказе нужны не только ссылки, но и снапшоты:
- адрес доставки на момент оформления;
- реквизиты юрлица и плательщика на момент выставления документов;
- цена каждой позиции на момент заказа;
- применённая скидка или тип цены;
- ФИО и контакты сотрудника, который оформил заказ;
- внешний номер или GUID из 1С после синхронизации.
Отдельно стоит хранить quantity_requested и quantity_actual: сколько клиент заказал и сколько реально собрано или отгружено. Для B2B с дефицитом склада, частичной сборкой и заменами это критичнее, чем кажется на старте.
Финансы лучше не считать в кабинете
Если у компании уже есть 1С, ERP или CRM, кабинет обычно не должен становиться самостоятельной финансовой системой. Он создаёт заказы и показывает данные, но источником истины по оплатам, счетам, актам и задолженности остаётся учётная система.
Практичная модель:
- заказ создаётся в кабинете;
- в очередь обмена кладётся событие для 1С или ERP;
- внешняя система принимает заказ, присваивает номер и рассчитывает документы;
- кабинет получает обратно статусы, счета, оплаты, отгрузки и PDF-документы;
- повторные синхронизации дедуплицируются по
external_id.
Так кабинет не спорит с мастер-системой и не создаёт расхождения в деньгах.
Сквозные паттерны, которые стоит заложить
Эти решения обычно окупаются даже в умеренно сложных B2B-проектах.
external_idна синхронизируемых сущностях. Если объект приходит из 1С или CRM, у него должен быть внешний идентификатор и уникальный индекс.- Очередь обмена. Заказ не должен теряться, если 1С временно недоступна. Нужны ретраи и понятный статус синхронизации.
- Заявки вместо прямого редактирования. Реквизиты, отмены, новые сотрудники и изменения профиля лучше проводить через request-сущности со статусами.
- Деактивация вместо удаления. Всё, на что ссылаются заказы и документы, выключается через
is_active, а не удаляется. - Раздельные статусы. Статус заказа, оплаты и доставки — разные поля. Заказ может быть собран, но не оплачен; оплачен, но не отгружен.
unknownдля внешних статусов. Если 1С пришлёт новый статус, синхронизация не должна падать из-за enum.- Снапшоты для истории. Адреса, цены и реквизиты в заказе должны сохраняться копией.
Как выбрать уровень сложности
Не каждому проекту нужна максимальная модель с M:N-связями во всех местах. Но каждому проекту нужна честная проверка сценариев.
Уровень 1. Простой кабинет
Подходит, если один пользователь работает от одного партнёра, у партнёра одно юрлицо, договоры и счета живут только в 1С, а кабинет нужен для заказов и истории.
Что можно упростить:
Customer = LegalEntity;- плательщик всегда равен юрлицу;
- договор не выбирается в интерфейсе;
- роли можно начать с владельца.
Что всё равно стоит заложить:
external_id;is_active;- историю статусов;
- снапшоты заказа;
- отдельный слой для будущих членств, если есть шанс на сотрудников.
Уровень 2. Кабинет для компании с несколькими юрлицами
Подходит для дилеров, сетевых клиентов, дистрибьюторов и компаний, где один аккаунт может оформлять заказы от разных юрлиц.
Здесь уже нужны:
Customer;LegalEntity;Membership;- доступы сотрудника к юрлицам;
- выбор юрлица при оформлении заказа;
- документы и счета по каждому юрлицу.
Уровень 3. Полноценный B2B-портал
Нужен, если есть несколько сотрудников, филиалы, договоры, разные плательщики, кредитные лимиты, интеграции, частичные отгрузки и сложные документы.
В этом случае модель должна включать:
- роли на членстве;
- договоры;
- плательщика в заказе;
- адреса с доступами;
- счета и оплаты;
- события синхронизации;
- request-процессы;
- аудит действий.
Типичные ошибки
Сделать “клиента” одной сущностью на всё.
Такой подход выглядит быстрее, но быстро ломается: коммерческие условия, реквизиты, роли и счета начинают жить в одном месте.
Хранить роль в аккаунте.
Роль должна зависеть от заказчика. Иначе невозможно нормально поддержать сценарий «один человек — несколько компаний».
Привязать договор к пользователю.
Договор относится к юрлицу, а не к человеку. Пользователь только выбирает доступный договор при заказе.
Не сохранять снапшоты.
Если заказ хранит только ссылку на адрес или реквизиты, правка справочника меняет историю.
Считать финансы внутри кабинета.
Если суммы, оплаты и документы уже живут в 1С, кабинет должен показывать их, а не пересчитывать по собственной логике.
Удалять справочники.
Юрлица, адреса, договоры и сотрудники должны деактивироваться. Удаление ломает историю заказов.
Не предусмотреть неизвестные статусы.
Интеграции меняются. Новый статус из внешней системы не должен останавливать обмен.
Чек-лист проверки модели
Перед разработкой полезно пройти восемь сценариев. Если модель поддерживает их без костылей, она выдержит большинство B2B-нагрузок.
- Новый клиент регистрируется, подтверждает контакт, проходит проверку и активацию менеджером.
- Владелец приглашает бухгалтера, который видит счета и документы, но не оформляет заказы.
- Один человек работает с двумя компаниями и переключает текущего заказчика.
- Заказ оформляется от филиала, а счёт выставляется на головное юрлицо.
- В одной корзине товары разделяются на несколько заказов по разным юрлицам.
- Юрлицо деактивировали во внешней системе, и кабинет корректно закрывает доступ к заказам от него.
- Заказ частично собран: клиент просил 10 единиц, склад подтвердил 7.
- Реквизиты изменились в 1С, но старые счета и заказы остались исторически корректными.
Вывод
B2B-кабинет редко ломается из-за того, что в нём мало экранов. Чаще он ломается из-за модели, которая не различает человека, заказчика, юрлицо, договор и плательщика.
Хорошая архитектура не обязана быть максимально сложной с первого дня. Но она должна ясно отвечать на ключевые вопросы: кто действует, от чьего имени, по каким условиям, кто платит и какая система является источником истины.
Если эти ответы заложены в модель данных, кабинет проще развивать: добавлять сотрудников, договоры, документы, роли, филиалы и интеграции без болезненной переделки фундамента.