Временные тарифы (Happy Hours)
Происхождение
Программа №3 из «Loyalty Programs» drawio коллеги (зелёный — низкий риск блокеров от PayKeeper). Реализуется в
erp-catalog-service, не вerp-loyalty-service— это про витрину/цены, не про клиента.
Что это
Повторяющееся окно во времени, в течение которого к выбранным товарам/категориям/всему меню применяется процентная скидка на конкретных ТТ. Самые частые сценарии: «утренний кофе −15% до 10:00», «happy hours в баре 17–19», «вечерняя выпечка −30% после 19:00».
Сущность
TimeTariff:
| Поле | Тип | Описание |
|---|---|---|
id | UUID | |
franchise_id | UUID | Тенант |
name | string ≤100 | Видимое имя |
discount_pct | DECIMAL(5,2) | (0, 100] |
days_of_week | int[] | ISO 1=Пн..7=Вс |
time_from | TIME | Старт окна |
time_to | TIME | Конец окна; должно быть time_from < time_to (межсутки не поддерживаем) |
scope | enum | all / categories / products |
target_ids | UUID[] | NULL когда scope=all |
store_ids | UUID[] | На какие ТТ распространяется (≥1) |
status | enum | active / disabled |
created_at, updated_at | TIMESTAMP |
Бизнес-правила
- 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 как-есть |
| Инвойс PayKeeper | Cart инвойса формируется из 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 видит цены]
Ролевая матрица
| Роль | List | View | CRUD |
|---|---|---|---|
| Владелец франшизы | ✅ все ТТ | ✅ | ✅ все ТТ |
| Владелец партнёра | ✅ свои ТТ | ✅ свои | ✅ свои ТТ |
| Менеджер ТТ | ❌ (read-only через POS-меню) | — | — |
| Кассир | — | — | — |
Permissions (переиспользуем):
menu.read— GET/api/v1/time-tariffsmenu.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) — отдельный сервис.
Ссылки
- Loyalty Programs (drawio)
- Catalog Service API
- Каталог — раздел «Комбо/бизнес-ланчи»