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

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

Партнёрские предложения — это совместные акции лояльности на продуктах партнёров (например, банк показывает в своём кабинете скидку партнёрского кафе). Модуль объединяет 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"]
  1. Резолв партнёра. Сначала по partnerCode (реестр партнёров); затем по mid — прямой поиск мерчанта в справочнике с точным сравнением, с учётом регистра (обрамляющие пробелы снимаются), — партнёром считается партнёр мерчанта, в том числе закрытого; затем по mcc — только среди действующих мерчантов и только если MCC однозначно указывает на одного партнёра. Неизвестный partnerCode отклоняется 422 partner_unknown; если партнёр не выведен ни по MID, ни по MCC — 422 partner_unresolved. Транзакции, у которых по MID не нашёлся мерчант или мерчант не отнесён к партнёру, считает показатель loyalty.partner.transaction.mid.unresolved.
  2. Отбор применимых офферов. Из офферов партнёра берутся только CALCULATED со статусом ACTIVE, попадающие в период (обе границы startsAt/endsAt — включительно, с точностью до секунды), прошедшие проверку аудитории (сегменты клиента — текущий состав, залитый внешней системой, см. «Интеграции»), акцепта (для OPT_IN — активированные клиентом), минимальной суммы чека (minAmount) и признака «только новым» (newCustomerOnly — у клиента нет прежних атрибуций у этого партнёра).
  3. Расчёт вознаграждения. По valueKind: PERCENT — процент от суммы с ограничением maxReward (maxRewardMinor); POINTS — фиксированные баллы; FIXED — фиксированная сумма. Номинал POINTS и FIXED задан в баллах и переводится в минорные единицы по точности валюты баллов программы (у программы с целыми баллами «5 баллов» — это 5, а не 500 минорных единиц). Вознаграждение округляется до целого по правилу HALF_UP — так же, как начисление по механикам акций.
  4. Лимиты и бюджет. Проверяются лимит на клиента (perCustomerLimit) и остаток бюджета. При исчерпании бюджета оффер пропускается и автоматически переводится в статус PAUSED.
  5. Идемпотентность. Повтор транзакции не начисляет вознаграждение повторно (см. ниже).

Итоговое начисление помечается источником 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.