Перейти к содержанию

Release Notes

История ключевых возможностей модуля CM Ocean Loyalty по доменам. Изменения фиксируются в формате Keep a Changelog.

Версионирование — календарное (CalVer) в формате YY.x.y, единое для продуктов CM Ocean:

  • YY — две последние цифры года выпуска;
  • x — порядковый номер функционального выпуска в течение года (новая функциональность);
  • y — номер хотфикса в рамках функционального выпуска (исправления без новой функциональности).

Релиз-кандидаты обозначаются суффиксом -rc.N (например, 26.0.0-rc.1). Несовместимые изменения помечаются меткой BREAKING.

Как читать

Раздел ниже отражает текущее состояние сервиса биллинга баллов (loyalty-billing). Возможности, ещё не реализованные, явно вынесены в раздел «В дорожной карте».

26.3.0 (в разработке)

Версия ещё не выпущена

Раздел описывает функциональность, накопленную в основной ветке репозиториев loyalty, loyalty-ui и loyalty-requirements после релиза 26.2.0, — по состоянию на 30.09.2026. Текущий публичный релиз — 26.2.0; следующая фича-версия — 26.3.0, дата выпуска не объявлена.

Крупный функциональный выпуск. Главное: конфигурации программы лояльности (уровни, акции программы и уровней, правила списания), переработанная форма акции с условиями из реестра («Настройка акции»), согласованием запуска и настройками начисляемых баллов, каталог клиентов и теги, карточка клиента по версионируемой настройке, субсчета баллов, срок жизни баллов по цепочке и сгорание по срокам, ручные операции, справочник валют и пары обмена, раздел «Отчёты». Кроме того — сегменты клиентов из системы кампаний, справочники городов, торговых точек, мерчантов и каталог товаров, возврат по чеку, очередь исходящих уведомлений, реферальная программа, достижения, мобильный кошелёк, очередь приёма команд, журнал аудита с защитой от подделки и передачей в SIEM, обмен данными с внешними системами. Выпуск содержит много BREAKING-изменений контракта API и ролевой модели, а также изменения схемы БД, которые требуют эксплуатационных шагов при обновлении контуров с данными, — см. «Совместимость и обновление» ниже.

Состав релиза по доменам

Домен Что входит в выпуск
Акции Переработанный каталог (серверные поиск/фильтры/сортировка, массовые действия, колонки статистики и статуса согласования), форма акции с вкладками «Общие данные», «Механика», «Условия», «Ограничения», тип события и настройки начисляемых баллов, условия из реестра условий (товары, место покупки, мерчант, канал, сегменты, сумма чека, клиент, теги, участие в акциях, дополнительные условия), метки акций, ограничения и лимиты, механики на справочнике параметров, согласование запуска и отдельное право запуска, автоведение по периоду (статус «Запланирована»), удаление и копия акции, предпросмотр импорта
Настройки программы Конфигурации программы вместо экрана «Уровни»: уровни в составе конфигурации, акции программы и уровней, правила списания, механика перехода и режим пересчёта уровня, активация с историей; уровень клиента хранится и считается по журналу покупок, понижается на пересмотре в личную дату клиента
Клиенты Каталог клиентов (поиск, фильтры, массовые операции, импорт из CSV), теги клиента, карточка клиента по версионируемой настройке, вкладки «Участие в акциях» и «Сроки сгорания», часовой пояс клиента, история смены статуса (API), мягкое удаление, обезличивание, валидация телефона/email
Баллы и операции Ручные операции (включая теги по списку клиентов), субсчета баллов с базовым субсчётом и областью списания, срок жизни баллов по цепочке и сгорание по срокам с предупреждениями по шагам, справочник валют и пары обмена, счета во всех включённых валютах, доступный остаток без баллов с наступившим сроком, резерв со сроком по умолчанию, кеш баланса в Redis
Вовлечённость Реферальная программа, достижения и бейджи, карта в мобильном кошельке, улучшения личного кабинета (B2C): витрина акций совпадает с движком начисления, описание операции, staff-предпросмотр, ограничение частоты
Партнёрские предложения Справочник мерчантов вместо MID/MCC в карточке партнёра, запрет удаления денежной истории, отчёт взаиморасчётов по окну, номинал вознаграждения по точности валюты, код оффера из последовательности БД, точное время периода
Администрирование Раскрывающееся меню «Администрирование», экраны «Настройка акции», «Профиль клиента», «Валюты», «Сегменты клиентов», справочники городов, торговых точек, мерчантов и каталог товаров (API), очередь приёма, журнал аудита (обращения к ПДн, доменные события, защита от подделки, SIEM), перенос справочников и механик с проверками админки, часовой пояс программы на сервере
Отчёты и мониторинг Раздел «Отчёты» (обзор, баллы, уровни, акции с выгрузкой CSV, рефералы), произвольный период и плитка «Сгорело» в мониторинге, сгорание и сторно не считаются тратой клиента, настоящий total в списках, оповещения Prometheus
Платформа и интеграции Асинхронный приём команд через очередь и Kafka, возврат по чеку POS, обязательный код торговой точки и переменные покупки, приём сегментов из системы кампаний, исходящие события (смена уровня, уведомления клиентам), обмен данными с внешними системами, сплошное ограничение частоты и проверка загружаемых файлов, сверка итогов и контроль целостности учёта, SBOM backend и веб-интерфейса

Новое в 26.3.0

Акции. Подробности — Каталог акций, Создание и редактирование акции, Условия акции, Ограничения и лимиты акции, Механики начисления, Жизненный цикл и арбитраж, Метки акций, Настройка акции.

  • Каталог акций переработан: серверные поиск, множественные фильтры и сортировка по всей базе, настраиваемые колонки, выбор строк с панелью массовых действий, отчёт частичного успеха. Новые колонки «Применений», «Начислено» и «Расход бюджета» (скрыты по умолчанию) и «Статус согласования» с фильтром и сортировкой; строка GET /v1/promotions несёт applications, bonusMinor (по версии) и budgetSpentMinor (по линии версий). Пока выполняется массовая операция, открыто окно «Выполняется операция».
  • Форма акции переработана. Вкладки — «Общие данные», «Механика», «Условия» и «Ограничения». В шапке — название, код, «Версия: N», статус и панель действий: «Создать новую версию», «Запустить», «Отменить запуск», «Приостановить», «Сохранить как…», «Архивировать»/«Разархивировать» и «Удалить»; каждое действие подтверждается отдельным окном. Справа — колонка «Расход бюджета» и чек-лист «Превью акции» («Готово к запуску: N из 6»). Кнопка «Сохранить черновик» теперь называется «Сохранить». Плашка режима чтения зависит от состояния акции: архивная, завершённая или запущенная.
  • Тип события акции — поле «Тип события» (eventType) из справочника event-type, по умолчанию «Покупка». Код вне справочника — 422 promotion_event_type_invalid. Движок поле пока не читает: акции применяются к событиям покупки.
  • Начисляемые баллы акции: «Срок жизни начисляемых баллов» (pointsLifetimeDays, 1–36 500 дней) и «Тип начисляемых баллов» (pointsSubaccountCode, по умолчанию «Акционные баллы»). Пустой срок берётся по цепочке «субсчёт → уровень клиента → валюта баллов», действующий срок показан серым в пустом поле. У механики, которая не начисляет баллы, настройки недоступны (422 promotion_points_settings_not_applicable).
  • Условия акции из реестра. Вкладка «Условия» строится по экрану «Настройка акции» и делится на зоны «Акция применяется, если» и «Не применяется, если». Коробочные условия: «Категории товаров», «Артикулы (SKU)», «Регион», «Город», «Магазин» (по коду торговой точки из покупки), «Мерчант» (по MID), «Канал продаж», «Сегменты клиентов», «Сумма чека» (перенесена из ограничений и доступна в обеих зонах), статус, уровень и баланс клиента, «Теги клиента», «Участие в акциях». Под подзаголовком «Дополнительные условия» — условия внедрения на переменную покупки или поле профиля клиента. Скрытое в настройке условие продолжает действовать в своих акциях (серая строка «Скрыто в настройке»), «Сохранить как» его не переносит. Значения из справочников показываются с пояснением записи: путь категории, город магазина, партнёр мерчанта. Окно подтверждения запуска предупреждает, если переменная дополнительного условия не приходила в покупках за последние 7 дней.
  • BREAKING. Условия акции передаются списком criteria ({code, mode, values | range}, режимы INCLUDE/EXCLUDE) вместо scope; границы суммы чека больше не читаются из params.minCheckMinor/maxCheckMinor, код invalid_check_range заменён на promotion_criterion_range_invalid, добавлены коды promotion_criterion_*. Файлы переноса со старым scope импортируются с конвертацией.
  • BREAKING. Условие «Уровень клиента» — список пар params.conditions.client.levels [{configId, code}] вместо строки level: прежний ключ — 400 validation_failed, неизвестная конфигурация или уровень (в том числе при импорте) — 422 promotion_level_unknown. Условие по уровням черновой конфигурации действует только на клиентов этой конфигурации.
  • Условия аудитории — «Теги клиента» и «Участие в акциях» (по меткам, конкретным акциям и самой акции — «Эта акция») в обеих зонах, с порогом числа участий и периодом. Удалённые значения в условиях — битые ссылки: черновик сохранить можно, запустить акцию нельзя (400 promotion_audience_unresolved). Условие «Сегменты клиентов» сверяется только с составом, залитым из системы кампаний; удалённый сегмент — тоже битая ссылка (kind: SEGMENT), а сегмент из условий работающей акции удалить нельзя (409 segment_in_use). Та же логика доступна интеграциям — POST /v1/integration/rtflow/condition-checks, включая тип условия CUSTOMER_SEGMENT.
  • Механики — на справочнике параметров. Для новой акции форма предлагает только механики с заведёнными параметрами; размер награды задаётся параметром механики, поле value не используется. Черновик на механике без параметров не запускается (409 promotion_mechanic_not_configured). У «Кэшбэка» удалены параметры «Макс. бонус», «Мин. сумма чека» и «Сегменты клиентов»; у «1+1=3» параметры задаются фразой, добавлена «Стоимость дополнительной единицы».
  • BREAKING. Награда «Множителя», «Подарка» и «Начисления за единицу» обязательна: без multiplier, giftMinor или pointsPerUnit — 422 promotion_param_required. Потолок бонуса — только поле акции maxBonusMinor, параметр params.maxBonusMinor больше не читается. Существующие акции миграция переводит без изменения начисления.
  • Согласование запуска по принципу «четырёх глаз» — включается настройкой развёртывания loyalty.promotion.approval.enabled вместе с признаком интерфейса PROMOTION_APPROVAL_ENABLED. Вкладка «Согласование» на странице акции: отправка, отмена отправки, согласование и отклонение, история. Статусы — «В работе», «На согласовании», «Согласована», «Отклонена»; содержательная правка сбрасывает согласование. Согласует сотрудник с правом LOYALTY_PROMOTION_APPROVE, не автор отправки. Новая версия и копия акции согласуются заново.
  • BREAKING. Запуск акции (activate, bulk-activate) требует отдельного права LOYALTY_PROMOTION_ACTIVATE, даже если согласование выключено: права LOYALTY_PROMOTION_WRITE больше недостаточно (403 access_denied).
  • Ограничения и лимиты акции: общий лимит участий, лимит участий на клиента с окном «За всё время акции», «По календарю» (день, неделя, месяц) или «За последние N дней» (N ≥ 2), бюджет как полноценное ограничение с возвратом в бюджет по сторно (в том числе при возврате покупки по чеку). Все ограничения передаются одним запросом вместе с акцией; «Сумма чека» переехала в условия, поэтому ограничений четыре. Календарные границы считаются в часовом поясе программы.
  • Лимит участий принимает только окна, которые показывает форма: единица «год» — 422 invalid_period, календарный период из нескольких единиц и скользящее окно не в днях или короче 2 дней — 422 invalid_participation_window. Уже сохранённые окна продолжают считаться.
  • BREAKING. budgetMinor акции — деньги, а не баллы; каждое начисление пересчитывается по курсу валюты баллов программы (points.currency) на момент начисления, настройка loyalty.promotion.budget.point-currency удалена. Бюджет в форме можно ввести в баллах: при сохранении он пересчитывается в деньги по текущему курсу, рядом — подсказка «по текущему курсу ≈ N».
  • BREAKING. Приоритет акции (priority) стал обязательным, как name, mechanicCode, startsAt и endsAt: без любого из них POST/PUT /v1/promotions отвечает 400 validation_failed; значение приоритета по умолчанию 100 больше не подставляется.
  • BREAKING. Тело акции — полная замена: отсутствующий participationLimit в PUT снимает лимит участий (как и отсутствующий бюджет). Исключения — criteria, audience, eventType и ключ levels условия «Уровень клиента»: отсутствующее поле сохранённое значение не меняет.
  • BREAKING. Правка акции на месте зависит от факта применения, а не от статуса: приостановленная акция, по которой уже было хотя бы одно начисление, правится только новой версией (409 promotion_not_editable).
  • BREAKING. Статусная модель акции изменена: статус ARCHIVED убран из перечисления (архив — отдельный обратимый признак archived), добавлен статус SCHEDULED. Фильтр status стал множественным; createdBy/updatedBy стали объектами {id, displayName}; массовые запросы каталога акций принимают не больше 500 id за раз.
  • Метки акций — новый справочник со своим экраном (переименование с контролем версии, слияние синонимов, массовое удаление, перенос бандлом); поле «Метки» в карточке акции правится в любом статусе, массовое проставление/снятие в каталоге — не больше 200 меток в пакете (400 bulk_size_exceeded), метка, удалённая или слитая во время операции, больше не теряется молча. Новые роли LOYALTY_PROMOTION_LABEL_READ / WRITE.
  • Новый статус «Запланирована» и автоведение акции по периоду (включается настройкой) — акция сама переходит в «Активна» по дате начала и в «Завершена» по дате окончания.
  • Удаление акции (черновик и запланированная) и копия «Сохранить как» с нулевой статистикой. Акцию, которая входит в программу лояльности, удалить нельзя: 422 promotion_bound_to_config с названиями конфигураций, признак boundToConfig, кнопка «Удалить» недоступна с пояснением.
  • Завершённая акция новой версии не получает — только копией (409 promotion_not_revisable).
  • Уникальность названия акции между акциями (409 promotion_name_taken).
  • Точное время начала и конца периода акции (обе границы включительны, по умолчанию 00:00/23:59) — в часовом поясе программы.
  • Мастер импорта акций/механик/справочников с предпросмотром (dryRun=true).
  • Механики «Множитель» и «Подарок» начали начислять (ранее давали нулевой бонус молча); механика «бонус за натуральный объём» заработала на кассовом канале POS.
  • В API акции добавлено поле pointsRedemptionScope — область списания баллов, начисленных акцией (товары, категории, мерчанты, торговые точки). Сервер проверяет и хранит её и при начислении записывает на партию баллов (422 promotion_redemption_scope_invalid для пустых, повторённых значений и больше 1000 значений), но при списании пока не проверяет. В форме акции поля нет.

Настройки программы. Подробности — Конфигурации программы, Уровни (тиры), Операционная конфигурация.

  • Конфигурации программы лояльности. Раздел «Конфигурации программы» (право LOYALTY_PROGRAM_CONFIG_READ) заменил экран «Уровни»: каталог с поиском, фильтрами по дате и автору («ФИО (логин)»), колонками «Как считается уровень» и «Механика» (диапазон ставок акций программы); мастер создания из четырёх шагов («Основное», «Акции программы», «Правила списания», «Уровни»); карточка конфигурации, панель «Как это сработает» с примером клиента и сводка «Как живут баллы». Действует ровно одна конфигурация; активная правится через дублирование и активацию копии.
  • Активация и история активаций. Отдельное право LOYALTY_PROGRAM_CONFIG_ACTIVATE, флажок «Отправлять клиентам сообщения о смене уровня», неизменяемый журнал активаций со снимком настроек и уровней. Новые клиенты сразу попадают на новые правила, текущие переходят при первой покупке или списании либо в дату своего пересмотра. Черновик, на котором ещё живут клиенты (customersCount), открывается только для просмотра, удалить его нельзя (config_has_customers).
  • Акции программы и акции уровней. К конфигурации привязываются акции базового начисления; каждому уровню назначаются свои «Акции начисления». Ставку начисления уровня задают акции, а не поле уровня.
  • Правила списания в конфигурации: область списания (любые покупки, покупки от суммы, товары или категории из списка), максимум за покупку (единый или свой на уровне), минимальное списание, минимальный баланс, кратность. Списание и резерв проверяются по правилам активной конфигурации и отклоняются своим кодом (redemption_below_min_balance, redemption_scope_not_eligible, redemption_not_multiple, redemption_items_required, redemption_purchase_amount_required); в hold/redeem появилось необязательное items[]. Если у конфигурации нет своих правил, действует общая политика списания раздела «Настройка ПЛ».
  • Механика перехода и режим пересчёта уровней. Уровень считается по сумме, количеству или сумме и количеству покупок (tierMetric, второй порог — entryCount). Период — скользящее окно с пересмотром раз в заданный интервал либо персональный период с переносом излишка (tierRecalc). Отсутствующие в запросе tierMetric и tierRecalc сбрасываются на значения по умолчанию («сумма покупок», скользящее окно) — интеграциям нужно передавать их при каждом сохранении. Свой срок жизни баллов у уровня необязателен: пустой срок берётся из валюты баллов.
  • Уровень клиента хранится и считается по журналу покупок. Помесячного сброса больше нет. Повышение — сразу при покупке, один раз на чек после акций; понижение — на пересмотре в личную дату клиента (LOYALTY_TIER_REVIEW_CRON, по умолчанию выключен) или при переносе на новую активную конфигурацию («Перенос системой»). Возврат по чеку уменьшает накопление, но уровень сразу не понижает. Ручное назначение работает как «пол» до ближайшего пересмотра.
  • BREAKING. Уровни записываются только в составе конфигурации (POST/PATCH /v1/program-configs): PUT/DELETE /v1/tiers/{code} и код tier_ordinal_taken удалены без периода депрекации. GET /v1/tiers остался только для чтения и принимает configId. У уровня нет поля metric (механика перехода принадлежит конфигурации), появился порог entryCount.
  • Статус уровня клиента (GET /v1/customers/{externalId}/tier) отдаёт assignedAt, nextReviewAt, periodEndsAt, config и вторую ось накопления (accumulatedCount, toNextCount). Вкладка «История уровней» карточки показывает источник, сроки и конфигурацию клиента.
  • BREAKING. История переходов уровня (GET /v1/customers/{externalId}/tier/transitions) отдаётся постранично ({items, page, size, total}), параметр limit удалён. В записи — названия уровней на момент перехода (fromName/toName), количество покупок (accumulatedCount), конфигурация, оператор и причина ручного перехода, источник source: «При покупке», «На пересмотре», «Вручную», «Перенос системой». Ручное назначение, массовый перевод и снятие назначения тоже попадают в историю.
  • Событие о смене уровня tiers.customer.tier-changed через очередь исходящих: доставка «не менее одного раза» с eventId, отправка выключена по умолчанию (LOYALTY_TIER_EVENTS_ENABLED), событие, не отправленное дольше 7 дней, списывается.
  • «Настройка ПЛ» содержит ограничения на списание, лимиты по разрезам и глобальную механику; блок «Валюты и баллы» убран — балльные валюты ведутся в «Администрирование → Валюты».

Клиенты. Подробности — Каталог клиентов, Карточка клиента, Теги клиента, Настройка карточки клиента.

  • Каталог клиентов — серверные поиск, мультизначные фильтры (статус, уровень, теги), период регистрации, серверная сортировка; массовые операции (блокировка, разблокировка, архивирование, удаление) из меню строки или панели выделения; импорт клиентов из CSV с предварительной проверкой. Фильтр и колонка «Уровень» показывают последний посчитанный уровень клиента (в том числе уровень прежней конфигурации), колонка «Сегменты» — названия сегментов. Форма «Новый клиент» больше не содержит выбора бонусных счетов.
  • BREAKING. Параметр фильтра по уровню переименован tierCode → tier, стал списком кодов; тумблер «Показать архивные» теперь возвращает всю базу с архивными первым блоком.
  • Теги клиента стали справочником: собственный экран с переименованием (контроль версии), слиянием синонимов, массовым удалением с блокировкой по условиям работающих акций; присвоение и снятие тегов клиенту (одиночно, до 10 000 клиентов из каталога и пакетом по списку на экране «Ручные операции»). Действие каталога «Удалить теги» переименовано в «Снять теги». Тег, удалённый между выбором и записью, отклоняет запрос 422 unknown_tag, а не выпадает молча. Новые роли LOYALTY_TAG_READ, LOYALTY_TAG_WRITE, LOYALTY_CUSTOMER_TAG_WRITE.
  • Карточка клиента собирается по активной настройке конфигуратора «Настройка профиля клиента»: слева — карточки с полями (по умолчанию «Клиент» и «Баллы и уровень»), справа — разделы «Баллы и уровень», «Клиент», «Активность и коммуникации» с вкладками продукта и таблицами данных из источников. Новые поля: «Сегменты» (из системы кампаний, с датой входа), «Дата рождения», «Часовой пояс» (только чтение, с подсказкой источника пояса).
  • Вкладка «Участие в акциях»: дата, акция (ссылка на карточку акции), механика, бонус и ID транзакции; фильтр по механике и сортировка по дате. Сторно участием не считается.
  • BREAKING. Значения атрибутов карточки — списки строк: attributes в GET /v1/customers/{externalId}/card и тело PUT /v1/customers/{externalId}/attributes перешли с «ключ → строка» на «ключ → список строк». Значение проверяется по формату типа поля (400 validation_failed); запись в неизвестное, преднастроенное, привязанное к источнику, архивное или скрытое поле отклоняется (unknown_attribute, system_field_immutable, field_archived, field_hidden), больше одного значения без множественного выбора — attribute_not_multiple.
  • BREAKING. Сегменты клиента берутся только из состава, залитого системой кампаний: поле segments удалено из профиля MDM (identity) в карточке и ответе записи профиля; сегменты клиента — GET /v1/customers/{externalId}/segments. Обезличивание SEGMENTS и удаление клиента выводят его из всех сегментов.
  • Карточка в API отдаёт теги (tags), флаг архива (archived) и дату рождения (identity.birthDate); PUT /v1/customers/{externalId}/profile принимает birthDate, city и timezone (IANA, иначе 422 customer_timezone_invalid). Пояс клиента определяется по первому доступному источнику: пояс профиля → город профиля → частый город покупок → пояс программы.
  • История смены статуса клиента: блокировка, разблокировка, удаление и слияние записываются с автором (у слияния — с причиной); чтение — GET /v1/customers/{externalId}/status-history. Блокировка больше не «оживляет» клиента, удалённого одновременно с ней (404 customer_not_found).
  • Слияние клиентов переносит выжившему журнал покупок и возвратов и пересчитывает его уровень. Конфликт merge_account_conflict — только если операции в одной валюте были у обоих клиентов. BREAKING: слияние уже слитого в другого клиента источника отвечает 409 version_conflict, удалённого — 404 customer_not_found вместо прежнего 200.
  • Карточка клиента (бывший «Профиль участника») получила мягкое удаление (DELETE /v1/customers/{externalId}), обезличивание персональных данных, валидацию и нормализацию телефона и email.
  • BREAKING. Недоступность внешнего реестра клиентов (MDM) — 503 mdm_unavailable вместо 404 customer_not_found.

Баллы и операции. Подробности — Ручные операции, Субсчета баллов, Справочник валют программы, Сгорание баллов, Баланс и операции.

  • Субсчета баллов — баланс клиента разбит на целевые счета (приветственные, партнёрские, акционные) со своим правилом сгорания и местом в очереди списания; списание может затронуть несколько субсчетов, сторно возвращает баллы на исходный. В каждой балльной валюте есть субсчёт «Акционные баллы» (PROMO); рядом с валютой субсчёта показан её курс со ссылкой «Изменить курс». Закрыть субсчёт, в который начисляют незавершённые акции, нельзя (409 subaccount_used_by_promotions).
  • Базовый субсчёт (BASE, «Основной») закреплён за валютой: его нельзя переназначить или закрыть, своего срока и области списания у него нет. BREAKING: признак — isBase вместо isDefault, операция POST /v1/points-subaccounts/{id}/default удалена, отказ закрытия — 409 subaccount_base_cannot_close.
  • BREAKING. Режим сгорания субсчёта без своего срока называется «Общий срок»: баллы сгорают по сроку уровня клиента или по базовому сроку валюты, а не хранятся бессрочно. Срок «На дату» задаётся датой и временем («Действуют до», по умолчанию 23:59:59): поле expiryDate заменено на expiryAt. Период в ответе приходит в днях (P30D).
  • Область списания субсчёта: «Товары», «Категории», «Мерчанты», «Торговые точки» из справочников. Действует при списании за покупку: резерв, списание и команды HOLD/REDEEM принимают storeCode, merchantCode и позиции items.
  • Срок жизни баллов по цепочке: срок начисления (акция или запрос) → своё правило субсчёта → срок уровня клиента в его конфигурации → базовый срок валюты → не сгорают. Срок и его источник (expirySource) фиксируются в партии и не пересчитываются. У балльной валюты — базовый срок «Срок жизни баллов, дней» (pointsLifetimeDays, 1–3650, по умолчанию 365).
  • Сгорание по сроку. Баллы с наступившим сроком недоступны сразу, а не с ночного прогона. Прогон сгорания идёт пачками (LOYALTY_EXPIRY_BATCH_SIZE), каждый счёт — в своей транзакции, и сжигает свободный остаток: резервы, созданные до срока, сохраняют свои баллы. Добавлены пробный прогон ?dryRun=true, запись в истории по каждому субсчёту и сообщение клиенту о факте сгорания. Ручное сгорание оператором пишется с источником MANUAL_EXPIRY («Сгорание оператором») и не считается тратой клиента.
  • Предупреждения о скором сгорании по шагам (LOYALTY_EXPIRY_NOTIFY_BEFORE, например P30D,P7D,P1D): по каждой партии и шагу — одно предупреждение с текстом «Через 7 дней…», «Завтра…», «Сегодня…»; дата указывается в часовом поясе программы, сообщения идут через очередь исходящих уведомлений. Ручной запуск — POST /v1/maintenance/points/expiry-notice.
  • В карточке клиента — вкладка «Сроки сгорания»: строка «Ближайшее сгорание» и партии баллов от ближайшего срока с источником срока (GET /v1/customers/{externalId}/points-lots). В балансе заполняются nearestExpiryAt и nearestExpiryAmountMinor (раньше поле срока всегда было пустым).
  • Доступный остаток считается как max(0, баланс − резервы − баллы с наступившим сроком + покрытое): резерв, созданный после срока партии, больше не обещает те же баллы дважды. Та же формула — в корректировке оператора и проверке пакета ручных операций: строка, готовая по проверке, не отказывает на выполнении из-за баллов с наступившим сроком.
  • Счета во всех включённых валютах: при заведении клиента и фоновой задачей при создании или включении валюты; разблокировка открывает пропущенные счета, ещё не открытый счёт показывается строкой с нулями. Поле preCreateAccounts устарело и игнорируется. Счёт без проводок больше не мешает удалить валюту.
  • Вкладка «Счета и балансы»: при двух субсчетах и больше — плитки «Доступно — <субсчёт>»; счета во всех включённых валютах, включая денежные; суммы — с точностью своей валюты.
  • Вкладка «Бонусные операции»: фильтры по периоду, типу операции, сумме «от — до» и источнику, сортировка по дате, сторно со знаком «−». В API GET /v1/customers/{externalId}/operations — также отбор по признаку акции и коду механики, в строке — subaccount и accountNumber.
  • Ручные операции с баллами — новый экран, мастер из трёх шагов (список клиентов → операция → результат): начисление, корректировка, перевод уровня, «Добавить теги» и «Снять теги»; пакет по списку или файлу (до 20 МБ, 1 000 000 строк), асинхронное выполнение с ходом на экране и CSV-отчётом, отложенный запуск. Экран открывается и по одной роли LOYALTY_CUSTOMER_TAG_WRITE — тогда доступны только операции с тегами.
  • BREAKING. Причина ручной операции — код справочника «Причины операций» плюс комментарий (обязателен при причине «Прочее»); свободный текст reason убран. Роль LOYALTY_CORRECTION_OTHER_WRITE переименована в LOYALTY_OPERATION_REASON_OTHER_WRITE; операторам начисления, корректировки и перевода уровня нужна роль LOYALTY_DICTIONARY_READ (справочник причин).
  • BREAKING. Единая форма ответа массовых операций bulk-accruals / bulk-corrections / tier-bulk: {batchId, status, total, succeeded[], errors[]}. Поле source убрано и из тел запроса этих трёх точек — источник операции сервер определяет сам.
  • Предел массовых операций поднят до 1000 идентификаторов в одном запросе.
  • Справочник валют программы — деньги и баллы одной записью с типом, курс балльной валюты с историей, ровно одна базовая денежная валюта; экран «Администрирование → Валюты» с вкладками «Валюты» и «Обмен валют». BREAKING: в GET /v1/currencies появилось поле kind, display.symbol может быть null, неизвестный код валюты — 404 вместо 500; прежний /v1/admin/point-currencies работает поверх справочника: color и sortOrder не сохраняются, удаление включённой валюты — 409 currency_enabled, валюты со ссылками — 409 currency_in_use. Начисление в выключенную валюту отклоняется.
  • Пары обмена между балльными валютами (/v1/admin/exchange-pairs, сводка и базовый курс направления): режимы курса, округление, остаток, направление и ограничения. Операция обмена пары пока не применяет.
  • BREAKING. У резерва баллов (hold) всегда есть срок: без expireAt действует срок по умолчанию (redemption.hold-ttl, 30 минут), просроченные резервы снимаются автоматически.
  • Покупка считается по чеку, а не по его позициям: чек из десяти товаров увеличивает счётчик покупок клиента на один. Сумма покупки сохраняется и связывается с начислениями; уже завышенные счётчики не пересчитываются.
  • Границы лимита списания округляются внутрь допустимого промежутка (было — обе «к ближайшему»).
  • Кеш баланса в Redis (выключен по умолчанию) — ускоряет чтение под нагрузкой, не меняя контракт.
  • Срок и источник срока партии баллов согласованы и на уровне базы: у партии «не сгорают» срока нет, у остальных источников он есть. Партия, где это нарушено ручной правкой данных, читается с ошибкой 500 lot_expiry_inconsistent, в том числе при списании.
  • Без Kafka неисполненные части пакета ручных операций после перезапуска пода публикуются заново: брошенный пакет подхватывается на старте и раз в минуту, если 5 минут от него нет признаков жизни; повторной проводки строк нет.

Вовлечённость. Подробности — Реферальная программа, Достижения и бейджи, Карта в мобильном кошельке, Личный кабинет клиента (B2C).

  • Реферальная программа — реферальный код клиента, связь «реферер → реферал» при регистрации, активация первой покупкой приглашённого, дуальное вознаграждение механикой «Реферальная». Оба начисления получают «Срок жизни начисляемых баллов» и «Тип начисляемых баллов» реферальной акции.
  • Достижения и бейджи — маркетолог настраивает правила «условие → бейдж»; клиент видит бейджи в карточке и в личном кабинете. Роли LOYALTY_ACHIEVEMENT_READ/WRITE заведены в Keycloak и входят в группу LOYALTY_ADMINS. Чек из кассы считается одной покупкой на сумму документа; сторно и ручное сгорание не увеличивают показатель «Списано баллов».
  • Карта участника в мобильном кошельке — Apple Wallet и Google Кошелёк; функция поставляется выключенной до настройки сертификата/ключа подписи заказчика (loyalty.wallet.enabled).
  • Личный кабинет (B2C): вкладка «Акции» показывает ровно те акции, которые применит движок, — условия, известные до покупки, проверяются общим правилом начисления, акция с незнакомым условием или удалённым сегментом не показывается. Описание операции из reason интеграционных начислений в истории, явный staff-предпросмотр по previewExternalId, ограничение частоты запросов кабинета (429 b2c_rate_limited).

Партнёрские предложения. Подробности — Партнёрские предложения, Партнёрские предложения и взаиморасчёты.

  • BREAKING. Поля merchantIds/mcc удалены из контракта партнёра (POST/PUT /v1/partners). MID и MCC ведутся в справочнике мерчантов /v1/admin/merchants (права LOYALTY_MERCHANT_READ/WRITE); существующие MID перенесены миграцией. Транзакция находит партнёра по MID через справочник, по MCC — только если MCC однозначно указывает на одного партнёра.
  • Денежная история партнёра больше не удаляется: оффер с атрибуциями удалить нельзя — 409 partner_offer_in_use (только архивировать), партнёра с проводками по счёту финансирования или с мерчантами — 409 partner_in_use.
  • Отчёт о взаиморасчётах больше не смешивает периоды: окно отчёта возвращается в ответе (from/to), добавлено fundedInPeriodMinor — финансирование за окно; fundedTotalMinor по-прежнему считается за всё время.
  • Номинал вознаграждения POINTS/FIXED переводится в минорные единицы по точности валюты баллов, а не умножением на 100; вознаграждение округляется HALF_UP, как начисление по механикам акций.
  • Сегмент аудитории оффера выбирается только из справочника сегментов, неизвестный код — 422 partner_offer_segment_not_found.
  • Код оффера — из последовательности БД (PO-0000001), исправляет редкую коллизию кодов на старой формуле; точное время начала/конца периода оффера. Период вводится и показывается в часовом поясе программы (подсказка «Часовой пояс: UTC+…»), прошедшие дни при создании выбрать нельзя.

Администрирование. Подробности — Единое окно и навигация, Настройка акции, Настройка карточки клиента, Справочники (НСИ), Очередь приёма, Журнал аудита, Справочники и НСИ.

  • Меню. «Администрирование» — раскрывающийся пункт левого меню с десятью подпунктами (в Core UI — подменю хоста, в автономном режиме — подменю оболочки), в том числе новые «Валюты» и «Настройка акции»; горизонтальных вкладок у экрана больше нет. Адрес /loyalty/admin без раздела и устаревшие ссылки ведут в «Справочники». В меню модуля появились «Конфигурации программы» и «Отчёты», раздела «Уровни» больше нет.
  • «Настройка акции» — реестр условий акции: администратор скрывает ненужные коробочные условия, меняет их порядок перетаскиванием и добавляет свои — на переменную из покупки или на поле профиля клиента, со способом «Из справочника», «Ручной ввод» или «Диапазон». Под названием переменной — статистика за 7 дней. Условие, заполненное в работающих акциях, удалить нельзя — откроется окно со списком акций. Правки сохраняются одним запросом с защитой от одновременной правки.
  • «Профиль клиента» (в автономном режиме — «Настройка профиля клиента») — конфигуратор карточки клиента: настройка версионируется целиком (черновики, копия, сравнение с активной версией, активация отдельным правом LOYALTY_CLIENT_PROFILE_ACTIVATE), вкладка «Структура профиля клиента» задаёт карточки левой колонки, разделы и таблицы данных из таблиц-источников, вкладка «Атрибуты» — поля с типами «Строка», «Число», «Дата», «Дата со временем», «Логический», выпадающими списками из справочника, множественным выбором, колонкой источника, архивацией и пакетным удалением; есть предпросмотр карточки. Удаление версий — только через API (право LOYALTY_CLIENT_PROFILE_DELETE).
  • BREAKING. API конфигуратора карточки: операции над содержимым требуют configurationId, изменяющие — ещё If-Match с ревизией версии; fieldType: LIST отклоняется (выпадающий список — inputSource: DROPDOWN + refDict); ключ нового поля проверяется по формату ^[a-z_][a-z0-9_]*$ (до 64 символов), а не исправляется, и после создания не меняется (field_key_immutable); удалить можно только незаполненное поле (field_has_values), заполненное архивируется; поле, на котором построено условие акции, не удаляется (profile_field_used_in_criterion).
  • Сегменты клиентов — собственный экран в реестре справочников с вкладками «Сегменты» и «Заливки»: сегмент можно архивировать, вернуть из архива и удалить, если он не стоит в действующих акциях. Сегменты заводит только внешняя система кампаний.
  • Собственные справочники продукта: категории товаров и SKU — каталог с деревом категорий до трёх уровней и CSV-импортом (/v1/admin/product-categories, /v1/admin/product-items, роли LOYALTY_PRODUCT_CATALOG_*) вместо внешнего dictionary-api; города (регион и часовой пояс), торговые точки (город и адрес) и мерчанты (MID, партнёр, MCC, город) — свой API и пара ролей у каждого. Их значения используются в условиях акции; экрана в интерфейсе у этих справочников нет.
  • BREAKING. Общие ручки /v1/admin/dictionaries/{key}/... по ключам customer-segment, product-category и sku отвечают 409 dictionary_own_screen — эти справочники ведутся своим API.
  • Справочники: запрет правки записи в статусе «Архив» без предварительного восстановления, защита от удаления записи, на которую ссылаются акции, — теперь и для типов событий, регионов, каналов и записей, выбранных в дополнительных условиях акции (признаки usedByPromotions и deletable у всех записей общего экрана). Перенос справочников и механик между контурами с предпросмотром (dryRun=true) проверяет записи и механики так же, как админка.
  • Очередь приёма — экран для разбора застрявших асинхронных команд начисления/списания/резерва: статусы, карточка команды, возврат в работу, отмена (в т. ч. ещё не наступившей отложенной команды). Роли LOYALTY_INTAKE_READ/WRITE.
  • Журнал аудита — отдельный экран: изменяющие вызовы и обращения к персональным данным (просмотр карточки, поиск, выгрузка) с ролями субъекта на момент действия и источником запроса; доменные события — согласование запуска акции (PROMOTION_APPROVAL_*), смена курса валюты (currency.rate.change) и пары обмена (exchange-pair.rate.change); выгрузка за период потоком в CSV; защита от подделки задним числом (цепочка печатей, выключена по умолчанию; проверка целостности показывает и часть журнала, ещё не покрытую печатями) и опциональная передача потока в SIEM заказчика по syslog/CEF. Фильтр «Время» отбирает записи по суткам в часовом поясе программы. Общая роль LOYALTY_MONITORING_READ.
  • Часовой пояс программы применяет сервер: в нём режутся календарные границы лимитов, итогов и отчётов, подписываются даты в текстах и выгрузках, трактуются расписания сгорания и выгрузок. Допустим только идентификатор IANA (422 program_timezone_invalid); GET /v1/program/timezone доступен любому пользователю интерфейса. Все даты в интерфейсе показываются и вводятся в поясе программы, а не браузера.
  • Системные счета по умолчанию разведены: MRK — источник начислений, RDM — списание, BRK — сгорание.

Отчёты и мониторинг. Подробности — Отчёты, Мониторинг, Наблюдаемость.

  • Раздел «Отчёты» — сводные величины программы за период на вкладках «Обзор», «Баллы», «Уровни», «Акции», «Рефералы». Период задаётся списком «Глубина» (7 дней, 30 дней, 90 дней, год), общим для всех вкладок. Доступ — по роли LOYALTY_MONITORING_READ.
  • API отчётов /v1/reports/*: пульс программы, баллы (движение, доля погашения, обязательства на конец периода, предстоящее сгорание 7/30/90 дней), акции за период с итогами и динамикой по дням, уровни (распределение, клиенты прежних конфигураций, переходы, включая ручные назначения и перенос системой), реферальная программа (период, когорта и срез состояния на момент запроса). Окно задаётся параметрами from/to, перевёрнутое окно — 422 invalid_period. Тестовые клиенты не учитываются.
  • Отчёт по акциям: строка на линию версий акции с колонками «Период», «Покупок» и «Выручка». Выручка считается за вычетом возвратов, по строкам не суммируется и остаётся пустой, если покупки были в разных валютах. Сортировка серверная по всем колонкам; выгрузка «Скачать таблицу (CSV)» (GET /v1/reports/promotions/export) берёт весь период, а не текущую страницу.
  • Сгорание и сторно не считаются тратой клиента. У сторно — свой тип движения REVERSAL, у ручного сгорания — свой источник MANUAL_EXPIRY. В сводке мониторинга redeemedMinor включает только траты клиентов, добавлены expiredMinor/expiredCount и сгорание в дневной динамике; в «Пульсе» и отчёте по баллам сторно вынесено в reversedMinor/reversedCount. Контракт расширен без удаления полей, но на данных со сгоранием или сторно значение redeemedMinor становится меньше.
  • Экран «Мониторинг»: к ширине окна 7/30/90 дней добавлен произвольный период (поля «Начало» и «Конец»), подпись «Показано за …» и плитка «Сгорело»; величины движения подписаны в баллах, а не в рублях. Выдачи /v1/monitoring/* принимают from/to, сводка возвращает фактическое окно; неположительная ширина или перевёрнутое окно — 422 invalid_period.
  • BREAKING. Списки мониторинга (аномалии, сгорающие лоты, аудит ручных начислений) переведены на конверт {items, page, size, total} с настоящим total вместо усечённого потолком в 500 массива.
  • Оповещения Prometheus по техническим и бизнес-показателям — недоступность, вставшая очередь приёма, команды в разборе, доля ответов 5xx, всплеск аномалий и ручных начислений, «Прогон сгорания баллов давно не отрабатывал» (LoyaltyExpiryStalled, заменило правило о крупном объёме баллов к сгоранию), «Уведомления клиентам не уходят» (LoyaltyNotificationsStuck). Выключены по умолчанию, включаются на контуре заказчика.
  • Операционный день заполнен у каждой операции журнала бонусных операций (прежние строки дозаполнены по связанной проводке), поэтому дневной отчёт и сверка итогов дня больше не теряют старые операции.
  • Итоги субсчёта за период больше не выдумывают разбивку «списано / сгорело»: признак expirySeparable показывает, выделяется ли сгорание; даты периода — в часовом поясе программы.
  • Проверка целостности учёта больше не краснеет постоянно: равенство баланса и суммы проводок сверяется по клиентским счетам, а для системных проверяется нулевая проекция баланса.
  • Метрики приёма сегментов клиентов: loyalty.segments.fills (по статусу заливки), loyalty.segments.fill.complete (длительность применения), loyalty.segments.members.entered / .exited.

Платформа и интеграции. Подробности — Интеграции, Конфигурация, Безопасность.

  • Асинхронный приём команд изменения баланса через очередь и опциональный Kafka-транспорт; порядок исполнения гарантирован в пределах одного клиента; отложенное исполнение («начислить в день рождения»). В примерах values чарта для dev/qa/prod Kafka-транспорты приёма команд и чанков ручных операций включены (JAAS из секрета loyalty-kafka).
  • BREAKING. Код торговой точки обязателен: storeCode в POST /v1/integration/pos/receipts и externalStoreCode в POST /v1/events/purchase; для покупки без физической точки передаётся код виртуальной точки из справочника торговых точек. Новые необязательные поля: канал channel, MID merchantCode и переменные покупки attributes — на весь чек и у каждой позиции (до 20 переменных, название — латиница, цифры, «_» и «-» до 64 символов, значение — до 256 символов).
  • Возврат по чеку POS: POST /v1/integration/pos/returns сторнирует баллы по исходному чеку пропорционально сумме возврата и уменьшает накопление для уровня. Баланс в минус не уходит: потраченные баллы остаются в unreversedPointsMinor. Отмена возврата — POST /v1/integration/pos/returns/{returnId}/cancel.
  • Сегменты клиентов из внешней системы (Batch Flow): регистрация, пачки участников и завершение заливки (/v1/integration/customer-segments/**, роль LOYALTY_SEGMENT_INTAKE); пока заливка не завершена, действует прежний состав. Сегменты клиента для внешних систем — GET /v1/integration/customers/{externalId}/segments.
  • BREAKING. Залитый внешней системой состав — единственный источник сегментов клиента: поле segments в POST /v1/events/purchase игнорируется (помечено deprecated), сегменты профиля MDM не учитываются. До первой заливки сегментные акции не применяются ни к кому.
  • Исходящие события через очередь исходящих: смена уровня tiers.customer.tier-changed и уведомления клиентам notifications.customer.message-requested (момент отправки — по поясу клиента, новый повод — списание баллов). Доставка «не менее одного раза», повтор определяется по eventId; отправка включается отдельно (LOYALTY_TIER_EVENTS_ENABLED, LOYALTY_NOTIFICATIONS_ENABLED). Разбор очереди уведомлений — через API /v1/admin/notifications (роли LOYALTY_NOTIFICATION_READ/WRITE).
  • BREAKING. Отказ валидации тела и параметров отвечает 400 validation_failed со списком нарушений в detail, а не bad_request, — в том числе на интеграционных точках POS, Promo, RT Flow и лент изменений.
  • Обмен данными с внешними системами: лента изменений с курсорной пагинацией и регламентные задания выгрузки витрин, по умолчанию выключены. Возобновление ленты больше не теряет и не дублирует строки (watermark — только на последней странице окна, changedSince строго исключающий); витрина DWH_TRANSACTIONS отдаёт клиента и субсчёт по каждой ноге проводки.
  • Интеграционный периметр защищён сплошным ограничением частоты (GCRA), проверкой загружаемых файлов до разбора содержимого (сигнатура, кодировка, антивирус) и сверкой итогов/целостности баланса.
  • BREAKING. Ключ идемпотентности действителен только для операции, которая его заняла: повторное использование на другой операции — 422 idempotency_key_reused.
  • Аренда instance-id генератора идентификаторов между репликами — исключает коллизии ID при масштабировании.
  • Ресурсы пода backend подняты до 2 CPU / 2 GiB; чарт получил liveness/readiness/startup-пробы и опциональные правила оповещений Prometheus. Корневая ФС контейнера — только на чтение (readOnlyRootFilesystem), пояс среды исполнения закреплён UTC (TZ=UTC), base-chart uniform-service 1.0.29 рендерит grace period 120 с.
  • Состав ПО (SBOM, CycloneDX): образ backend несёт его внутри, для веб-интерфейса перечень зависимостей по package-lock.json публикуется отчётом GitLab в CI.
  • BREAKING. Переоткрытие операционного дня убрано из контракта (POST /v1/business-day/{date}/reopen удалён); инвариант «открыт ровно один день» — ограничение базы. Исправление ошибки закрытого дня — только встречной операцией текущим днём. При пустом справочнике рабочих дней учётная дата берётся в поясе программы.

Модель данных

Ключевые новые таблицы:

  • Акции: promotion_label + promotion_label_link + promotion_label_history, promotion_audience_condition + promotion_audience_ref, promotion_participation_limit, реестр условий criterion_def + criterion_registry_revision, журнал согласования approval_event, снимки области списания redemption_scope + redemption_scope_item. Условия акции хранятся в criteria_json, колонки scope_json и границ суммы чека удалены.
  • Конфигурация программы и уровни: program_config, program_config_activation, program_config_promotion, program_config_redemption (+ _item, _tier), уровень клиента customer_tier, очередь событий tier_event_outbox. Уровни loyalty_tier принадлежат конфигурации; customer_tier_progress удалена.
  • Покупки: purchase, purchase_return, purchase_return_line, статистика переменных покупки purchase_attribute_daily + purchase_attribute_value.
  • Клиенты: tag + customer_tag_history, версии настройки карточки client_profile_configuration (+ разделы, карточки и таблицы, столбцы, реестр ключей полей client_profile_field_key), customer_status_history, customer_timezone; сегменты customer_segment, customer_segment_member (+ _history), segment_fill (+ _member). Колонка identity_profile.segments удалена.
  • Баллы и валюты: points_subaccount (+ points_subaccount_restriction), пары обмена exchange_pair (+ _limit, _subaccount), задачи открытия счетов account_opening_run, базовый срок currency.points_lifetime_days, источник срока партии account_lot.expiry_source. Таблица point_currency удалена, слита в единый справочник currency (kind MONEY/POINTS, курс, базовая валюта).
  • Справочники: city, store, merchant (MID и MCC перенесены из partner), product_category, product_item.
  • Операции и платформа: operation_batch (+ _row, _chunk), command_inbox, outbound_notification, exchange_job + exchange_run, reconciliation_run + reconciliation_diff, audit_seal, siem_cursor, реестр сотрудников app_user.
  • Вовлечённость: referral_link, achievement + customer_achievement (+ _metric).

Производные и нечитаемые колонки удалены из схемы (в том числе account.balance и chart_of_accounts.parent). Полный состав и ключевые поля — Модель данных.

Совместимость и обновление

BREAKING (внутренняя схема): переход леджера на партиционированную схему

Первичный ключ проводки леджера стал составным (posted_at, entry_id), таблица партиционирована помесячно по времени проводки. На пустой базе миграция применяется автоматически. На контуре с уже накопленными данными нужно до деплоя новой версии, в окно обслуживания, выполнить deploy/sql/ledger-time-model-swap.sql — иначе старт приложения останавливается. Подробности — Установка и развёртывание.

BREAKING (ролевая модель): запуск акции — отдельное право

POST /v1/promotions/{id}/activate и POST /v1/promotions/bulk-activate требуют роль LOYALTY_PROMOTION_ACTIVATE независимо от того, включено ли согласование. До выкатки перезапустите import.sh и выдайте новую роль тем, кто запускает акции, — иначе «Запустить» ответит 403 access_denied.

  • Роли Keycloak. Число ролей LOYALTY_* выросло (в 26.2.0 был 21, стало 54). Новые, среди прочих: LOYALTY_PROMOTION_ACTIVATE, LOYALTY_PROMOTION_APPROVE, LOYALTY_PROGRAM_CONFIG_*, LOYALTY_CLIENT_PROFILE_ACTIVATE/DELETE, LOYALTY_SEGMENT_*, LOYALTY_NOTIFICATION_*, LOYALTY_CITY_*, LOYALTY_STORE_*, LOYALTY_MERCHANT_*, LOYALTY_PRODUCT_CATALOG_*, LOYALTY_ACHIEVEMENT_*. Новые роли входят в группу LOYALTY_ADMINS, кроме технических LOYALTY_INTEGRATION_READ, LOYALTY_EXCHANGE_READ/WRITE (их получают интеграционные клиенты) и LOYALTY_SEGMENT_INTAKE (только сервисная учётная запись системы, заливающей сегменты). На уже провижиненном realm заведение ролей требует повторного запуска import.sh — helm upgrade роли не заводит. Роль LOYALTY_CORRECTION_OTHER_WRITE переименована в LOYALTY_OPERATION_REASON_OTHER_WRITE — старую роль нужно снять вручную, повторный импорт только заводит новую. Подробности — Ролевая модель.
  • Миграции применяются автоматически при старте. Перед обновлением контура с данными прочитайте «Что учесть при обновлении на текущую версию»: значения атрибутов клиента по ключам, которых нет в схеме полей, удаляются безвозвратно (проверочный запрос приведён там); пока в кластере живут поды прежней версии, часть запросов у них отвечает ошибкой на удалённых колонках; отдельные проверки данных откладываются до исправления; после выкладки идут разовые фоновые задачи (открытие счетов во всех включённых валютах). Существующая настройка карточки клиента становится активной версией, условия «Уровень клиента» существующих акций переписываются на конфигурацию «Основная».
  • Бэкенд и веб-интерфейс выкатываются одной версией: интерфейс 26.3.0 читает новые форматы ответов (постраничная история уровней, условия criteria, вкладки продукта карточки клиента), а прежний интерфейс с новым бэкендом работает неверно.
  • PostgreSQL — 15 или новее: на этой версии работают стенды и интеграционные тесты, более ранние версии не проверяются.
  • Настройки развёртывания (подробности — Конфигурация):
    • loyalty.promotion.budget.point-currency и customer.default.currency больше не читаются — бюджет акций и счета новых клиентов считаются по валюте баллов программы (points.currency);
    • системные счета по умолчанию разведены (MRK, RDM, BRK; переопределяются LOYALTY_ACCRUAL_SOURCE_ACCOUNT, LOYALTY_REDEMPTION_SINK_ACCOUNT, LOYALTY_EXPIRY_BREAKAGE_ACCOUNT); прежние проводки остаются на MRK;
    • часовой пояс программы (LOYALTY_PROGRAM_TIMEZONE, только IANA) применяется сервером: расписания сгорания, предупреждений и выгрузок трактуются в нём, смена пояса применяется к расписаниям после перезапуска; пояс среды исполнения — UTC;
    • первое включение сгорания на контуре: включить предупреждения → выждать самый длинный шаг LOYALTY_EXPIRY_NOTIFY_BEFORE → пробный прогон dryRun=true → задать LOYALTY_EXPIRY_CRON;
    • пересмотр уровней (LOYALTY_TIER_REVIEW_CRON) выключен по умолчанию: пока расписание не задано, уровни клиентов не понижаются и ручные назначения не снимаются;
    • отправка событий смены уровня и уведомлений клиентам выключена по умолчанию (LOYALTY_TIER_EVENTS_ENABLED, LOYALTY_NOTIFICATIONS_ENABLED) — события копятся в очереди исходящих;
    • согласование запуска акций включается LOYALTY_PROMOTION_APPROVAL_ENABLED бэкенда в паре с PROMOTION_APPROVAL_ENABLED веб-интерфейса.
  • Интеграции. До обновления каналы покупок должны передавать код торговой точки, а система кампаний — заливать сегменты: до первой заливки сегментные акции не применяются ни к кому.
  • Несовместимые изменения контракта API (BREAKING). Сверьте интеграции перед обновлением; полный список с пояснениями — API Reference → «Существенные изменения контракта в 26.3.0», коды ошибок — Коды ошибок:
    • ошибки валидации — 400 validation_failed вместо bad_request, в том числе на точках POS, Promo, RT Flow и лент изменений;
    • обязательный код торговой точки в покупке; поле segments в событии покупки игнорируется, из профиля клиента (identity) оно удалено;
    • акции: criteria вместо scope, пары levels в условии «Уровень клиента», обязательная награда механики и потолок бонуса только полем maxBonusMinor, обязательный priority, полная замена тела при PUT, правка на месте только без применений, статусная модель (archived, SCHEDULED), budgetMinor в деньгах;
    • запуск акции — право LOYALTY_PROMOTION_ACTIVATE;
    • уровни: PUT/DELETE /v1/tiers/{code} удалены, у уровня нет metric, история переходов — постранично;
    • клиенты: фильтр tier вместо tierCode, атрибуты карточки — списки значений с проверкой по типу поля, API конфигуратора карточки требует configurationId и If-Match, слияние в гонках отвечает 404/409, недоступность MDM — 503 mdm_unavailable;
    • баллы: базовый субсчёт (isBase, без POST .../default, subaccount_base_cannot_close), срок субсчёта expiryAt, режим без своего срока — «Общий срок», срок у резерва по умолчанию, причина ручной операции — код справочника, единый ответ массовых операций, kind в GET /v1/currencies, прежний /v1/admin/point-currencies поверх справочника валют;
    • партнёры: merchantIds/mcc удалены из контракта партнёра;
    • справочники: общие ручки по ключам customer-segment, product-category, sku отвечают 409 dictionary_own_screen;
    • мониторинг: списки в конверте {items, page, size, total};
    • платформа: ключ идемпотентности — только для своей операции (422 idempotency_key_reused), переоткрытие операционного дня удалено.
    • спецификация: схема ответа журнала аудита GET /v1/admin/audit называется AuditEntryView (под именем AuditView теперь описана история изменений справочника) — клиенты, сгенерированные по openapi.yml, нужно перегенерировать.

Открытые вопросы и известные несоответствия

  • Мастер импорта клиентов в интерфейсе пока доходит только до шага «Проверка» — серверная проверка и сама загрузка CSV доступны через API, но не как последний шаг мастера в UI.
  • Область списания баллов акции (pointsRedemptionScope) задаётся только через API, а тело акции — полная замена: сохранение акции из интерфейса снимает область, заданную через API.
  • Бюджет акции в баллах: форма берёт курс первой включённой валюты баллов, а сервер считает расход по курсу валюты баллов программы (points.currency). Если балльных валют несколько, бюджет надёжнее задавать в рублях.
  • Пары обмена пока не применяются операцией обмена POST /v1/exchange, хотя подсказка на вкладке «Обмен валют» описывает целевое поведение.
  • Итоги субсчёта при expirySeparable = false показывают «Сгорело за период» = 0 без пояснения.
  • Карточка клиента: история смены статуса, теги и флаг архива пока доступны только в API; признак чувствительности атрибута не маскирует значения.
  • Экранов нет у справочников городов, торговых точек, мерчантов и каталога товаров, у очереди исходящих уведомлений и у отчёта о взаиморасчётах с партнёром — они доступны через API.
  • Согласование запуска акции: e-mail-оповещений согласующих и выбора согласующих по именам в интерфейсе нет, включение — только настройкой развёртывания.
  • Оповещение LoyaltyExpiryStalled считается по каждому поду, а показатели прогона сгорания хранятся в памяти пода: оповещение срабатывает после каждой выкладки или перезапуска до первого прогона на этом поде и не гаснет у реплики, которой прогон не достаётся.
  • Переливка в DWH: поставляемый запрос вставки витрины DWH_TRANSACTIONS ссылается на прежние поля customerExternalId и subaccountCode — перед включением LOYALTY_DTP_ENABLED задайте собственный merge-statement под поля по ногам проводки.
  • Очередь событий смены уровня (tier_event_outbox) не убирается и растёт, пока отправка событий выключена (LOYALTY_TIER_EVENTS_ENABLED=false).
  • Форма партнёра ещё показывает поля «MID» и «MCC», но бэкенд их больше не принимает: мерчанты ведутся только через API /v1/admin/merchants.
  • Валюту с субсчётом «Акционные баллы» (PROMO) удалить нельзя: удаление отвечает 409 currency_in_use с видом ссылки SUBACCOUNTS, закрытый субсчёт тоже считается ссылкой.
  • Сертификат Apple Pass Type ID и ключ сервисного аккаунта Google для мобильного кошелька — секреты заказчика; до их подключения выпущенный пакет Apple Wallet структурно верен, но не подписан, а сохранение в Google Кошелёк не собирается вовсе.

26.2.0 (2026-07-05) — инженерный релиз качества

Функциональность 26.1.0 не менялась: релиз закрепляет качество кода и сборки по итогам двух архитектурных ревью (frontend-аудит и чек-лист backend-архитектора). Обратная совместимость полная, контракт API не тронут, миграций БД нет.

Состав релиза

Область Что вошло
Backend Рефакторинг партнёрского пакета: константы вместо литералов, методы записей вместо статик-хелперов, устранение дублей и лишних запросов (identity-профиль и история — лениво). Поведение эквивалентно, интеграционные тесты зелёные
Frontend: архитектура Типизированный runtime-конфиг, auth-bridge c одноразовым обновлением токена по 401, единый адаптер ошибок, Public API модулей, константы маршрутов MF-манифеста, раздельные локали ru/en, фабрики query-ключей
Frontend: сборка Миграция CRA/CRACO → чистый Webpack 5 (контракт Module Federation remote сохранён 1:1), Jest-раннер с юнит-тестами хелперов, детерминированная установка (lock-файл), registry-токен вынесен из исходников в CI-переменную
Frontend: страницы Пять крупных экранов разобраны на компонентную структуру (мастер акции, настройки ПЛ, карточка клиента, справочники, мониторинг)
Качество Комплексный прогон: unit (backend полный + UI-хелперы), 17 функциональных API-сценариев, e2e по экранам — пройдено; подготовлен план нагрузочного тестирования

Совместимость и обновление

  • Обратно совместим с 26.1.0; ручных шагов нет (миграции БД в релизе отсутствуют).
  • Для CI сборки UI теперь требуется переменная NPM_AUTH (masked) вместо токена в Dockerfile.

26.1.0 (2026-07-03) — B2C-кабинет и партнёрские предложения

Второй функциональный выпуск. Открывает B2C-контур (личный кабинет клиента: API, SDK и референс-веб-кабинет — чтобы ритейлер собрал клиентский сайт или приложение поверх лояльности) и целиком закрывает эпик партнёрских предложений — совместные акции лояльности на продуктах партнёров (например, банк показывает в своём кабинете скидку партнёрского кафе), от настройки до финансовых взаиморасчётов. Доменное ядро (леджер, лоты, проводки, обмен) не менялось — обратная совместимость с 26.0.0 сохранена, миграции применяются автоматически.

Состав релиза по доменам

Домен Что входит в выпуск
Партнёрские предложения Реестр партнёров (MID/MCC); офферы по четырём осям — доставка (промокод / расчёт кэшбэка), акцепт (акцептное / безакцептное), аудитория (все / сегмент), финансирование (партнёр / программа / совместно); экран администратора и витрина офферов в кабинете клиента
Card-linked начисление Начисление кэшбэка по транзакциям у партнёра: резолв партнёра по коду/MID/MCC, подбор предложений, расчёт вознаграждения, лимиты и бюджет с авто-паузой, идемпотентность
Взаиморасчёты Partner-funded вознаграждения отражаются на счёте партнёра в леджере; отчёт взаиморасчётов с партнёром (атрибуции и фактически профинансировано)
Личный кабинет (B2C) Клиентский API /v1/b2c/me/**, JS/TS-SDK и референс web-кабинет: карта, баланс, уровень, история, доступные и партнёрские предложения
Акции Наполняемый справочник механик (= диаграммы RT Flow); копия акции («Сохранить как»)

Новое в 26.1.0

  • Партнёрские предложения. Экран «Администрирование → Партнёрские предложения»: реестр партнёров и кампании по независимым осям — доставка PROMOCODE | CALCULATED, акцепт OPT_IN | AUTO, аудитория ALL | SEGMENT, финансирование PARTNER | PROGRAM | SHARED. Жизненный цикл оффера DRAFT → ACTIVE → PAUSED → ARCHIVED; промокод виден клиенту только для AUTO или после акцепта. Управление — /v1/partners и /v1/partner-offers, роли LOYALTY_PROMOTION_READ / WRITE.
  • Card-linked начисление кэшбэка. POST /v1/integration/partner/transactions (роль LOYALTY_INTEGRATION_WRITE): транзакция клиента у партнёра резолвит партнёра по коду/MID/MCC, подбирает применимые CALCULATED-офферы (период, аудитория, акцепт, минимальная сумма, «только новым»), считает вознаграждение (PERCENT с ограничением / POINTS / FIXED), проверяет лимит на клиента и бюджет с авто-паузой оффера. Начисление помечается источником PARTNER.
  • Взаиморасчёты с партнёром. Ось финансирования определяет счёт-источник в леджере: PARTNER и SHARED дебетуют счёт партнёра PRT-<code> (статья плана счетов PRTF), PROGRAM — системный источник. Отчёт GET /v1/partners/{id}/settlement даёт атрибуции по офферам, итоги по финансированию и фактически профинансировано (сумма дебет-проводок партнёра — основа для счёта). Роль LOYALTY_PROMOTION_READ.
  • Личный кабинет клиента (B2C). Клиентский API /v1/b2c/me/** с отдельной security-цепочкой (скоуп loyalty.b2c; идентификатор клиента берётся из токена — доступ только к своим данным): карта, баланс, уровень, история операций, доступные акции и партнёрские предложения с активацией. JS/TS-SDK и референс web-кабинет для быстрой интеграции клиентского сайта/приложения.
  • Справочник механик = диаграммы RT Flow. Каталог механик стал наполняемым справочником, синхронизированным с диаграммами RT Flow (первая реализация — ведётся вручную); механика выбирается при создании акции.
  • Копия акции. Кнопка «Сохранить как» создаёт независимую копию существующей акции.

Идемпотентность card-linked начислений

Ровно один раз — три слоя

Приём транзакции у партнёра защищён от повторов на трёх уровнях: (1) заголовок Idempotency-Key на границе контроллера (по умолчанию — бизнес-ключ транзакции externalRef); (2) атрибуция partner_offer_redemption с ограничением UNIQUE (offer, transaction_ref) — повтор той же транзакции не даёт второго вознаграждения по офферу; (3) backstop ядра леджера (ledger_posting_reference, ADR 0002). Ретраи вызывающей стороны безопасны.

Совместимость и обновление

  • Обратно совместим с 26.0.0: доменное ядро не менялось, миграции БД (partner, partner_offer, partner_offer_acceptance, partner_offer_redemption, статья плана счетов PRTF) применяются автоматически на старте сервиса — ручных шагов нет.
  • Промокоды остаются за CM Ocean Promo; партнёрские card-linked начисления — за Loyalty (источник PARTNER).
  • Для B2C-кабинета нужен клиентский OAuth-скоуп loyalty.b2c в Keycloak (клиентские токены отдельно от staff). Если B2C не разворачивается — на остальной функционал не влияет.

26.0.0 (2026-06-27) — первый публичный релиз

Первый публичный выпуск модуля CM Ocean Loyalty. Версия фиксирует продуктовый контракт API (плоские DTO, RFC-7807, идемпотентность) и собирает воедино функциональность по всем доменам. Доменный слой (леджер, лоты, проводки, обмен) сохраняет бизнес-логику начислений и списаний, выверенную в трёх волнах рефакторинга контракта.

Состав релиза по доменам

Домен Что входит в выпуск
Ядро-леджер Двойная бухгалтерия над счетами, лотами и проводками; начисление, двухфазное и прямое списание, перевод, обмен, сгорание по срокам; идемпотентность денежных операций
Акции Каталог механик, мастер создания, жизненный цикл, детерминированный арбитраж и версионирование, статистика; экспорт-импорт акций между контурами
Уровни Каталог уровней, расчёт уровня по обороту за период; ручное назначение уровня и near-RT журнал переходов уровня
Клиенты Карточка профиля (MDM identity + лоялти); правка и создание профиля клиента, слияние клиентов «1 телефон = 1 профиль»
Администрирование Справочники (НСИ) — гибридная модель; операционная конфигурация; лимиты списания по разрезам
Безопасность OAuth2 resource server (Keycloak, JWT), deny-by-default, роли LOYALTY_*, гейтинг прав в UI, сквозной журнал аудита

Новое в 26.0.0

  • Правка и создание профиля клиента. Запись «золотого» профиля в MDM через бэкенд (PUT /v1/customers/{externalId}/profile); в UI — действия «Редактировать» и «Новый клиент». Требует роль LOYALTY_CUSTOMER_WRITE.
  • Ручное назначение уровня. Override расчётного уровня с причиной и аудитом (set_by / set_at) и бейджем «Назначен вручную»: PUT /v1/customers/{externalId}/tier и DELETE /v1/customers/{externalId}/tier. Near-RT журнал переходов уровня — GET /v1/customers/{externalId}/tier/transitions. Требует роль LOYALTY_TIER_WRITE.
  • Слияние клиентов «1 телефон = 1 профиль». POST /v1/customers/{externalId}/merge переносит данные источника на выжившего клиента; источник переводится в статус MERGED. Guard на пересечение валют счетов даёт 422. Требует роль LOYALTY_CUSTOMER_WRITE.
  • Лимиты списания по разрезам. Per-scope лимиты TIER / CURRENCY / SEGMENT поверх глобальной политики погашения по правилу «частное бьёт общее»; нарушение отклоняется RedemptionGuard с кодом 422. В UI — раздел «Настройка ПЛ» → «Лимиты по разрезам». Требует роль LOYALTY_DICTIONARY_WRITE.
  • Сквозной журнал аудита. GET /v1/admin/audit фиксирует все write-операции, включая отклонённые (403 / 422 / 404). Требует роль LOYALTY_MONITORING_READ.
  • Экспорт-импорт акций между контурами. Перенос акций по натуральному коду, идемпотентно: GET /v1/admin/promotions/export и POST /v1/admin/promotions/import; в UI «Акции» — кнопки «Экспорт» и «Импорт». Требует роли LOYALTY_PROMOTION_READ / LOYALTY_PROMOTION_WRITE.
  • Гейтинг прав в UI (RBAC). Все write-кнопки в интерфейсе неактивны без соответствующей роли LOYALTY_*_WRITE (с тултипом-подсказкой); итоговая проверка прав выполняется на бэкенде (403).

Контракт и доменный слой (выверено к релизу)

Сервис приведён к продуктовому контракту CM Ocean — выполнены три волны рефакторинга. Доменный слой (леджер, лоты, проводки, обмен) при этом не менялся: бизнес-логика начислений и списаний сохранена.

Леджер (двойная бухгалтерия)

  • Балльный леджер на двойной бухгалтерии над счетами, лотами и проводками: операции пополнения (top-up), списания (withdraw), перевода (transfer) и обмена валют (exchange).
  • Идемпотентность денежных операций: заголовок Idempotency-Key и таблица idempotency_record дают гарантию «ровно один раз» на уровне БД — повтор того же ключа возвращает сохранённый результат, а операция выполняется однократно.
  • Деньги в API и ответах представлены в минорных единицах.

Защита денежных операций

Эндпоинты /v1/top-up, /v1/withdraw, /v1/transfer, /v1/exchange требуют заголовок Idempotency-Key.

Контракт API

  • BREAKING (API): ответы переведены с HATEOAS/HAL на плоские DTO и конверт коллекций {items, page, size, total}. Убраны _embedded / _links; HAL-индекс (RootController) удалён — обзор API теперь через OpenAPI / Swagger UI.
  • Версионирование API: все контроллеры размещены под префиксом /v1 (снаружи — /api/v1).
  • Обработка ошибок по RFC-7807: ответ application/problem+json со стабильным кодом ошибки code и идентификатором трассировки traceId. Бизнес-нарушения и «не найдено» теперь дают корректные коды 4xx вместо 500.
Аспект контракта Было Стало
Формат ответа HATEOAS / HAL (_embedded, _links) Плоские DTO + конверт {items, page, size, total}
Версионирование — Префикс /v1
Ошибки в т. ч. 500 на бизнес-нарушениях RFC-7807 problem+json, code + traceId, корректные 4xx
Деньги в ответах — Минорные единицы

Безопасность

  • OAuth2 resource server на Keycloak (JWT), модель deny-by-default.
  • Ролевой доступ @PreAuthorize с ролями LOYALTY_*; роли берутся из resource_access[...].roles настроенного клиента.
  • Секреты (пароль БД и пр.) подаются только из окружения; трассировка стека в ответах отключена (server.error.include-stacktrace: never).

Наблюдаемость и корреляция

  • Корреляция запросов: фильтр проставляет traceId в MDC и возвращает эхо-заголовок X-Request-Id.
  • JSON-логи в прод-профиле (logback + logstash-encoder).
  • Management-эндпоинты на отдельном порту 9090: метрики Prometheus (/metrics), liveness / readiness-пробы (/health).

Схема данных и миграции

  • Схема БД версионируется Liquibase и применяется приложением на старте; добавлено ограничение UNIQUE на код валюты (currency.code).
  • На уже засеянных стендах baseline помечается как применённый (MARK_RAN) через preconditions.

Исправление: применение миграций

В Spring Boot 4 автоконфигурация Liquibase переехала в модуль spring-boot-liquibase (вместо liquibase-core). Подключён правильный модуль — миграции снова применяются на старте.

OpenAPI и качество

  • Спецификация OpenAPI генерируется из кода (springdoc) и коммитится в репозиторий; на каждом эндпоинте задан operationId вида loyalty.<resource>.<action>.
  • Введён diff-gate (OpenApiSpecIT): сборка падает при расхождении кода и зафиксированной спеки.
  • Тестовый харнес на Testcontainers (PostgreSQL); интеграционные тесты идемпотентности и diff-gate OpenAPI выполняются через Failsafe. CI больше не пропускает тесты.

Удалено

  • Зависимости HATEOAS (spring-boot-starter-hateoas, hal-explorer) и HAL-слой контроллеров.

В дорожной карте

Запланировано, ещё не реализовано

Перечисленные ниже возможности находятся в проработке и в текущем выпуске недоступны:

  • Публикация изменений справочников и акций через outbox → Kafka в потоковый decisioning (SDE); кеш справочников в памяти реплик уже реализован.
  • Механики акций как параметры — конфигурируемые шаблоны механик.
  • Сертификаты — выпуск и погашение.