Партнёрские предложения и взаиморасчёты¶
Партнёрские предложения — это совместные акции лояльности на продуктах партнёров (например, банк показывает в своём кабинете скидку партнёрского кафе). Модуль объединяет CM Ocean Loyalty и CM Ocean Promo: Loyalty ведёт реестр партнёров, конструктор офферов и card-linked начисление, а редемпшн промокодов остаётся за Promo.
Эта страница описывает администрирование партнёрских предложений: приём партнёрских транзакций (card-linked начисление) и взаиморасчёты с партнёром (сетлмент). Бизнес-модель оффера — четыре ортогональные оси, вознаграждение, ограничения, жизненный цикл — подробно разобрана в руководстве пользователя: см. Партнёрские предложения — обзор.
Обзор осей оффера¶
Оффер (partner_offer) описывается четырьмя ортогональными осями — они задаются независимо и в
сумме определяют, как предложение доставляется клиенту, как активируется, кому показывается и кто за
него платит.
| Ось | Значения | Что определяет |
|---|---|---|
Доставка (delivery) |
PROMOCODE | CALCULATED |
Промокод (редемпшн ведёт CM Ocean Promo) либо расчёт кэшбэка самой программой Loyalty |
Акцепт (acceptance) |
OPT_IN | AUTO |
Клиент активирует предложение сам либо оно доступно сразу, без активации |
Аудитория (audience) |
ALL | SEGMENT |
Предложение доступно всем либо только заданным сегментам |
Финансирование (funding) |
PARTNER | PROGRAM | SHARED |
Кто оплачивает вознаграждение: партнёр, программа или совместно |
Для card-linked начисления и взаиморасчётов, описанных ниже, определяющими являются оси доставки
(runtime-начисление работает по офферам CALCULATED) и финансирования (funding задаёт счёт-источник
проводки в леджере).
Разделение с CM Ocean Promo
Офферы с доставкой PROMOCODE погашаются на стороне CM Ocean Promo по промокоду и кампании
(promoCampaignId). Card-linked начисление, описанное на этой странице, относится к офферам
CALCULATED, где вознаграждение рассчитывает и начисляет сама программа Loyalty по механике
(диаграмме RT Flow).
Card-linked начисление¶
Card-linked начисление вводит в петлю лояльности транзакции, совершённые клиентом у партнёра: по идентификатору мерчанта система резолвит партнёра, подбирает применимые офферы, рассчитывает вознаграждение и начисляет кэшбэк на счёт клиента. Эндпоинт вызывает диаграмма RT Flow или шлюз партнёра.
Эндпоинт. POST /v1/integration/partner/transactions
| Заголовок | Назначение |
|---|---|
Idempotency-Key |
Ключ exactly-once при ретраях; по умолчанию равен externalRef из тела |
Authorization |
Bearer <JWT>, роль LOYALTY_INTEGRATION_WRITE |
Тело запроса — партнёрская транзакция клиента:
{
"externalId": "CUST-77f3a0",
"amountMinor": 120000,
"externalRef": "PTX-2026-07-01-000513",
"partnerCode": "CAFE-AURORA"
}
Партнёр адресуется одним из трёх идентификаторов — partnerCode, mid или mcc (в порядке
приоритета резолва):
| Поле | Тип | Обязательность | Описание |
|---|---|---|---|
externalId |
строка | да | Идентификатор клиента в Loyalty (согласован с MDM) |
amountMinor |
целое > 0 | да | Сумма чека у партнёра в минорных единицах |
externalRef |
строка | да | Бизнес-ключ транзакции; по умолчанию используется как Idempotency-Key |
partnerCode |
строка | один из трёх | Код партнёра из реестра |
mid |
строка | один из трёх | Идентификатор мерчанта (Merchant ID) из справочника мерчантов |
mcc |
строка | один из трёх | Код категории мерчанта (Merchant Category Code) |
Справочник мерчантов¶
MID партнёра ведутся не строкой в карточке партнёра, а справочником мерчантов
/v1/admin/merchants (роли LOYALTY_MERCHANT_READ / LOYALTY_MERCHANT_WRITE): код мерчанта — MID
(уникален, хранится как есть), название, партнёр, MCC, город (регион выводится через город) и статус.
Мерчанта не удаляют, а закрывают (POST /v1/admin/merchants/{code}/status): на него ссылаются
отработавшие акции. Поля merchantIds и mcc удалены из контракта партнёра (POST/PUT /v1/partners):
переданные значения не сохраняются, а существовавшие перенесены в справочник мерчантов тем же MID
(одинаковый MID у разных партнёров отдан первому по коду партнёру, остальные записаны в лог применения
миграции). Удалить партнёра, за которым числится мерчант, нельзя.
Экрана справочника мерчантов в веб-интерфейсе пока нет
Мерчанты ведутся только через API /v1/admin/merchants. Форма партнёра в разделе
«Партнёрские предложения» ещё показывает поля «MID» и «MCC», но бэкенд их больше не принимает:
введённые там значения не сохраняются и на резолв партнёра не влияют.
Ответ содержит применённые офферы и итоговый баланс клиента:
{
"partnerCode": "CAFE-AURORA",
"applied": [
{
"offerId": "9f13...",
"offerCode": "AURORA-CB5",
"title": "Кэшбэк 5% в кафе «Аврора»",
"rewardMinor": 6000
}
],
"totalRewardMinor": 6000,
"balanceMinor": 131860
}
| Поле | Тип | Описание |
|---|---|---|
partnerCode |
строка | Код резолвленного партнёра |
applied[] |
массив | Применённые офферы (пустой, если ни один оффер не сработал) |
applied[].offerId |
строка | Идентификатор оффера |
applied[].offerCode |
строка | Код оффера — человекочитаемый идентификатор вида PO-0000001, выдаётся последовательностью БД при создании оффера |
applied[].title |
строка | Название оффера |
applied[].rewardMinor |
целое | Начислено по офферу, в минорных единицах |
totalRewardMinor |
целое | Суммарно начислено по всем применённым офферам |
balanceMinor |
целое | Итоговый баланс клиента после начисления |
Пайплайн начисления¶
Обработка транзакции повторяет схему CLO-сетей (card-linked offers): resolve merchant → eligibility → attribute & reward.
flowchart TD
A["Транзакция партнёра"] --> B["1. Резолв партнёра<br/>partnerCode → MID → MCC"]
B --> C["2. Отбор применимых<br/>CALCULATED-офферов"]
C --> D["3. Расчёт вознаграждения<br/>PERCENT / POINTS / FIXED"]
D --> E["4. Лимиты и бюджет"]
E --> F["5. Идемпотентность"]
F --> G["Начисление<br/>источник PARTNER"]
- Резолв партнёра. Сначала по
partnerCode(реестр партнёров); затем поmid— прямой поиск мерчанта в справочнике с точным сравнением, с учётом регистра (обрамляющие пробелы снимаются), — партнёром считается партнёр мерчанта, в том числе закрытого; затем поmcc— только среди действующих мерчантов и только если MCC однозначно указывает на одного партнёра. НеизвестныйpartnerCodeотклоняется422 partner_unknown; если партнёр не выведен ни по MID, ни по MCC —422 partner_unresolved. Транзакции, у которых по MID не нашёлся мерчант или мерчант не отнесён к партнёру, считает показательloyalty.partner.transaction.mid.unresolved. - Отбор применимых офферов. Из офферов партнёра берутся только
CALCULATEDсо статусомACTIVE, попадающие в период (обе границыstartsAt/endsAt— включительно, с точностью до секунды), прошедшие проверку аудитории (сегменты клиента — текущий состав, залитый внешней системой, см. «Интеграции»), акцепта (дляOPT_IN— активированные клиентом), минимальной суммы чека (minAmount) и признака «только новым» (newCustomerOnly— у клиента нет прежних атрибуций у этого партнёра). - Расчёт вознаграждения. По
valueKind:PERCENT— процент от суммы с ограничениемmaxReward(maxRewardMinor);POINTS— фиксированные баллы;FIXED— фиксированная сумма. НоминалPOINTSиFIXEDзадан в баллах и переводится в минорные единицы по точности валюты баллов программы (у программы с целыми баллами «5 баллов» — это 5, а не 500 минорных единиц). Вознаграждение округляется до целого по правилуHALF_UP— так же, как начисление по механикам акций. - Лимиты и бюджет. Проверяются лимит на клиента (
perCustomerLimit) и остаток бюджета. При исчерпании бюджета оффер пропускается и автоматически переводится в статусPAUSED. - Идемпотентность. Повтор транзакции не начисляет вознаграждение повторно (см. ниже).
Итоговое начисление помечается источником PARTNER — в истории операций клиента оно отображается как
«Кэшбэк партнёра».
Три уровня идемпотентности и авто-пауза по бюджету
Защита от двойного начисления работает на трёх уровнях: (1) ключ запроса Idempotency-Key
(по умолчанию externalRef) на границе контроллера; (2) уникальность атрибуции
partner_offer_redemption UNIQUE(offer_id, transaction_ref); (3) backstop ядра леджера по
ledger_posting_reference (ADR 0002). Повторная отправка той же транзакции безопасна и не создаёт
второй проводки. Отдельно: при исчерпании бюджета оффер не просто пропускается, но и
авто-переводится в PAUSED, поэтому последующие транзакции по нему не обрабатываются до
ручного возобновления.
Код оффера — из последовательности БД
Код оффера (PO-0000001, семь цифр) выдаётся последовательностью базы данных при создании —
это гарантирует уникальность кода при любой частоте создания офферов. Офферы, заведённые ранее,
сохраняют прежний шестизначный код (PO-000001) — он не мигрируется и остаётся действительным,
поскольку новая семизначная нумерация физически не может с ним совпасть.
Взаиморасчёты (сетлмент)¶
Партнёрские начисления учитываются в двойном леджере так, чтобы источник финансирования каждого вознаграждения был прослеживаем. Это основа для выставления счёта партнёру.
План счетов. Статья PRTF «Финансирование партнёров» (миграция 0045). Леджер-счёт партнёра
PRT-<code> заводится лениво — при первом partner-funded начислении по этому партнёру.
Счёт-источник по оси финансирования. Ось funding оффера определяет, с какого счёта списывается
финансирование вознаграждения:
Финансирование (funding) |
Счёт-источник в леджере |
|---|---|
PARTNER |
Счёт партнёра PRT-<code> |
SHARED |
Счёт партнёра PRT-<code> |
PROGRAM |
Системный источник начислений (accrual.source-account) |
Технический шов — PointsAccrualPort.accrue(..., sourceAccountNumber): номер счёта-источника
подставляется по оси финансирования оффера.
SHARED: один лег в леджере, доля программы — расчётная
Для офферов SHARED в леджере создаётся один лег — со счёта партнёра (PRT-<code>). Доля
программы (50 %) не проводится отдельной проводкой, а является расчётной строкой отчёта
взаиморасчётов (sharedProgramShareMinor). Таким образом, весь дебет по счёту партнёра отражает
сумму, профинансированную партнёром, а совместное финансирование раскрывается только в отчёте.
Отчёт взаиморасчётов¶
Эндпоинт. GET /v1/partners/{id}/settlement?days=N, роль LOYALTY_PROMOTION_READ.
Отчёт возвращает разбивку по офферам за окно N дней (по умолчанию 30; значение вне диапазона 1–365
приводится к ближайшей границе) и
агрегированные итоги. Границы окна берутся одним снимком времени и возвращаются в ответе (from, to),
поэтому строки отчёта и итоги по леджеру относятся к одному и тому же окну.
{
"partnerId": "4b1c...",
"partnerCode": "CAFE-AURORA",
"partnerName": "Кафе «Аврора»",
"days": 30,
"from": "2026-08-31T10:00:00+03:00",
"to": "2026-09-30T10:00:00+03:00",
"fundingAccount": "PRT-CAFE-AURORA",
"offers": [
{
"offerId": "9f13...",
"code": "AURORA-CB5",
"title": "Кэшбэк 5% в кафе «Аврора»",
"funding": "PARTNER",
"redemptions": 214,
"amountMinor": 25680000,
"rewardMinor": 1284000
},
{
"offerId": "b7a2...",
"code": "AURORA-WELCOME",
"title": "Приветственный кэшбэк «Аврора»",
"funding": "SHARED",
"redemptions": 48,
"amountMinor": 5760000,
"rewardMinor": 288000
}
],
"rewardTotalMinor": 1572000,
"partnerFundedMinor": 1428000,
"programFundedMinor": 0,
"sharedProgramShareMinor": 144000,
"fundedInPeriodMinor": 1428000,
"fundedTotalMinor": 1428000
}
| Поле | Тип | Описание |
|---|---|---|
partnerId, partnerCode, partnerName |
строка | Партнёр отчёта |
days, from, to |
целое, дата-время | Ширина окна в днях и его фактические границы |
fundingAccount |
строка | Номер счёта партнёра PRT-<code>; отсутствует, если счёт ещё не заведён |
offers[] |
массив | Разбивка по офферам за окно N дней |
offers[].offerId |
строка | Идентификатор оффера |
offers[].code |
строка | Код оффера |
offers[].title |
строка | Название оффера |
offers[].funding |
строка | Ось финансирования оффера (PARTNER / PROGRAM / SHARED) |
offers[].redemptions |
целое | Число атрибуций (сработавших начислений) за окно |
offers[].amountMinor |
целое | Оборот (сумма чеков) по офферу за окно |
offers[].rewardMinor |
целое | Начислено вознаграждений по офферу за окно |
rewardTotalMinor |
целое | Суммарно начислено вознаграждений за окно |
partnerFundedMinor |
целое | Профинансировано партнёром за окно |
programFundedMinor |
целое | Профинансировано программой за окно |
sharedProgramShareMinor |
целое | Расчётная доля программы в SHARED-офферах за окно |
fundedInPeriodMinor |
целое | Профинансировано партнёром за окно — сумма дебет-проводок со счёта партнёра между from и to |
fundedTotalMinor |
целое | Профинансировано партнёром за всё время — сумма дебет-проводок со счёта партнёра за всю историю счёта |
Счёт за период — по fundedInPeriodMinor
Для выставления счёта партнёру за период используйте fundedInPeriodMinor: это сумма дебет-проводок
со счёта партнёра PRT-<code> за то же окно, что и строки отчёта, — она сопоставима со строками
offers[] и оконными итогами. fundedTotalMinor — сумма за всю историю счёта; раньше отчёт отдавал
только её, и счёт за период мог включить вознаграждения прошлых периодов. Обе величины считаются по
проводкам леджера, а не по атрибуциям.
Денежная история не удаляется¶
Удаление, которое стёрло бы историю начислений партнёра, запрещено — такие объекты архивируют:
| Что удаляется | Когда запрещено | Ответ |
|---|---|---|
| Оффер | Оффер в статусе ACTIVE либо по нему есть атрибуции (сработавшие начисления) |
409 partner_offer_active / 409 partner_offer_in_use |
| Партнёр | У партнёра есть офферы, проводки по счёту финансирования PRT-<code> или мерчанты в справочнике |
409 partner_in_use |
Запрет на партнёра с проводками защищает и от подмены истории: номер счёта финансирования строится из кода партнёра, и партнёр, заведённый позже с тем же кодом, иначе получил бы чужие исторические проводки.
Роли и доступ¶
| Функция | Требуемая роль |
|---|---|
Приём партнёрских транзакций (POST /v1/integration/partner/transactions) |
LOYALTY_INTEGRATION_WRITE |
Реестр партнёров и офферы — чтение (GET /v1/partners, GET /v1/partner-offers, отчёт взаиморасчётов) |
LOYALTY_PROMOTION_READ |
| Реестр партнёров и офферы — изменение (CRUD, переходы статуса) | LOYALTY_PROMOTION_WRITE |
Справочник мерчантов — чтение / изменение (/v1/admin/merchants) |
LOYALTY_MERCHANT_READ / LOYALTY_MERCHANT_WRITE |
Безопасность
Все эндпоинты защищены OAuth2 (resource server на Keycloak, JWT) с принципом deny-by-default.
Приём партнёрских транзакций отделён от администрирования офферов: runtime-начисление требует
интеграционной роли LOYALTY_INTEGRATION_WRITE, а конструктор офферов и отчёт взаиморасчётов —
ролей LOYALTY_PROMOTION_READ / LOYALTY_PROMOTION_WRITE.