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_sumday_of_week[],time_from,time_toorder_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
- Мета:
- Движок применения — сложный:
- Находит все
is_active AND now BETWEEN active_from AND active_toкампании - Фильтрует по всем
conditionsприменительно к текущему заказу - Сортирует по
priority - Применяет каскадом, учитывая
exclude_subsequent_promos - Возвращает список применённых скидок + пересчитанную сумму
- Находит все
- Три уровня применения (из 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 + 1 | CRM + ручные скидки. Кассир уже может работать с клиентами и скидками |
| B · Удержание | Фазы 2 + 3 | Баллы + уровни. Клиенты возвращаются |
| C · Маркетинг-движок | Фазы 4 + 5 | Сложные кампании и промокоды. Основной инструмент роста |
| D · Опциональные расширения | Фазы 6 + 7 + 8 | Подарочные карты, кредит, спец. платежи. По мере бизнес-запроса |
Кластеры A → B → C — естественная цепочка. D можно врезать в любой момент после A.
Архитектурные заметки
-
Loyalty Service как отдельный микросервис — лучше чем модуль в User/Order, потому что:
- Изолированный write-heavy workload (все транзакции баллов)
- Независимый релиз-цикл
- Будущая возможность шардирования
- Порт
:3007уже зарезервирован в07-Tasks/Repositories.md(Phase 3)
-
Customer Service vs модуль в User Service — на развилке:
- Модуль в User — быстрее стартовать, общая инфра (JWT, scope)
- Отдельный микросервис — правильнее долгосрочно (Customer != Employee, другая модель доступа). Порт
:3013свободен - Решение отложить на Фазу 0 — оформить ADR
-
Event-driven между Order ↔ Loyalty:
order.closed→ Loyalty начисляет баллыorder.refunded→ Loyalty делает обратную операцию (storno)customer.group.changed→ Order пересчитывает применимые скидки (если заказ открыт)
-
Применение скидок — где считать:
- Вариант A: на стороне POS BFF — тонкий клиент, дёргает Loyalty Service
POST /apply-promoс content заказа - Вариант B: на стороне Order Service — event-driven
- Рекомендую вариант A: POS BFF агрегирует, это его зона
- Вариант A: на стороне POS BFF — тонкий клиент, дёргает Loyalty Service
-
Совместимость с PayKeeper:
- Скидки применяются до отправки в PK (в наш заказ)
- В PK уходит уже итоговая сумма с рассчитанными скидками
- Баллы, подарочные карты, кредит — не выходят в PK как тип оплаты (PK их не знает). Они закрывают часть заказа у нас, а в PK улетает оставшаяся часть наличными/картой
- Соответственно: сверка сумм между нами и PK должна учитывать внутренние типы оплаты
Что посмотреть у YumaPOS при детальной проработке
(источники для каждой фазы — для быстрого возврата к деталям при написании BR’ов)
| Фаза | Файлы YumaPOS в _reference/yumapos/ |
|---|---|
| 0 | customers-list.md, customers-groups.md, add-customer.md, customer-revenue-report.md |
| 1 | discounts.md |
| 2 | customer-loyalty-programs.md, how-to-add-points.md, pay-order.md (секция баллов) |
| 3 | customers-groups.md (dynamic rules), how_to_launch_promo.md (примеры по уровням) |
| 4 | promotional-campaigns.md (главный, самый большой), how_to_launch_promo.md |
| 5 | one-time_promotional_codes.md |
| 6 | gift-cards.md, sell-refill-gift-cards.md, redeem-gift-cards.md, check-balance-gift-card.md |
| 7 | house-account-payments.md |
| 8 | custom-types-of-payment.md |
Ссылки
- Workflow разработки
- Роли и permissions — здесь уже есть
pos.discount.apply,pos.refund,pos.price.change,pos.cash.* - Репозитории — свободные порты для новых сервисов
- YumaPOS · Promotional Campaigns — главный источник для Фазы 4