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 | Описание |
|---|---|---|---|
id | uuid | auto | Первичный ключ |
franchise_id | uuid | yes | FK на franchises. Основа scope (см. §7) |
phone | string (E.164) | yes | Нормализованный формат +7XXXXXXXXXX. Уникальность per franchise_id при deleted_at IS NULL |
email | string(255) | no | Lower-case. Валидация по RFC 5322. Уникальность НЕ требуется |
first_name | string(100) | yes | |
last_name | string(100) | no | |
birthday | date | no | Используется в будущих промо «именинники ±N дней» |
gender | enum | no | male / female / other / unknown. Default unknown |
notes | text | no | Свободные заметки кассира/менеджера |
registration_source | enum | yes | pos / web / mobile / admin / import. Default admin |
registered_by_employee_id | uuid | no | Если завёл кассир/админ (для аудита) |
registered_at | timestamp | auto | = created_at, дублируем для удобства аналитики |
consent_signed_at | timestamp | no | Отметка о согласии на обработку ПД (ФЗ-152). Заполнение — out of scope этой BR, но поле заводим сразу |
deleted_at | timestamp | no | Soft delete с анонимизацией (см. §3) |
created_at | timestamp | auto | |
updated_at | timestamp | auto |
Валидация на входе:
phoneнормализуется к E.164 (89991234567→+79991234567). Если невалидный —400 VALIDATION_ERROR.emaillower-case, проверка на валидность. Пустая строка → null.first_nameне может быть пустым.
§2. Адреса клиента
Отдельная таблица customer_addresses — у одного клиента может быть несколько адресов доставки (дом / работа / родителям).
| Поле | Тип | Required | Описание |
|---|---|---|---|
id | uuid | auto | |
customer_id | uuid | yes | FK на customers |
city | string(100) | yes | |
street | string(255) | yes | |
apartment | string(50) | no | |
entrance | string(20) | no | |
floor | string(20) | no | |
intercom | string(50) | no | |
notes | text | no | «Код от домофона 1234», «оставить у консьержа» |
is_default | boolean | yes | Один default на клиента. Default false если не первый |
created_at, updated_at | timestamp | auto |
Правило: у клиента не может быть двух is_default = true одновременно. При пометке нового как default — предыдущий default снимается в одной транзакции.
§3. Операции над клиентом
| Операция | Ключ доступа | Поведение |
|---|---|---|
| Создание | customers.edit (админ) или customers.create_quick (POS) | Quick на POS: только phone + first_name. Полная форма в админке — все поля. Email / phone проверяются на нормализацию и уникальность |
| Получение по id | customers.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 + anonymize | customers.delete | deleted_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: на экране заказа — кнопка «Клиент» → модалка:
- Ввод телефона
GET /api/v1/pos/customers/search?phone=+7...→ если найден — показываем карточку (имя, группы, ближайший ДР)- Если не найден — предложение «Создать клиента»: упрощённая форма (phone + first_name минимум, опционально email и дата рождения)
- После создания/выбора:
POST /api/v1/pos/orders/{order_id}/attach-customer
- Кассир видит в карточке прикреплённого клиента: имя, группы, LTV (сумма закрытых заказов), ближайший ДР (если он в ±14 дней). Скидок, баллов, промо — пока НЕТ — это BR 3.2+.
- Открепить клиента от заказа —
DELETE /api/v1/pos/orders/{order_id}/customer(до закрытия заказа).
§6. Ролевая матрица
| Действие | Franchise-owner | Partner-owner | Manager ТТ | Cashier | Required permission |
|---|---|---|---|---|---|
| Список клиентов в админке | ✅ | ✅ (той же франшизы) | ❌ | ❌ | customers.read |
| Карточка клиента | ✅ | ✅ | ❌ | ❌ | customers.read |
| Создание в админке (полная форма) | ✅ | ✅ | ❌ | ❌ | customers.edit |
| Редактирование | ✅ | ✅ | ❌ | ❌ | customers.edit |
| Soft delete (анонимизация) | ✅ | ❌ | ❌ | ❌ | customers.delete |
| Merge дубликатов | ✅ | ❌ | ❌ | ❌ | customers.edit + только owner франшизы |
| Поиск по телефону на POS | — | — | — | ✅ | customers.create_quick (включает read своей франшизы) |
| Quick-create на POS | — | — | — | ✅ | customers.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.delete | Soft delete клиента (анонимизация) |
customers.create_quick | Быстрое создание клиента на POS кассиром + поиск + attach к заказу |
customer_groups.read | Просмотр групп в админке |
customer_groups.edit | CRUD групп в админке |
Системная роль «Администратор» (см. 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. В БД всегда одно представление. - Уникальность
phoneperfranchise_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 |
| Импорт клиентов из Excel | 3.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.closedCustomer Service пересчитывает группы только для конкретного клиента. Полный пересчёт — только раз в сутки. Это консенсус на этапе контракта.
7. Ссылки
- Roadmap — Скидки и лояльность — откуда взялась BR, какие идут после
- BR 1.4.4 — ролевая модель, куда добавляем новые permissions
- BR 1.5 — гейтинг UI по permissions, применяется к новому разделу «Клиенты»
- Роли и permissions — обновляется после реализации BR
- Репозитории —
erp-customer-serviceперенести в основную таблицу - YumaPOS · customers — общий обзор клиентов
- YumaPOS · customers-list — экран списка
- YumaPOS · customers-groups — static + dynamic группы (главный источник для §4)
- YumaPOS · add-customer — форма создания
- Репо: https://github.com/nearbyErp/erp-customer-service