Временные тарифы (Happy Hours)

Происхождение

Программа №3 из «Loyalty Programs» drawio коллеги (зелёный — низкий риск блокеров от PayKeeper). Реализуется в erp-catalog-service, не в erp-loyalty-service — это про витрину/цены, не про клиента.

Что это

Повторяющееся окно во времени, в течение которого к выбранным товарам/категориям/всему меню применяется процентная скидка на конкретных ТТ. Самые частые сценарии: «утренний кофе −15% до 10:00», «happy hours в баре 17–19», «вечерняя выпечка −30% после 19:00».

Сущность

TimeTariff:

ПолеТипОписание
idUUID
franchise_idUUIDТенант
namestring ≤100Видимое имя
discount_pctDECIMAL(5,2)(0, 100]
days_of_weekint[]ISO 1=Пн..7=Вс
time_fromTIMEСтарт окна
time_toTIMEКонец окна; должно быть time_from < time_to (межсутки не поддерживаем)
scopeenumall / categories / products
target_idsUUID[]NULL когда scope=all
store_idsUUID[]На какие ТТ распространяется (≥1)
statusenumactive / disabled
created_at, updated_atTIMESTAMP

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

  • Per-ТТ. Тариф применяется только к перечисленным ТТ. ТТ без тарифа работают по дефолтным ценам.
  • Конфликты — больший процент. Если для пары (store, product, now) подходят несколько активных тарифов, выигрывает тот у которого больше discount_pct.
  • Только повторяющиеся окна. Одноразовые акции (one-off) не поддерживаются — отдельная фича.
  • Скидка применяется к базовой цене товара, не к модификаторам. Расширим позже.
  • Скидка автоматически снимается когда now выходит из окна — никаких ручных действий не требуется.

Где применяется

СлойПоведение
POS меню (/internal/catalog/menu?store_id=X)At-request-time резолв — POS видит снижённые цены без задержки
Заказ (OrderItem.unit_price)POS уже передал сниженную цену → попадает в Order Service как-есть
Инвойс PayKeeperCart инвойса формируется из OrderItem-ов → чек печатается со скидкой ✅
Каталог в ЛК PK (ims-api)Scheduler триггерит upsert на границах окна; PK видит «лучшую» цену по любой ТТ ЮЛ (per-ЮЛ ограничение каталога PK)

Архитектура (компактно)

Admin создаёт TimeTariff
        ↓
[catalog-service: time_tariffs + time_tariff_stores]
        ↓
   ┌────┴────────┐
   ↓             ↓
[/menu]   [TimeTariffScheduler @1min]
at-request-time   на границах окна
TimeTariffResolver:  publish catalog.product.upserted
max(discount_pct)    → adapter → PK ims-api
   ↓
[POS видит цены]

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

РольListViewCRUD
Владелец франшизы✅ все ТТ✅ все ТТ
Владелец партнёра✅ свои ТТ✅ свои✅ свои ТТ
Менеджер ТТ❌ (read-only через POS-меню)
Кассир

Permissions (переиспользуем):

  • menu.read — GET /api/v1/time-tariffs
  • menu.edit — POST/PATCH/DELETE

UI

Страница Каталог → Временные тарифы:

  • Список: имя, скидка, дни недели, окно, scope, кол-во ТТ, статус, действия.
  • Кнопка «Создать тариф» открывает модалку.
  • Форма: имя, % скидки, чекбоксы Пн-Вс, время с/до, radio scope, мультиселект ТТ.
  • Для scope=categories / scope=products — текстовый ввод UUID списка (MVP). В будущем — пикер с поиском.
  • На статус disabled — кнопка «Отключить» в форме редактирования (soft delete).

Точность времени

Scheduler tick раз в минуту → передача в PayKeeper-каталог происходит с задержкой до ~1.5 минуты после фактического начала/конца окна. На POS меню задержки нет — резолв at-request-time.

Phase 2 — prefetch

Предзагрузка цен «за минуту до» границы окна — отдельная фаза. Тогда задержка снизится до ~30 сек после фактического перехода.

Что НЕ поддерживается (out of scope)

  • Одноразовые акции (one-off) — нужно добавлять в модель отдельным полем + UI.
  • Анти-списание остатков (триггер по партии в warehouse) — отдельная фича из drawio.
  • Скидки на модификаторы — только base price.
  • Конфликты по приоритету (больший % выигрывает; нет ручного weight).
  • Cross-merchant identity (программа №13) — отдельный сервис.

Ссылки