Roadmap — Скидки и программы лояльности (YumaPOS-style)

Зачем этот документ

YumaPOS — наш основной референс по функциональной полноте для общепита. В нём детально проработаны скидки, программы лояльности, подарочные карты и промо-акции. У нас в ERP всего этого нет. Документ фиксирует в каком порядке и какими блоками имеет смысл это интегрировать. Без сроков — только зависимости и логика.


TL;DR

  • Всё завязано на клиента как сущность — начинаем с CRM.
  • Потом идут простые скидки (ручная кнопка кассира) — быстрый win.
  • Затем лояльность (баллы + группы) — удержание.
  • В самом конце полноценный промо-движок — сложные кампании для маркетинга.
  • Подарочные карты и кредитные счета — опциональные расширения.

Зависимости

flowchart TD
    F0["Фаза 0 · CRM<br/>клиент + группы"] --> F1["Фаза 1 · Простые скидки<br/>ручная кнопка кассира"]
    F0 --> F2["Фаза 2 · Базовая лояльность<br/>баллы earn/spend"]
    F2 --> F3["Фаза 3 · Уровни/клубы<br/>динамические группы"]
    F0 --> F4["Фаза 4 · Промо-движок<br/>сложные кампании"]
    F4 --> F5["Фаза 5 · Промокоды<br/>разовые и единые"]
    F0 --> F6["Фаза 6 · Подарочные карты<br/>новый тип платежа"]
    F0 --> F7["Фаза 7 · Кредитные счета<br/>B2B опционально"]
    F4 --> F8["Фаза 8 · Спец. типы платежей<br/>сервисный сбор, округление"]

    style F0 fill:#1a1a2e,stroke:#e94560,color:#fff
    style F1 fill:#16213e,stroke:#40916c,color:#fff
    style F2 fill:#16213e,stroke:#40916c,color:#fff
    style F3 fill:#0f3460,stroke:#f72585,color:#fff
    style F4 fill:#0f3460,stroke:#f72585,color:#fff
    style F5 fill:#0f3460,stroke:#f72585,color:#fff
    style F6 fill:#2d1b4e,stroke:#a855f7,color:#fff
    style F7 fill:#2d1b4e,stroke:#a855f7,color:#fff
    style F8 fill:#2d1b4e,stroke:#a855f7,color:#fff

Легенда:

  • Красная рамка (F0) — фундамент. Без него ничего
  • Зелёная (F1, F2) — быстрые wins с прямой отдачей
  • Розовая (F3, F4, F5) — основной маркетинговый функционал
  • Фиолетовая (F6, F7, F8) — опциональные расширения

Фаза 0 · CRM — Клиент и группы

Цель. Завести в системе сущность Клиент и механику групп клиентов. Без этого никакая скидка/баллы/промо не работают.

Что делаем:

  • Customer Service (новый микросервис, :3013 — сейчас свободен) либо модуль в User Service. Модель: customers (id, franchise_id, phone, email, first_name, last_name, birthday, registered_at, registration_source, current_group_ids[])
  • Привязка клиента к заказу — в Order Service добавить customer_id (nullable)
  • Группы клиентов — сущность customer_groups (static / dynamic). Для dynamic — правила:
    • По сумме покупок за период (всего / за N дней)
    • По количеству дней без активности
    • По дате рождения (месяц+день с окном ±N дней)
    • По городу / зоне доставки
    • По баллам (диапазон — нужна Фаза 2, пока без этого правила)
    • Включение/исключение других групп
  • Scheduler — раз в сутки пересчитывает dynamic группы
  • API: CRUD customer, search (phone/email), add/remove to group, list groups
  • UI: раздел «Клиенты» — список, карточка (профиль, заказы, группы), раздел «Клиенты → Группы» — CRUD + конструктор правил для dynamic

Что на выходе: клиент при заказе на POS (по телефону) добавляется в БД, попадает в группы. Можно ввести его в заказ на POS (для будущих скидок).


Фаза 1 · Простые скидки (ручные)

Цель. Быстрый win — кассир может на POS нажать «Скидка» и применить процент или фиксированную сумму на позицию или чек. Админ заводит список разрешённых скидок.

Что делаем:

  • Loyalty Service (новый микросервис :3007 — зарезервирован под Loyalty Phase 3). Модель: discounts (id, franchise_id, name, type=percent|fixed, value, scope=order|item, max_value, is_active)
  • API: CRUD скидок, применение к заказу (internal, из POS BFF)
  • POS-логика: кнопка «Скидка» на экране заказа. Permission pos.discount.apply уже есть — привязать к ней. Если у роли есть max_value → ограничение
  • UI: раздел «Маркетинг → Скидки» — список + форма создания/редактирования
  • История применения: каждая применённая скидка логируется в заказ (order.applied_discounts[]) — для отчётов

Что НЕ делаем на этой фазе:

  • Никакой автоматики (условия, приоритеты, расписания) — это Фаза 4
  • Никаких баллов, купонов, промокодов

Фаза 2 · Базовая лояльность (баллы earn/spend)

Цель. Клиент получает баллы за покупки, оплачивает ими следующие заказы.

Что делаем:

  • Loyalty Service — расширяем. Модели:
    • point_balances (customer_id, balance, updated_at)
    • point_transactions (id, customer_id, type=earn|spend|expire|manual, amount, order_id, reason, created_at, expires_at)
  • Настройки франшизыfranchises или отдельной таблице loyalty_settings):
    • earn_rate — коэффициент начисления (например, 0.03 = 3 балла на 100 руб)
    • welcome_bonus — баллы при регистрации
    • points_ttl_days — сгорание (N дней без активности)
    • points_timer_reset_on_purchase — обновлять таймер при каждой покупке (опция из YumaPOS)
  • Event-driven:
    • При order.closed (завершён, оплачен) → Loyalty Service начисляет баллы из Kafka
    • При оплате баллами → Loyalty Service списывает, записывает transaction
  • Сгорание — scheduler: раз в сутки находит баллы с expires_at < now(), помечает expired
  • Товар «разрешает оплату баллами» — флаг is_point_payable в products (по умолчанию true)
  • POS: на экране оплаты показываем «Можно использовать баллов: X». Кассир выбирает сумму.
  • UI: карточка клиента — баланс, история транзакций, кнопка «Начислить вручную» (permission-based)
  • UI админ: «Настройки → Лояльность» — параметры франшизы

Что НЕ делаем:

  • Уровней/клубов — Фаза 3
  • Бесплатных товаров за баллы, сложных правил начисления — Фаза 4

Фаза 3 · Уровни лояльности (клубы/статусы)

Цель. Бронза → Серебро → Золото. Автоматический переход клиента между статусами по накопленным баллам или обороту.

Что делаем:

  • Расширяем Customer Groups (Фаза 0) — добавляем правила автозаполнения:
    • По диапазону баллов (points_balance BETWEEN ... AND ...)
    • По сумме покупок за период
  • Loyalty Service публикует points.changed → Customer Service пересчитывает группы для клиента
  • Связь со скидками: скидка может быть привязана к группе (если клиент в группе «Gold» — скидка 10%). Это простой случай Фазы 4, но можно поддержать минимум уже здесь: в discounts добавить customer_group_id (nullable)
  • POS: при добавлении клиента в заказ — автоматически применяется скидка его уровня
  • UI: в карточке группы — поле «Скидка клиентам этой группы»

Что НЕ делаем:

  • Полноценных промо с условиями «день недели + категория + сумма чека» — это Фаза 4

Фаза 4 · Промо-движок

Цель. Самая мощная фича. Сложные кампании: «по пятницам с 18:00 скидка 15% на кофе при чеке от 500 ₽», «купи 5 кофе — 6-й в подарок», «скидка 10% клиентам-именинникам ±3 дня».

Что делаем:

  • Loyalty Service → новая модель promo_campaigns (большая):
    • Мета: name, priority, active_from, active_to, is_active, franchise_id
    • Audience (кому): все / зарегистрированные / include_group_ids[] / exclude_group_ids[]
    • Items (на что): все / category_ids[] / product_ids[] / set (N товаров из категории)
    • Conditions (когда):
      • min_sum, max_sum
      • day_of_week[], time_from, time_to
      • order_types[] (dine_in / takeaway / delivery / fast)
      • payment_methods[]
      • order_sources[] (pos / web / mobile / aggregator)
      • delivery_zone_ids[]
      • birthday_offset_days (±N дней от дня рождения клиента)
      • order_history_over_period_days (учитывать покупки за N дней для «купи 5 → 6-й бесплатно»)
    • Reward (что дают):
      • percent / fixed_sum — скидка
      • special_price_list_id — подменить прайс на время акции
      • free_items — список бесплатных товаров или выбор из категории
      • earn_points_multiplier — начислять баллов больше обычного
      • free_delivery — бесплатная доставка
      • add_to_group_id / remove_from_group_id — переложить клиента
      • promo_code — только по коду (Фаза 5 отдаёт ввод)
    • Exclusions: exclude_modifier_cost, exclude_subsequent_promos
    • Limits: max_uses_total, max_uses_per_customer, max_uses_per_day
  • Движок применения — сложный:
    1. Находит все is_active AND now BETWEEN active_from AND active_to кампании
    2. Фильтрует по всем conditions применительно к текущему заказу
    3. Сортирует по priority
    4. Применяет каскадом, учитывая exclude_subsequent_promos
    5. Возвращает список применённых скидок + пересчитанную сумму
  • Три уровня применения (из YumaPOS):
    • Перед ручными скидками и баллами
    • После ручных скидок, перед баллами
    • После всего Задаётся на кампании
  • UI: раздел «Маркетинг → Промо-акции» — форма с вкладками (Условия / Товары / Клиенты / Награда / Расписание / Лимиты). Большая, но подсказана YumaPOS’ом
  • История: каждое применение кампании к заказу → campaign_applications(campaign_id, order_id, customer_id, amount, applied_at)

Что здесь самое сложное:

  • Наборы с историей покупок («купи 5 → 6-й бесплатно») требуют счёт по заказам клиента за период кампании
  • Порядок применения и каскады — много edge-case’ов
  • Тестовое покрытие должно быть огромным

Фаза 5 · Промокоды

Цель. Раздавать коды для промо-кампаний: через SMS, рассылки, социалки.

Что делаем:

  • Модель promo_codes (id, campaign_id, code, is_used, used_at, customer_id, order_id)
  • Два типа:
    • Единый код — один code на кампанию, поле max_uses_per_customer ограничивает использование
    • Набор одноразовых — генератор создаёт N кодов по правилам (только цифры / буквы / длина), можно экспортировать в CSV/Excel
  • Ввод кода: на POS — кнопка «Промокод», на Customer BFF — поле в корзине. Вызывает Loyalty Service → проверяет → привязывает к campaign
  • UI: внутри карточки кампании — вкладка «Промокоды», генератор, экспорт, статистика использования

Фаза 6 · Подарочные карты

Цель. Дополнительный канал продаж и новый тип оплаты.

Что делаем:

  • Модели:
    • gift_card_types (id, franchise_id, name, default_value, sell_price, is_open_sum, is_refundable, is_refillable, is_active)
    • gift_cards (id, type_id, number UNIQUE, balance, status, created_at, sold_at, sold_by_employee_id, customer_id)
  • Life cycle:
    • Выпуск — кассир продаёт карту как товар (особый SKU), вводит номер / выбирает из генератора
    • Пополнение — операция на POS (если is_refillable)
    • Оплата — карта как тип платежа (баланс снимается)
    • Возврат — если is_refundable
    • Проверка баланса — команда на POS / endpoint
  • Payment Type — новый тип gift_card в POS-оплатах. Поддержка смешанной оплаты (часть картой, часть наличными/СБП)
  • UI:
    • «Маркетинг → Подарочные карты → Типы» — CRUD типов
    • «Маркетинг → Подарочные карты → Карты» — список выпущенных, фильтры, состояние

Фаза 7 · Кредитные счета (house account)

Цель. B2B-клиенты с оплатой в кредит (корпоративы, оптовые клиенты). Опциональная фича.

Что делаем:

  • Поля в customers: credit_limit, credit_balance, credit_enabled
  • Флаг на группе клиентов: credit_enabled (из YumaPOS — активация через группу)
  • Новый тип платежа credit — списывает с credit_balance, если не хватает — уходит в отрицательный баланс до лимита
  • Пополнение: через POS (операция «Credit → Пополнить») либо через Customer BFF (личный кабинет)
  • Отчёт «Задолженность по покупателям» — дебиторка/кредиторка
  • UI: вкладка «Кредит» в карточке клиента

Фаза 8 · Специальные типы платежей

Цель. Для агрегаторов доставки и сервисных сборов.

Что делаем:

  • Модель payment_types (id, franchise_id, name, code, is_active, rounding_mode, include_in_cashflow)
  • service_fee_rules — вложенная таблица: per тип платежа, правила сервисного сбора (% или фикс, с диапазонами сумм чека)
  • Предустановленные типы: cash, card, sbp, points, gift_card, credit, + custom (Yandex.Еда, Delivery Club если агрегаторы встают как отдельные типы)
  • UI: «Настройки → Типы платежей» — CRUD, настройка сбора, флаги

Стратегические кластера

Чтобы не тащить всё одним заходом, предлагаю 3 кластера:

КластерЧто внутриЗачем
A · Фундамент + быстрый winФазы 0 + 1CRM + ручные скидки. Кассир уже может работать с клиентами и скидками
B · УдержаниеФазы 2 + 3Баллы + уровни. Клиенты возвращаются
C · Маркетинг-движокФазы 4 + 5Сложные кампании и промокоды. Основной инструмент роста
D · Опциональные расширенияФазы 6 + 7 + 8Подарочные карты, кредит, спец. платежи. По мере бизнес-запроса

Кластеры A → B → C — естественная цепочка. D можно врезать в любой момент после A.


Архитектурные заметки

  1. Loyalty Service как отдельный микросервис — лучше чем модуль в User/Order, потому что:

    • Изолированный write-heavy workload (все транзакции баллов)
    • Независимый релиз-цикл
    • Будущая возможность шардирования
    • Порт :3007 уже зарезервирован в 07-Tasks/Repositories.md (Phase 3)
  2. Customer Service vs модуль в User Service — на развилке:

    • Модуль в User — быстрее стартовать, общая инфра (JWT, scope)
    • Отдельный микросервис — правильнее долгосрочно (Customer != Employee, другая модель доступа). Порт :3013 свободен
    • Решение отложить на Фазу 0 — оформить ADR
  3. Event-driven между Order ↔ Loyalty:

    • order.closed → Loyalty начисляет баллы
    • order.refunded → Loyalty делает обратную операцию (storno)
    • customer.group.changed → Order пересчитывает применимые скидки (если заказ открыт)
  4. Применение скидок — где считать:

    • Вариант A: на стороне POS BFF — тонкий клиент, дёргает Loyalty Service POST /apply-promo с content заказа
    • Вариант B: на стороне Order Service — event-driven
    • Рекомендую вариант A: POS BFF агрегирует, это его зона
  5. Совместимость с PayKeeper:

    • Скидки применяются до отправки в PK (в наш заказ)
    • В PK уходит уже итоговая сумма с рассчитанными скидками
    • Баллы, подарочные карты, кредит — не выходят в PK как тип оплаты (PK их не знает). Они закрывают часть заказа у нас, а в PK улетает оставшаяся часть наличными/картой
    • Соответственно: сверка сумм между нами и PK должна учитывать внутренние типы оплаты

Что посмотреть у YumaPOS при детальной проработке

(источники для каждой фазы — для быстрого возврата к деталям при написании BR’ов)

ФазаФайлы YumaPOS в _reference/yumapos/
0customers-list.md, customers-groups.md, add-customer.md, customer-revenue-report.md
1discounts.md
2customer-loyalty-programs.md, how-to-add-points.md, pay-order.md (секция баллов)
3customers-groups.md (dynamic rules), how_to_launch_promo.md (примеры по уровням)
4promotional-campaigns.md (главный, самый большой), how_to_launch_promo.md
5one-time_promotional_codes.md
6gift-cards.md, sell-refill-gift-cards.md, redeem-gift-cards.md, check-balance-gift-card.md
7house-account-payments.md
8custom-types-of-payment.md

Ссылки