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

Интеграции

CM Ocean Loyalty работает не изолированно, а в составе платформы CM Ocean. Модуль принимает данные о покупках и начислениях из соседних систем, обогащает профиль клиента мастер-данными, отдаёт клиенто- безопасные проекции кассовым сценариям, поставляет ленты изменений и витрины в DWH и синхронно проверяет условия для потокового decisioning RT Flow.

На этой странице перечислены все точки интеграции с указанием, что реализовано и доступно сегодня, а что относится к дорожной карте.

Обзор контура

flowchart LR
    POS["POS / касса"] -->|"чек, возврат, резолюция карты"| LOY
    PROMO["CM Ocean Promo"] -->|эффект «баллы»| LOY
    MDM["CM Ocean MDM"] -.->|профиль клиента| LOY
    RTF["RT Flow / SDE"] -->|проверка условий, начисление| LOY
    BF["Система расчёта сегментов"] -->|состав сегментов| LOY
    LOY["CM Ocean Loyalty"] -.->|"смена уровня, уведомления (опц.)"| BROKER
    LOY -->|ленты изменений, регламент, DTP| DWH["DWH / витрины"]
    CS["Check Scan"] -->|проекция клиента| LOY
    BROKER["Брокер (Kafka)"] -.->|"команды приёма (опц.)"| LOY
    UI["Core UI (хост)"] --> LOY
Точка интеграции Направление Статус
Приём чеков и возвратов POS, резолюция карты вход Реализовано
Приём начислений от CM Ocean Promo вход Реализовано
Профиль клиента из MDM (externalId) контур Реализовано на уровне идентификатора
Проверка условий и начисление из RT Flow вход/выход Реализовано (синхронные вызовы); потоковая поставка бонусных операций в SDE — дорожная карта
Сегменты клиентов из внешней системы вход Реализовано
Проекция клиента для Check Scan выход Реализовано
Асинхронный приём команд (API и брокер Kafka) вход Реализовано
Исходящие события: смена уровня, уведомления клиентам выход Реализовано, выключено по умолчанию
Ленты изменений, регламент обмена, выгрузка витрин в DWH выход Реализовано
Теги/метки: создание клиента и акции с внешними значениями вход Реализовано
События сайта / мобильного приложения вход Через POS-адаптер / контур

Общие принципы интеграционных вызовов

Все входящие операции, изменяющие баланс, идемпотентны: вызывающая система передаёт заголовок Idempotency-Key, и повтор запроса возвращает сохранённый результат, не создавая повторной проводки. Доступ к интеграционным эндпоинтам защищён OAuth2 (JWT Keycloak); запись данных требует роли LOYALTY_INTEGRATION_WRITE, резолюция/чтение (карта, Check Scan, условия RT Flow, сегменты клиента) — отдельной роли LOYALTY_INTEGRATION_READ, которую не обязаны получать все пишущие интеграционные клиенты. Подробнее об ограничении частоты и проверке файлов — в разделе «Интеграционный периметр» руководства по безопасности.

Приём чеков POS

POS-адаптер вводит данные о продажах в петлю лояльности: чек разбирается по позициям, позиции оцениваются против активных акций, рассчитанный кэшбэк начисляется на счёт клиента.

Эндпоинт. POST /v1/integration/pos/receipts

Заголовок Назначение
Idempotency-Key Бизнес-ключ чека для exactly-once при ретраях
Authorization Bearer <JWT>, роль LOYALTY_INTEGRATION_WRITE

Тело запроса — чек с позициями:

{
  "customerCard": "4000000000000001",
  "receiptId": "POS-2026-06-25-000142",
  "currency": "RUB",
  "storeCode": "MSK-0012",
  "lines": [
    { "sku": "SKU-1001", "category": "fuel", "amountMinor": 250000, "quantity": 34.5, "unitCode": "LITER" },
    { "category": "shop", "amountMinor": 18000 }
  ]
}
Поле Тип Обязательность Описание
customerCard строка да Идентификатор карты клиента
receiptId строка да Идентификатор чека в POS
currency код валюты да Валюта чека
storeCode строка да Код торговой точки из справочника точек — один на весь чек. Код, которого нет в справочнике, чек не отклоняет, но включающие гео-условия акции (регион, город, магазин) такой чек не проходит
channel строка нет Код канала продаж из справочника channel
merchantCode строка нет MID мерчанта из справочника мерчантов — для условия акции «Мерчант»
attributes объект «название — значение» нет Переменные покупки на весь чек для дополнительных условий акции: название — латиница, цифры, _, -, до 64 символов; значение — до 256 символов; не больше 20 переменных. Действуют на каждую позицию, если у позиции такой переменной нет
lines[] массив, 1–1000 позиций да, непустой Позиции чека
lines[].sku строка нет Артикул товара; если товар есть в каталоге, условие по категориям сравнивается по категориям товара
lines[].category строка нет Товарная категория
lines[].amountMinor целое > 0 да Сумма позиции в минорных единицах
lines[].quantity число > 0 нет Количество — для механики «бонус за натуральный объём» (UNIT_BONUS)
lines[].unitCode код единицы измерения нет Единица количества (см. «Справочники»)
lines[].attributes объект «название — значение» нет Переменные покупки позиции — те же правила, что у attributes чека

Код точки обязателен

Поле storeCode стало обязательным: чек без него отклоняется 400 validation_failed. Для покупок без физической точки передавайте код виртуальной точки, заведённой в справочнике торговых точек. То же правило действует в событии покупки POST /v1/events/purchase (поле externalStoreCode); поле segments в событии покупки больше не учитывается — сегменты клиента берутся только из состава, залитого внешней системой (см. «Сегменты клиентов из внешней системы»).

Ограничения и проверка позиций

Чек ограничен 1000 позициями — столько допускает крупнейший из сценариев заказчика (лотерейные билеты в одной покупке); ограничение объявлено в контракте (maxItems) и на сервере. Каждая позиция валидируется (сумма строго положительна) — позиция с нулевой или отрицательной суммой отклоняет расчёт, а не проходит как обычная покупка. Внутренняя ссылка позиции строится по номеру позиции в чеке, а не по артикулу: чек с несколькими одинаковыми товарами (одинаковый артикул или оба без артикула) обрабатывается корректно, без ошибки на дубликате.

Ответ содержит результат расчёта акций и итоговый баланс:

{
  "receiptId": "POS-2026-06-25-000142",
  "appliedPromotions": 2,
  "totalBonusMinor": 5360,
  "balanceMinor": 124360
}

Чек регистрируется как одна покупка на сумму документа — по ней считаются накопление уровня, выручка в отчётах и возвраты; начисления по акциям по-прежнему считаются по каждой позиции. Повторная доставка того же чека вторую покупку не заводит. Позиции и артикулы чека модуль не хранит.

События сайта и мобильного приложения

Сейчас события покупки попадают в модуль через POS-адаптер (чек). Отдельный приём событий веб- и мобильных каналов решается на уровне контура платформы: фронтальные системы публикуют чеки/события, которые доставляются в Loyalty тем же контрактом чека POS.

Возврат по чеку POS

Возврат покупки сторнирует баллы, начисленные по исходному чеку, и уменьшает накопление для уровня.

Эндпоинт. POST /v1/integration/pos/returns, роль LOYALTY_INTEGRATION_WRITE, заголовок Idempotency-Key обязателен.

Поле Обязательность Описание
returnId да Идентификатор возврата в системе кассы — по нему возврат идемпотентен независимо от ключа запроса
receiptId да Исходный чек — тот же receiptId, что в POST /v1/integration/pos/receipts
customerCard да Карта клиента
currency да Валюта возврата — должна совпадать с валютой покупки
amountMinor да, > 0 Сумма возврата — не больше невозвращённого остатка чека

Ответ: returnId, receiptId, reversedPointsMinor (сторнировано), unreversedPointsMinor (сколько полагалось сторнировать, но клиент уже потратил), balanceMinor, purchaseFullyReturned.

  • Сторно пропорционально доле возврата по каждому начислению чека (акциям и базовому), при возврате всей суммы — точно до балла; расход бюджета акции уменьшается.
  • Сторно не больше доступного остатка: потраченные баллы не сторнируются, баланс в минус не уходит, а недосторнированное следующим возвратом не наверстывается. Лимит считается по субсчёту исходного начисления.
  • Уровень при возврате немедленно не понижается — накопление уменьшается, а уровень пересматривается в дату пересмотра клиента.

Отмена возврата — POST /v1/integration/pos/returns/{returnId}/cancel (Idempotency-Key обязателен): баллы начисляются обратно с той же привязкой к акциям, накопление восстанавливается; ответ — returnId, restoredPointsMinor, balanceMinor.

Код Когда
404 purchase_not_found Исходный чек не найден
422 return_amount_exceeds_purchase Сумма возврата больше невозвращённого остатка чека
422 return_currency_mismatch Валюта возврата не совпадает с валютой покупки
404 return_not_found Отменяемый возврат не найден
409 return_already_cancelled Возврат уже отменён
409 return_id_conflict returnId уже использован с другими данными

Резолюция карты лояльности (POS)

Касса умеет проверить карту клиента до пробития чека — например, при кассир-ассистированном сканировании (Check Scan).

Эндпоинт. GET /v1/integration/pos/cards/{cardId}

Заголовок Назначение
Authorization Bearer <JWT>, роль LOYALTY_INTEGRATION_READ (не _WRITE — её несут все пишущие интеграционные клиенты, а резолюция им не выдаётся скопом)

Ответ: { "customerExternalId": "CUST-77f3a0" }.

Anti-enumeration

Неизвестная, удалённая или заблокированная карта отвечают одинаково — 404 card_not_resolved без уточнения причины: различать «нет», «удалён» и «заблокирован» в ответе значило бы дать инструмент перебора карт. Частота запросов ограничена отдельным правилом (см. «Интеграционный периметр»).

Проекция клиента для Check Scan

Клиенто-безопасная витрина клиента для сценария Check Scan — тот же состав данных, что кассовый кабинет клиента (/v1/b2c/me), но без служебных полей (cardId, status, memberSince).

Эндпоинт. GET /v1/integration/checkscan/customers/{externalId}

Заголовок Назначение
Authorization Bearer <JWT>, роль LOYALTY_INTEGRATION_READ

Ответ — имя, баланс (null, если клиент не заведён в бонусную программу) и уровень:

{
  "displayName": "Иван Иванов",
  "balance": { "currency": "RUB", "balanceMinor": 12000, "heldMinor": 500, "availableMinor": 11500 },
  "tier": { "code": "GOLD", "name": "Золотой", "accumulatedMinor": 340000, "nextName": "Платиновый", "toNextMinor": 60000 }
}

Как и резолюция карты: неизвестный/неактивный клиент — 404 checkscan_customer_not_found без уточнения причины (anti-enumeration), недоступность MDM — 503 mdm_unavailable (интеграция обязана повторить запрос, а не трактовать это как «карты нет»), частота ограничена отдельным правилом. Обращение регистрируется в журнале аудита как просмотр персональных данных (см. «Журнал аудита»).

Приём начислений от CM Ocean Promo

CM Ocean Promo (движок промокодов) и CM Ocean Loyalty (накопительный бонусный леджер) дополняют друг друга. Когда в Promo погашается промокод с эффектом «начислить баллы» (loyalty_points), диспетчер Promo (асинхронно, из outbox) вызывает Loyalty для зачисления баллов клиенту.

Эндпоинт. POST /v1/integration/promo/accruals

{
  "externalId": "CUST-77f3a0",
  "pointsMinor": 1500,
  "code": "SUMMER25",
  "campaignId": "f1e2d3c4-...",
  "redemptionId": "a9b8c7d6-...",
  "reason": "промокод летней кампании"
}
Поле Тип Обязательность Описание
externalId строка да Идентификатор клиента в Loyalty (golden record / MDM)
pointsMinor целое > 0 да Сумма эффекта в минорных единицах баллов
code строка да Погашаемый промокод
campaignId строка нет Идентификатор кампании Promo
redemptionId строка да Бизнес-ключ погашения; передаётся как Idempotency-Key
reason строка нет Причина начисления; по умолчанию promocode <code>

Ответ:

{ "transactionId": 840193, "balanceMinor": 125860 }

Обратный эффект — сторнирование начисления — POST /v1/integration/promo/accruals/reversals ({externalId, redemptionId, pointsMinor, reason}, Idempotency-Key: <redemptionId>:reverse), полное сторно всего начисления по ссылке на прямую операцию. В журнал бонусных операций сторно пишется отдельным типом движения REVERSAL (источник — PROMOCODE), а не списанием, поэтому в отчётах и мониторинге оно не считается тратой клиента.

Двухслойная идемпотентность

Promo передаёт redemptionId как Idempotency-Key. Защита работает в два слоя: (1) на границе контроллера повтор возвращает сохранённый результат; (2) backstop ядра леджера по ссылке reference = redemptionId не создаёт двойной проводки даже при вызове в обход контроллера. Поэтому ретраи диспетчера outbox безопасны (exactly-once).

Аудит источника

Начисление пишется в журнал бонусных операций с источником PROMOCODE (а не MANUAL), поэтому фрод-мониторинг не помечает его как «начисление без покупки».

Обратное направление (обмен баллов на промокод)

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

Направление «списать баллы → выпустить промокод в Promo» (клиент обменивает накопленные баллы на разовый промокод) пока не реализовано. Требуется доработка единичной выдачи кода на стороне Promo и симметричная точка интеграции на стороне Loyalty.

Профиль клиента из MDM

Идентичность клиента ведётся в мастер-системе CM Ocean MDM (golden record). В интеграционных контрактах Loyalty клиент адресуется по externalId — это идентификатор клиента в Loyalty, согласованный с MDM. Если внешняя система оперирует собственным идентификатором, сопоставление выполняется до вызова Loyalty (на стороне вызывающей системы / MDM).

Разграничение ответственности

MDM — отдельный модуль платформы и владелец мастер-данных клиента. Loyalty не дублирует профиль, а оперирует согласованным идентификатором клиента и владеет только бонусным контуром (баланс, леджер, сгорание, уровни).

Профиль клиента содержит, кроме ФИО и контактов, дату рождения, город и часовой пояс. Город и пояс определяют время отправки уведомлений клиенту (см. «Конфигурация» → «Уведомления клиентам: очередь исходящих»); в карточке клиента пояс только для чтения. Запись профиля — PUT /v1/customers/{externalId}/profile: поля city и timezone (пустая строка очищает значение, пояс — только имя зоны IANA, иначе 422 customer_timezone_invalid) и birthDate (YYYY-MM-DD). Адаптер Data Ocean MDM передаёт и читает из этих полей только дату рождения; город и пояс хранятся при локальном хранилище профиля (LOYALTY_MDM_ADAPTER=local). Сегменты клиента из профиля мастер-системы больше не используются — единственный источник сегментов описан в разделе «Сегменты клиентов из внешней системы».

Недоступность MDM — не «клиент не найден»

Сбой обращения к MDM (таймаут, обрыв соединения, 5xx) отдаётся как 503 mdm_unavailable, а не как 404 customer_not_found: это разные факты, и вызывающая сторона обязана их различать (повторять 404 бессмысленно, 503 — можно и нужно). Подробнее — в руководстве по безопасности.

Приём команд из очереди

Асинхронный вход для команд, изменяющих баланс (начисление, списание, резерв), — команда принимается в очередь и подтверждается немедленно, а исполняется отдельно. Существующие синхронные точки приёма (чек POS, эффекты Promo) не заменяются: синхронный отклик по-прежнему нужен там, где касса ждёт результат (например, при создании резерва).

Эндпоинт. POST /v1/intake/commands, роль LOYALTY_INTAKE_WRITE, ответ 202 Accepted — подтверждение приёма, а не результат исполнения:

{
  "eventId": "src-2026-000481",
  "type": "ACCRUE",
  "externalId": "CUST-77f3a0",
  "reference": "birthday-2026-CUST-77f3a0",
  "occurredAt": "2026-09-14T10:00:00Z",
  "source": "crm-campaigns",
  "payload": { "pointsMinor": 5000, "reason": "День рождения" },
  "executeAt": "2026-10-02T00:00:00+03:00"
}
Поле Описание
eventId Идентификатор события у отправителя — ключ дедупликации приёма; повтор с тем же значением не создаёт второго исполнения
type ACCRUE | REDEEM | HOLD
reference Бизнес-ключ операции — уходит в леджер как последний рубеж против повтора самого факта
occurredAt Момент наступления события у источника (не время подачи)
executeAt Необязательный срок исполнения — «не раньше»: даёт отложенное начисление (день рождения, старт акции); отсрочка дальше срока хранения очереди отклоняется

У команд HOLD и REDEEM в payload можно передать позиции чека items[] (sku, category, amountMinor), код торговой точки storeCode и MID мерчанта merchantCode — так же, как в синхронных запросах резерва и списания. Позиции нужны правилам списания конфигурации программы с областью «товары» или «категории» и субсчетам с ограничением «на что»; точка и мерчант — субсчетам с ограничением «где».

Транспорт. Помимо синхронного API, команды можно поставлять через брокер Kafka — потребитель поверх той же очереди (выключен по умолчанию, требует настройки топика и группы). Оба транспорта ведут в одну очередь и исполняются одним воркером; неразобранное сообщение брокера не останавливает партицию и попадает в отдельный счётчик для ручного разбора.

Порядок исполнения. Команды одного клиента исполняются строго по порядку — конкурентное исполнение двух команд одного клиента (например, начисления и его сторно) не допускается: захват очереди берёт не более одной незавершённой команды на клиента.

Разбор оператором. GET /admin/intake/commands (роль LOYALTY_INTAKE_READ) — список команд по статусу (PENDING/RUNNING/DONE/PARKED); неисполнимая команда (неизвестный тип, битое тело, исчерпанные попытки) откладывается в статус PARKED. Оператор возвращает её в работу (POST .../{eventId}/retry) или отменяет с обязательным основанием (POST .../{eventId}/discard); команду, ещё не наступившую по сроку, можно отдельно отменить, пока срок не пришёл (POST .../{eventId}/cancel) — роль LOYALTY_INTAKE_WRITE.

Проверка условий из RT Flow

Сценарии RT Flow (потоковый decisioning) проверяют клиента по тегам и участию в акциях — той же семантикой и тем же кодом, что условия аудитории акции (см. «Справочники»).

Эндпоинт. POST /v1/integration/rtflow/condition-checks, роль LOYALTY_INTEGRATION_READ.

Запрос — {externalId, conditions[]}, каждое условие несёт дискриминатор type:

{
  "externalId": "CUST-77f3a0",
  "conditions": [
    { "type": "CUSTOMER_TAG", "mode": "HAS", "match": "ANY", "tagIds": [17, 25] },
    { "type": "PROMOTION_PARTICIPATION", "labelIds": [501], "count": { "operator": "GTE", "value": 2 } }
  ]
}
Тип условия Поля
CUSTOMER_TAG mode (HAS/HAS_NOT — отрицание результата), match (ANY/ALL), tagIds[]
PROMOTION_PARTICIPATION labelIds[], promotionIds[] (id любой версии — считаются по линии версий), count (оператор GT/LT/GTE/LTE/EQ/BETWEEN, без него — «не менее 1»), period ({from, fromInclusive, to, toInclusive}, включительность по умолчанию true)
CUSTOMER_SEGMENT mode (HAS/HAS_NOT), match (ANY/ALL), segmentCodes[] — коды сегментов без учёта регистра; проверка идёт по текущему составу, залитому внешней системой

Ответ 200:

{
  "results": [
    { "index": 0, "passed": true },
    { "index": 1, "passed": true }
  ],
  "allPassed": true
}
Поле ошибки Когда
400 condition_check_invalid Неизвестный type, пустой список условий, у PROMOTION_PARTICIPATION нет ни labelIds, ни promotionIds, у CUSTOMER_SEGMENT нет ни одного кода сегмента, либо передан self (у вызова из RT Flow нет «этой акции»)
404 customer_not_found externalId не зарегистрирован

Битая ссылка не роняет весь запрос

Ссылка на удалённый тег, метку, акцию или сегмент даёт passed: false только у своего условия и попадает в поле unresolved (tag:{id}, label:{id}, promotion:{id}, segment:{code}); остальные условия пакета считаются. Архивная акция битой ссылкой не считается.

Наблюдаемость

Таймер loyalty.integration.rtflow.condition_checks{outcome} и счётчик …condition_checks.unresolved — по нему видно сценарии, сломанные удалением значения из справочника.

Начисление по сценарию RT Flow

Отдельно от проверки условий, диаграмма RT Flow может напрямую зачислить баллы результатом своего расчёта (например, реферальное вознаграждение) — вся логика расчёта и проверок выполняется на стороне диаграммы, Loyalty лишь фиксирует итог.

Эндпоинт. POST /v1/integration/rtflow/accruals, роль LOYALTY_INTEGRATION_WRITE.

Поле Описание
externalId, pointsMinor Клиент и сумма начисления
reason «За что» начислено — фиксируется для мониторинга и разбора
externalRef Идентификатор отработки сценария RT Flow — трассировка и ссылка леджера для идемпотентности
source REFERRAL (по умолчанию) или RT_FLOW

Идемпотентность — заголовком Idempotency-Key, а при его отсутствии — самим externalRef: повтор сценария по тому же externalRef возвращает исходный ответ без повторной проводки.

Сегменты клиентов из внешней системы

Состав сегментов клиентов рассчитывает внешняя система (например, Batch Flow) и заливает его в Loyalty. Залитый состав — единственный источник сегментов клиента: по нему работают условия акций по сегментам, витрина акций кабинета клиента, партнёрские предложения с аудиторией «сегмент», лимиты списания по разрезу SEGMENT, карточка и каталог клиентов и проверка условий из RT Flow. Сегменты из профиля MDM и поле segments в событии покупки не учитываются; до первой заливки сегментные акции не применяются ни к кому.

Приём — роль LOYALTY_SEGMENT_INTAKE, которая выдаётся только сервисной учётной записи внешней системы (в группу LOYALTY_ADMINS не входит):

Шаг Эндпоинт Что делает
1. Регистрация PUT /v1/integration/customer-segments/{code} Заводит сегмент (новый код — 201) или начинает перезаливку существующего (replace: true — 200) и открывает заливку. Тело: label, source, externalLoadId (идентификатор расчёта), asOf (момент расчёта), необязательные description, replace. Ответ: fillId, maxMembersPerRequest
2. Пачки участников POST /v1/integration/customer-segments/{code}/fills/{fillId}/members Массив [{externalId}], не больше maxMembersPerRequest за вызов; повторы схлопываются. Пачки копятся «в тени» и на действующий состав не влияют
3. Завершение POST /v1/integration/customer-segments/{code}/fills/{fillId}/complete Заменяет состав сегмента разом, одной транзакцией; клиенты, оставшиеся в сегменте, сохраняют дату входа

Все шаги безопасно повторять: регистрация с тем же externalLoadId возвращает ту же заливку, повтор завершения — прежний итог. Пока заливка не завершена, действует прежний состав — исключающие условия акций не перестают исключать.

Код Когда
409 segment_already_exists Регистрация существующего кода без replace: true — дополнения состава нет, только перезаливка целиком
409 segment_fill_stale Расчёт (asOf) старше уже применённого состава; заливка сохраняется в журнале отклонённой
413 segment_batch_too_large В пачке больше участников, чем LOYALTY_SEGMENTS_MAX_MEMBERS_PER_REQUEST

Текущие сегменты клиента для внешних систем — GET /v1/integration/customers/{externalId}/segments (роль LOYALTY_INTEGRATION_READ): код, название, дата входа и момент расчёта; клиента нет в программе — 404 customer_not_found. Журнал заливок — GET /v1/admin/customer-segments/fills (роль LOYALTY_SEGMENT_READ), в интерфейсе — экран «Сегменты клиентов», вкладка «Заливки». Сроки хранения истории и журнала — см. «Конфигурация» → «Сегменты клиентов из внешней системы».

Удалённый сегмент в условии акции

Сегмент, который используется в условиях запланированной, активной или приостановленной акции, удалить нельзя (409 segment_in_use). Сегмент, удалённый из справочника, в условии акции становится битой ссылкой: черновик сохранить можно, запустить — нет. Новый код сегмента, которого нет в справочнике, отклоняется и в условии акции, и в партнёрском предложении (422 partner_offer_segment_not_found).

Исходящие события

Loyalty публикует в брокер (Kafka) два вида событий. Оба пишутся в очередь исходящих в БД в той же транзакции, что и сам факт, и отправляются отдельным отправителем: доставка «не менее одного раза», повтор потребитель отличает по eventId в теле. Отправку в каждом контуре включают отдельно; пока она выключена, события копятся в очереди, а пролежавшие дольше срока годности не отправляются.

Событие (eventType) Когда Тело Включение
tiers.customer.tier-changed Смена уровня клиента (покупка, пересмотр, ручное назначение, перенос на активную конфигурацию) клиент, уровни «из» и «в» (код и название), направление (up/down/same), источник перехода, накопление по сумме и числу покупок, конфигурация с признаком notifyCustomers LOYALTY_TIER_EVENTS_ENABLED, топик LOYALTY_TIER_EVENTS_TOPIC
notifications.customer.message-requested Созрело уведомление клиенту (начисление, списание, скорое и состоявшееся сгорание) клиент, повод (ACCRUAL, REDEMPTION, POINTS_EXPIRING, POINTS_EXPIRED), готовый текст, schedule (sendAfter, validUntil) LOYALTY_NOTIFICATIONS_ENABLED, топик LOYALTY_NOTIFICATIONS_TOPIC

Ключ сообщения — идентификатор клиента: события одного клиента идут в одну партицию по порядку. Событие смены уровня уходит всегда — отправлять ли по нему сообщение клиенту, решает потребитель по признаку notifyCustomers. Канал, адрес и согласия клиента в теле уведомления не передаются — их выбирает потребитель. Настройки — «Конфигурация».

Ленты изменений, регламент обмена и выгрузка в DWH

Обмен данными с внешними витринами и хранилищами построен на трёх взаимодополняющих механизмах.

Ленты изменений

GET /v1/admin/data-exchange/feeds/{customers,operations,accounts} — инкрементальная выборка «что изменилось с момента» с курсорной пагинацией (параметры changedSince/cursor, размер страницы до 5000). Роль — LOYALTY_EXCHANGE_READ, отдельная от LOYALTY_INTEGRATION_READ: лента клиентов отдаёт реестр целиком, включая персональные данные, и раздавать её вместе с интеграционным доступом кассовых клиентов нельзя. Верхняя граница окна отстаёт от текущего момента на настраиваемую величину (по умолчанию 5 минут) — так поздно закоммиченная запись не пропадает из ленты навсегда.

Окно ленты задаётся параметрами changedSince (нижняя граница, строго исключающая) и changedUntil (верхняя, включительная). Ответ несёт nextCursor, hasMore и watermark:

  • пока окно не дочитано (hasMore: true), продолжайте курсором nextCursor — watermark в таком ответе пустой;
  • watermark приходит только у последней страницы окна — это верхняя граница дочитанного окна, с неё следующая сессия начинает как с changedSince.

Так возобновление после обрыва не теряет непрочитанные страницы окна и не дублирует строки на стыке окон. Размер страницы (limit) — от 1 до 5000; значение вне диапазона отклоняется 400 validation_failed.

Регламент обмена

GET /admin/data-exchange/jobs / PATCH /admin/data-exchange/jobs/{code} (роль LOYALTY_EXCHANGE_WRITE на изменение расписания и включение) управляют регламентными заданиями выгрузки в DWH; ручной запуск — POST /admin/data-exchange/jobs/{code}/runs — идёт тем же путём, что и плановый (тот же срез, тот же журнал прогонов, тот же сдвиг водяного знака). Задания засеяны выключенными. Журнал прогонов — GET /admin/data-exchange/jobs/runs.

Выгрузка витрин (DTP)

Переливка витрин (DWH_TRANSACTIONS, DWH_BALANCES, DWH_CUSTOMERS) в целевую БД заказчика — по образцу платформенного DTP: соответствие полей задаётся запросом вставки из конфигурации, выгрузка выключена, пока приёмник не настроен (LOYALTY_DTP_ENABLED, адрес и учётные данные целевой БД). Метрика отставания — loyalty.exchange.lag.seconds. Витрина проводок DWH_TRANSACTIONS отдаёт клиента и субсчёт по каждой ноге проводки (debitCustomerExternalId/creditCustomerExternalId, debitSubaccountCode/creditSubaccountCode), поэтому получатель перевода баллов между клиентами виден в витрине наравне с отправителем. Поставляемый по умолчанию запрос вставки этой витрины ещё рассчитан на прежний состав полей — перед включением переливки задайте свой (см. «Конфигурация» → «Обмен данными с внешними системами»). Расписания заданий трактуются в поясе программы.

Интеграции по тегам и меткам

Внешние системы могут ссылаться на клиентские теги и метки акций по значению, не зная их внутренних идентификаторов.

  • Создание клиента с тегами. POST /v1/customers принимает tags — список значений: недостающие теги заводятся в справочнике автоматически, но только при наличии роли LOYALTY_TAG_WRITE у вызывающего клиента; без неё неизвестное значение отклоняет создание клиента целиком (422 unknown_tag). Помимо LOYALTY_TAG_WRITE, вызов с непустыми tags/tagIds требует и роль LOYALTY_CUSTOMER_TAG_WRITE (создание с тегами — это ещё и присвоение). Форма Core UI использует параллельное поле tagIds (только существующие теги).
  • Массовая проверка наличия. POST /v1/tags/lookup — по списку значений (до 1000 за вызов) проверяет, какие теги уже есть в справочнике: найденные возвращаются с id, ненайденные — как пришли; сам вызов ничего не заводит. Симметричная ручка для меток акций — POST /v1/promotion-labels/lookup.
  • Создание акции с метками. POST /v1/promotions принимает labels — список значений: недостающие метки заводятся под ролью LOYALTY_PROMOTION_LABEL_WRITE, иначе — 422 unknown_label.

Потоковый decisioning RT Flow / SDE — что ещё в дорожной карте

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

Синхронные вызовы (проверка условий, начисление) уже соединяют RT Flow и Loyalty, а события смены уровня и уведомления клиентам уходят в брокер через очередь исходящих (см. «Исходящие события»). Не реализована потоковая поставка событий бонусных операций в RT Flow / SDE (Apache Flink) для полностью real-time decisioning без запроса к Loyalty на каждое условие — вместе с кэшем справочников для обогащения потока.

Безопасность интеграций

Аспект Значение
Аутентификация OAuth2 resource server, JWT Keycloak
Авторизация LOYALTY_INTEGRATION_WRITE (запись), LOYALTY_INTEGRATION_READ (резолюция/чтение), LOYALTY_EXCHANGE_READ/WRITE (обмен данными), LOYALTY_INTAKE_WRITE/READ (очередь приёма), LOYALTY_SEGMENT_INTAKE (приём сегментов) — @PreAuthorize, deny-by-default
Ограничение частоты GCRA-лимитер на весь интеграционный периметр, детали — «Интеграционный периметр»
Идемпотентность Заголовок Idempotency-Key на всех изменяющих баланс вызовах; ключ действителен только для своей операции
Ошибки RFC 7807 (application/problem+json) со стабильным code и traceId
Наблюдаемость JSON-логи, метрики Prometheus, сквозной traceId

Обработка ошибок вызывающей стороной

Ответы об ошибках возвращаются в формате RFC 7807. Ответы 4xx (ошибка валидации, клиент не найден) повторять не следует — их направляют в dead-letter / ручной разбор. Ответы 5xx, таймауты и сетевые сбои безопасно ретраить с backoff: благодаря Idempotency-Key повтор не приведёт к двойному начислению.

Ошибка валидации — validation_failed, а не bad_request

Нарушение ограничений тела или параметров запроса (обязательное поле, диапазон, размер) на интеграционных точках — чек POS, начисление и сторно Promo, начисление и проверка условий RT Flow, лента изменений — отвечает 400 validation_failed; поле detail перечисляет нарушения в виде поле: сообщение. Код bad_request остаётся только для нечитаемого тела или значения, которое не разбирается (например, не число). Интеграции, сопоставлявшие отказ валидации с bad_request, нужно перенастроить.

Точки интеграции: сводная таблица

Точка Эндпоинт(ы) Роль Статус
Чек POS POST /v1/integration/pos/receipts LOYALTY_INTEGRATION_WRITE Реализовано
Возврат по чеку и его отмена POST /v1/integration/pos/returns, .../{returnId}/cancel LOYALTY_INTEGRATION_WRITE Реализовано
Резолюция карты POS GET /v1/integration/pos/cards/{cardId} LOYALTY_INTEGRATION_READ Реализовано
Проекция клиента Check Scan GET /v1/integration/checkscan/customers/{externalId} LOYALTY_INTEGRATION_READ Реализовано
Начисление / сторно Promo POST /v1/integration/promo/accruals, .../reversals LOYALTY_INTEGRATION_WRITE Реализовано
Проверка условий RT Flow POST /v1/integration/rtflow/condition-checks LOYALTY_INTEGRATION_READ Реализовано
Начисление RT Flow POST /v1/integration/rtflow/accruals LOYALTY_INTEGRATION_WRITE Реализовано
Приём команд (API) POST /v1/intake/commands LOYALTY_INTAKE_WRITE Реализовано
Приём команд (Kafka) брокер, топик loyalty.intake.commands — Реализовано, выключено по умолчанию
Приём сегментов клиентов /v1/integration/customer-segments/** LOYALTY_SEGMENT_INTAKE Реализовано
Сегменты клиента GET /v1/integration/customers/{externalId}/segments LOYALTY_INTEGRATION_READ Реализовано
Событие смены уровня брокер, топик loyalty.tiers.tier-changed — Реализовано, выключено по умолчанию
Уведомления клиентам брокер, топик loyalty.notifications.customer-message — Реализовано, выключено по умолчанию
Ленты изменений GET /v1/admin/data-exchange/feeds/** LOYALTY_EXCHANGE_READ Реализовано
Регламент и выгрузка DWH .../data-exchange/jobs/**, DTP LOYALTY_EXCHANGE_WRITE/READ Реализовано, выключено по умолчанию
Теги/метки по значению POST /v1/tags/lookup, /v1/promotion-labels/lookup, tags/labels при создании по контексту Реализовано
Профиль из MDM адаптер identity — Реализовано на уровне идентификатора
Потоковая поставка бонусных операций в SDE — — Дорожная карта