BR 3.1 — Клиенты и группы клиентов (CRM)

Это первая BR новой серии Phase 3 — Loyalty & Marketing

Phase 3 покрывает CRM, лояльность, промо-кампании, подарочные карты. BR 3.1 — фундаментальная: без сущности Клиент и групп клиентов ни одна последующая фича (скидки, баллы, промо, карты) не реализуется. Источник и порядок фаз — Roadmap — Скидки и лояльность.


1. Контекст

Что есть сейчас:

  • В user_db — только сотрудники (employees). Клиент как сущность в системе не существует.
  • orders не имеет customer_id — заказ анонимен.
  • Customer BFF (:3021) есть в архитектуре, но не возвращает профиль клиента — нечего возвращать.
  • Репозиторий erp-customer-service создан на GitHub, пустой. Порт :3013 в Repositories.md зарезервирован.

Что блокируется без CRM:

  • Лояльность (BR 3.2+). Баллы привязываются к клиенту. Без клиента баллы не начислить и не списать.
  • Скидки и промо-кампании (BR 3.4+). Таргетинг «именинникам ±3 дня», «VIP-клиентам», «купи 5 — 6-й бесплатно» требуют знать кто покупает.
  • Мобильное приложение и сайт клиента. Customer BFF не может отдать профиль / историю / баллы — их негде хранить.
  • Аналитика. Сейчас видна только выручка по ТТ / сотрудникам. LTV, когорты, повторные покупки — не посчитать.

Что делает эта BR: Вводит базовую модель клиента, операции CRUD, поиск по телефону как основной ключ, привязку клиента к заказу на POS, группы клиентов (статические + динамические с правилами автозаполнения), ролевой доступ.

Референс: YumaPOS — customers, customers-groups, add-customer, customer-revenue-report. Модель клиента и группы берём как основу, адаптируя под нашу permission-based ролевую модель (BR 1.4.4) и нашу мультитенантность.


2. Требования

§1. Сущность Customer — поля и валидация

ПолеТипRequiredОписание
iduuidautoПервичный ключ
franchise_iduuidyesFK на franchises. Основа scope (см. §7)
phonestring (E.164)yesНормализованный формат +7XXXXXXXXXX. Уникальность per franchise_id при deleted_at IS NULL
emailstring(255)noLower-case. Валидация по RFC 5322. Уникальность НЕ требуется
first_namestring(100)yes
last_namestring(100)no
birthdaydatenoИспользуется в будущих промо «именинники ±N дней»
genderenumnomale / female / other / unknown. Default unknown
notestextnoСвободные заметки кассира/менеджера
registration_sourceenumyespos / web / mobile / admin / import. Default admin
registered_by_employee_iduuidnoЕсли завёл кассир/админ (для аудита)
registered_attimestampauto= created_at, дублируем для удобства аналитики
consent_signed_attimestampnoОтметка о согласии на обработку ПД (ФЗ-152). Заполнение — out of scope этой BR, но поле заводим сразу
deleted_attimestampnoSoft delete с анонимизацией (см. §3)
created_attimestampauto
updated_attimestampauto

Валидация на входе:

  • phone нормализуется к E.164 (89991234567+79991234567). Если невалидный — 400 VALIDATION_ERROR.
  • email lower-case, проверка на валидность. Пустая строка → null.
  • first_name не может быть пустым.

§2. Адреса клиента

Отдельная таблица customer_addresses — у одного клиента может быть несколько адресов доставки (дом / работа / родителям).

ПолеТипRequiredОписание
iduuidauto
customer_iduuidyesFK на customers
citystring(100)yes
streetstring(255)yes
apartmentstring(50)no
entrancestring(20)no
floorstring(20)no
intercomstring(50)no
notestextno«Код от домофона 1234», «оставить у консьержа»
is_defaultbooleanyesОдин default на клиента. Default false если не первый
created_at, updated_attimestampauto

Правило: у клиента не может быть двух is_default = true одновременно. При пометке нового как default — предыдущий default снимается в одной транзакции.

§3. Операции над клиентом

ОперацияКлюч доступаПоведение
Созданиеcustomers.edit (админ) или customers.create_quick (POS)Quick на POS: только phone + first_name. Полная форма в админке — все поля. Email / phone проверяются на нормализацию и уникальность
Получение по idcustomers.readВозвращает клиента с адресами и списком групп
Поиск по телефонуcustomers.read или customers.create_quick (чтобы POS мог искать перед созданием)Возвращает 0 или 1 результат. Телефон нормализуется перед поиском
Список / фильтрcustomers.readПоиск по подстроке имени/фамилии/email/phone. Фильтры: registration_source, группы, диапазон даты регистрации
Редактированиеcustomers.editМеняются любые поля кроме id, franchise_id, registered_at, registered_by_employee_id
Soft delete + anonymizecustomers.deletedeleted_at = now(). PII анонимизируются: first_name = «Удалён», last_name = null, phone = +7000000<id_suffix> (уникальность сохраняется), email = null, notes = null, birthday = null. Адреса обнуляются. Связь с заказами через customer_id сохраняется — история заказов остаётся.
Merge дубликатовcustomers.edit (только owner франшизы)POST /customers/{target_id}/merge { source_id }: источник soft-deleted, на target переносятся адреса, customer_id в orders. Баллы/промо — не в scope (будут в 3.2). MVP — только API, UI в админке — отдельная BR

Soft delete, а не hard:

  • В orders.customer_id есть FK — физическое удаление сломает историю заказов.
  • Бухгалтерия/54-ФЗ требует сохранять обезличенную историю транзакций.
  • ФЗ-152: «право на забвение» удовлетворяется анонимизацией — персональные данные удалены, но транзакционная история (обезличенная) сохраняется.

§4. Группы клиентов

Две категории:

А. Статические группы — админ явно добавляет/удаляет клиентов.

  • Таблица customer_groups (id, franchise_id, name, description, type=static, deleted_at, created_at, updated_at)
  • Таблица customer_group_members (group_id, customer_id, added_at) — ручное членство

Б. Динамические группы — система пересчитывает членство по правилам.

  • Таблица customer_groups (id, franchise_id, name, description, type=dynamic, rules_json, deleted_at, created_at, updated_at)
  • Членство вычисляется и кэшируется в customer_group_members (та же таблица, для унификации чтения)
  • Пересчёт: scheduler раз в сутки (в 03:00 ночи по МСК) + по событиям customer.updated, order.closed (точечный пересчёт для конкретного клиента)

Правила автозаполнения (MVP для dynamic групп):

  • По сумме покупок: all-time / за последние N дней / в конкретный диапазон
  • По количеству дней без активности (последний заказ был > N дней назад)
  • По месяц+день рождения ±N дней (для именинников)
  • По городу (из customer_addresses)
  • По зоне доставки (FK на delivery_zones из Store Service)
  • По полу
  • По источнику регистрации
  • Логический оператор: все правила AND. Из комбинаций правил — до 5 штук на группу.
  • Включение / исключение других групп — клиент группы B исключается из A (NOT IN (group_B))

Не в MVP (оставляем как открытый вопрос / следующая BR):

  • Правила «по баллам лояльности» — будут в BR 3.3 после появления Loyalty Service
  • Правила «по оценкам заказов» (4-5 звёзд) — отложено до появления оценок
  • Сложная логика OR внутри правил — на старте только AND

Правила:

  • Клиент может быть в нескольких группах одновременно (и static, и dynamic).
  • Удаление группы — soft delete. Членство (customer_group_members) остаётся в истории, но UI фильтрует по deleted_at IS NULL.
  • Уникальность имени группы per franchise_id при deleted_at IS NULL.

§5. Привязка клиента к заказу (POS)

  • Новое поле orders.customer_id (nullable) в Order Service. Заказ может быть анонимным (null) — анонимные продажи на вынос не требуют привязки клиента.
  • POS-flow: на экране заказа — кнопка «Клиент» → модалка:
    1. Ввод телефона
    2. GET /api/v1/pos/customers/search?phone=+7... → если найден — показываем карточку (имя, группы, ближайший ДР)
    3. Если не найден — предложение «Создать клиента»: упрощённая форма (phone + first_name минимум, опционально email и дата рождения)
    4. После создания/выбора: POST /api/v1/pos/orders/{order_id}/attach-customer
  • Кассир видит в карточке прикреплённого клиента: имя, группы, LTV (сумма закрытых заказов), ближайший ДР (если он в ±14 дней). Скидок, баллов, промо — пока НЕТ — это BR 3.2+.
  • Открепить клиента от заказа — DELETE /api/v1/pos/orders/{order_id}/customer (до закрытия заказа).

§6. Ролевая матрица

ДействиеFranchise-ownerPartner-ownerManager ТТCashierRequired permission
Список клиентов в админке✅ (той же франшизы)customers.read
Карточка клиентаcustomers.read
Создание в админке (полная форма)customers.edit
Редактированиеcustomers.edit
Soft delete (анонимизация)customers.delete
Merge дубликатовcustomers.edit + только owner франшизы
Поиск по телефону на POScustomers.create_quick (включает read своей франшизы)
Quick-create на POScustomers.create_quick
Прикрепить клиента к заказуcustomers.create_quick
CRUD групп клиентовcustomer_groups.edit

Новые permissions (добавляются к существующим 46 из BR 1.4.3 / 1.4.4):

PermissionНазначение
customers.readПросмотр клиентов в админке
customers.editСоздание, редактирование, merge в админке
customers.deleteSoft delete клиента (анонимизация)
customers.create_quickБыстрое создание клиента на POS кассиром + поиск + attach к заказу
customer_groups.readПросмотр групп в админке
customer_groups.editCRUD групп в админке

Системная роль «Администратор» (см. BR 1.4.4) получает все новые permissions автоматически — нужна миграция, которая добавляет их во все существующие системные роли.

§7. Scope и мультитенантность

Изоляция по franchise_id — один пул клиентов на бренд:

  • Владелец франшизы (legal_entities.owner_user_id + type=franchise) — видит всех клиентов franchise_id
  • Владелец партнёра-франчайзи (type=franchisee) — видит тех же клиентов (общий пул)
  • Обычные сотрудники — нет доступа

Важно (нюанс из BR 1.4.4): Для franchises.type = individual (ИП — одно главное ЮЛ, без партнёров) scope работает идентично corporate. Разница только в UI — раздел «Юр. лица» скрыт, раздел «Клиенты» виден и работает. В данных, API, permissions — изоляция та же: franchise_id. Эта заметка важна чтобы при реализации не сделать «только для corporate». Проверка в коде — по franchise_id в JWT, без оглядки на type.

Между франшизами клиенты не пересекаются. У Demo Coffee и Другая Сеть Кофеен — свои клиенты, даже если у них одинаковый телефон.


3. Бизнес-правила

  • Телефон — E.164, нормализация на входе. Пользователь может ввести 89991234567, +7 (999) 123-45-67, 79991234567 — система приводит к +79991234567. В БД всегда одно представление.
  • Уникальность phone per franchise_id + deleted_at IS NULL. При попытке создать второго — 409 CUSTOMER_PHONE_TAKEN. Если дубль по номеру телефона найден — POS предлагает «Использовать существующего».
  • Email без уникальности. Семьи пользуются одним email, агенты по доставке — одним корпоративным. Но валидация формата — обязательна.
  • Soft delete + анонимизация — единственный способ удаления. Физический DELETE запрещён.
  • Динамические группы пересчитываются раз в сутки + триггерно по customer.updated, order.closed. Точечный пересчёт — только для затронутого клиента, не для всех.
  • Один клиент — в N группах одновременно. Исключение одной группы из другой описывается в правиле dynamic группы.
  • Frontend на POS после успешного quick-create показывает сообщение: «Клиент создан. Собрано согласие на обработку ПД? Если нет — добавьте отметку в админке позже» — чтобы не терять compliance.

4. Затронутые сервисы

СервисЧто меняется
Customer Service (новый, :3013, репо erp-customer-service)Создаётся с нуля. Модель Customer + CustomerAddress + CustomerGroup + CustomerGroupMember + CustomerGroupRules. CRUD API. Scheduler для dynamic групп. Kafka-consumers для customer.updated, order.closed (точечный пересчёт групп)
Order ServiceДобавить orders.customer_id (nullable, FK на Customer Service logically — без физического FK между сервисами). API вернуть customer_id в заказе. Endpoint PATCH /orders/{id}/customer, DELETE /orders/{id}/customer
Auth ServiceНе трогаем. Клиентская авторизация (регистрация через мобильное приложение / сайт, OTP по SMS) — отдельная BR 3.5
User ServiceНе трогаем. Customer полностью живёт в Customer Service. Владельцы, партнёры, сотрудники — остаются в User Service
Admin BFF (:3020)Проксирование /api/v1/admin/customers/*, /api/v1/admin/customer-groups/* в Customer Service
POS BFF (:3022)Проксирование /api/v1/pos/customers/search, /api/v1/pos/customers (quick create), /api/v1/pos/orders/{id}/attach-customer (в Order Service с предварительным lookup в Customer Service)
Customer BFF (:3021)Пока не трогаем. Авторизация клиентов — отдельная BR 3.5. Когда будет — Customer BFF получит /api/v1/me/profile и т.д.
Admin Franchise (web)Новый раздел «Клиенты» — список, карточка, форма создания/редактирования. Раздел «Клиенты → Группы» — список групп, конструктор правил (для dynamic). На карточке заказа (/orders/:id) — блок «Клиент» с ссылкой на карточку

5. Out of scope

Явно не делаем в этой BR (идут отдельно):

ТемаBR
Баллы лояльности — начисление/списание, настройки, карты лояльности3.2
Скидки (простые, ручные на POS)3.3
Динамические группы «по баллам» (правило требует Loyalty Service)3.3 как довесок к 3.1
Промо-кампании с условиями, промокоды3.4
Авторизация клиентов (регистрация / OTP / JWT для клиентов)3.5
Мобильное приложение и сайт клиента — профиль, история заказов3.6
Подарочные карты3.7
Кредитные счета (house account)3.8
Импорт клиентов из Excel3.X — отдельная BR на compliance/импорт
UI объединения дубликатов (merge UI)3.X — MVP только API
Customer revenue report, LTV-аналитика, когортный анализ3.X — после накопления данных
Процесс сбора согласия на обработку ПД (ФЗ-152 UX/бумаги)Отдельная BR на compliance

6. Открытые вопросы

  • Согласие на обработку ПД (ФЗ-152). В эту BR включаем поле consent_signed_at как отметку, но не специфицируем UX. Как собирается согласие (галочка в POS / бумажная форма / электронная подпись) — отдельная BR. Бизнес решает кто это делает (юристы).
  • Клубы/статусы (VIP / Gold / Silver). В этой BR — только через группы. Отдельный «tag по клубу» на карточке клиента (как в YumaPOS) — не вводим, достаточно принадлежности к группе. Если окажется что этого мало — добавим отдельным полем в BR 3.3.
  • Merge UI. MVP — только API POST /customers/{id}/merge. Интерфейс объединения дубликатов в админке (выбор двух клиентов → предпросмотр → подтверждение) — отдельная BR.
  • Автоматическое обнаружение дубликатов. YumaPOS показывает предложения «возможно это дубликаты». Откладываем.
  • Scheduler — где живёт. В Customer Service как встроенный @Scheduled или через внешний cron? Решаем в техническом контракте (03-Services/Customer Service/Overview.md).
  • Точечный пересчёт групп — по событию order.closed Customer Service пересчитывает группы только для конкретного клиента. Полный пересчёт — только раз в сутки. Это консенсус на этапе контракта.

7. Ссылки