Интеграции¶
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> |
Ответ:
Обратный эффект — сторнирование начисления — 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 | — | — | Дорожная карта |