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

Личный кабинет клиента (B2C): API и SDK

Контур B2C — это отдельный клиентский периметр CM Ocean Loyalty для показа лояльности конечному клиенту. Он даёт ритейлеру собрать собственный сайт или мобильное приложение поверх Loyalty: клиент видит свою карту, баланс, уровень, доступные акции и партнёрские офферы. Контур доступен только на чтение (плюс акцепт и отмена партнёрских офферов) и работает поверх токена клиента, а не сотрудника.

B2C опционален

Контур личного кабинета — опциональная часть развёртывания. Если ритейлер не строит собственный клиентский фронтенд поверх Loyalty, B2C можно не разворачивать: соответствующий шаг настройки (клиентский scope и маппер в Keycloak) просто пропускается. Административный и интеграционный контуры от этого не зависят.

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

flowchart LR
    APP["Сайт / приложение клиента"] -->|"Bearer JWT (scope loyalty.b2c)"| B2C
    KC["Keycloak\n(сервер авторизации)"] -->|выпуск JWT клиента| APP
    B2C["/v1/b2c/me/**\n(отдельная security-цепочка)"] --> LOY["loyalty-billing"]
    B2C -.->|идентификатор из токена| CID["loyalty_external_id\n→ preferred_username → sub"]
Свойство Значение
Базовый путь /v1/b2c/me/**
Периметр Отдельная security-цепочка, независимая от административного API
Требование доступа OAuth2-скоуп loyalty.b2c в токене клиента
Идентификатор клиента Из токена (claim), не из пути запроса
Режим доступа Чтение + акцепт/отмена партнёрских офферов
Токен JWT клиента (не сотрудника)

Security-цепочка /v1/b2c/**

Пути /v1/b2c/** обслуживаются отдельной security-цепочкой — она независима от административного контура и предъявляет собственное требование: в access-токене должен присутствовать OAuth2-скоуп loyalty.b2c. Токен без этого скоупа к контуру B2C не допускается, даже если он валиден для других частей API.

Authorization: Bearer <JWT клиента со scope loyalty.b2c>

Почему отдельная цепочка

Административный контур авторизуется по ролям LOYALTY_* из токена сотрудника, а B2C — по скоупу loyalty.b2c из токена клиента. Разделение цепочек не даёт клиентскому токену получить доступ к административным операциям и наоборот.

Идентификатор клиента и защита «только свои данные»

Идентификатор клиента для контура B2C берётся из токена, а не из пути запроса. Значение читается из клеймов по следующему приоритету:

Приоритет Клейм токена
1 loyalty_external_id
2 preferred_username
3 sub

Так как эндпоинты адресуют данные как «мои» (/me), а не по идентификатору в пути, клиент физически не может запросить чужие данные: сервис всегда работает с тем клиентом, чей идентификатор извлечён из предъявленного токена.

Не полагайтесь на идентификатор в пути

В контуре B2C нет пути вида /v1/b2c/{customerId}/.... Любой запрос интерпретируется в контексте владельца токена. Это ключевая гарантия изоляции данных: подмена чужого идентификатора в запросе невозможна, потому что идентификатор в запросе не передаётся.

Предпросмотр кабинета сотрудником

Кроме клиентского токена, контур /v1/b2c/** принимает staff-токен с ролью LOYALTY_PROFILE_READ — это не расширение прав: обладатель этой роли и так читает карточку любого клиента через /v1/customers/{externalId}/card. Такой запрос открывает кабинет не владельца токена, а участника, указанного явно:

Параметр запроса Кто его читает Поведение
previewExternalId (query, необязательный) Только staff-токен Открывает кабинет указанного участника; если не передан — кабинет сконфигурированного дефолта (loyalty.b2c.preview-external-id, по умолчанию клиент демо-сида PLT-1002)

Клиентский токен (со скоупом loyalty.b2c) параметр previewExternalId не читает ни при каких условиях — для него идентификатор всегда берётся из токена; переданное клиентом значение молча игнорируется, а не отклоняется ошибкой.

Зачем это нужно

Предпросмотр решает две задачи: открыть кабинет на тестовом стенде без клиентского IdP и показать сотруднику поддержки, что видит конкретный клиент. И loyalty-ui (кабинет, встроенный в стенд по пути /cabinet/:externalId?), и b2c-sdk (опция getPreviewExternalId) прокидывают этот параметр только для staff-сценариев.

Эндпоинты /v1/b2c/me/**

Все эндпоинты контура работают в контексте клиента, определённого по токену.

Метод и путь Назначение
GET /v1/b2c/me Обзор: карта клиента, баланс, уровень
GET /v1/b2c/me/transactions?limit История операций клиента
GET /v1/b2c/me/offers Доступные клиенту акции — по тем же правилам условий, что у движка начисления (см. ниже)
GET /v1/b2c/me/partner-offers Партнёрские офферы, доступные клиенту
POST /v1/b2c/me/partner-offers/{id}/accept Активация OPT_IN-оффера (акцепт)
POST /v1/b2c/me/partner-offers/{id}/revoke Отмена ранее активированного оффера
GET /v1/b2c/me/achievements Бейджи клиента — см. Достижения и бейджи
GET /v1/b2c/me/wallet/apple Пакет карты для Apple Wallet (application/vnd.apple.pkpass)
GET /v1/b2c/me/wallet/google Объект карты и ссылка сохранения для Google Кошелька

Какие акции видит клиент

Витрина GET /v1/b2c/me/offers отбирает акции тем же правилом разбора условий, что и движок начисления: клиент видит акцию, если проходит её условия по сегментам (включающие и исключающие) и добавленные условия по полям своего профиля. Сегменты клиента — текущий состав, залитый внешней системой (см. «Интеграции» → «Сегменты клиентов из внешней системы»). Остальные условия — в том числе зависящие от самой покупки (товары, сумма чека, торговая точка, переменные покупки), — до покупки не проверяются и акцию не скрывают. Акция, в условии которой указан удалённый сегмент, не показывается — движок её тоже никому не применяет. Акция с испорченными условиями не скрывается, а ошибка разбора пишется в журнал.

Доступно к списанию

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

Видимость промокода

Промокод партнёрского оффера возвращается клиенту только для AUTO-офферов (доступных без активации) или после акцепта OPT_IN-оффера. До акцепта OPT_IN-оффера промокод в ответе не отдаётся.

Мобильный кошелёк

Обе точки кошелька живут в том же клиентском контуре: идентификатор участника берётся из токена, поэтому выпустить чужую карту нельзя. Пока сертификат/ключ подписи не настроены, точки отвечают 404 wallet_disabled (настройка loyalty.wallet.enabled, по умолчанию выключена). Пакет Apple без настоящей подписи всё равно возвращается — с заголовком X-Loyalty-Pass-Signed: false; ссылка сохранения Google без подписи не собирается вовсе (поле saveUrl отсутствует). Подробности содержимого карты и настройка сертификатов — в Карта участника в мобильном кошельке и Конфигурация.

Пример запроса обзора:

GET /v1/b2c/me HTTP/1.1
Host: loyalty.example.ru
Authorization: Bearer <JWT клиента>

JS/TS-SDK

Для клиентского фронтенда поставляется JS/TS-SDK (loyalty-ui/src/b2c-sdk), инкапсулирующий вызовы контура B2C. Методы SDK соответствуют эндпоинтам:

Метод SDK Эндпоинт
overview() GET /v1/b2c/me
transactions() GET /v1/b2c/me/transactions
offers() GET /v1/b2c/me/offers
partnerOffers() GET /v1/b2c/me/partner-offers
acceptPartnerOffer(id) POST /v1/b2c/me/partner-offers/{id}/accept
revokePartnerOffer(id) POST /v1/b2c/me/partner-offers/{id}/revoke
achievements() GET /v1/b2c/me/achievements
googleWalletPass() GET /v1/b2c/me/wallet/google
applePass() GET /v1/b2c/me/wallet/apple (возвращает пакет и признак signed)

Сериализация идентификаторов-снежинок

Идентификаторы-снежинки (snowflake id) сериализуются строкой. SDK разбирает ответы без потери точности для значений больше 2^53, поэтому идентификаторы следует передавать и хранить как строки, не приводя их к числовому типу JavaScript.

Настройка Keycloak

Для включения контура B2C в Keycloak настраиваются два элемента:

Элемент Значение
Клиентский scope loyalty.b2c (назначается как default для клиентского приложения)
Маппер Claim loyalty_external_id в access-токене клиента

Скоуп loyalty.b2c даёт токену клиента доступ к security-цепочке /v1/b2c/**. Маппер добавляет в токен клейм loyalty_external_id — приоритетный источник идентификатора клиента (см. таблицу приоритета клеймов выше); при его отсутствии контур опускается на preferred_username, затем на sub.

Шаг пропускается, если B2C не разворачивается

Настройка клиентского scope и маппера относится только к контуру личного кабинета. Если B2C в конкретном развёртывании не используется, этот шаг Keycloak пропускается без последствий для остального модуля.

CORS

Клиентский фронтенд обращается к /v1/b2c/** из браузера, поэтому список разрешённых origin настраивается отдельно от административного контура:

Настройка Переменная окружения Поведение
loyalty.security.b2c-cors-allowed-origins LOYALTY_B2C_CORS_ALLOWED_ORIGINS Origin, которым разрешено обращаться к /v1/b2c/**

Если список для B2C не задан (пусто), действует общий список loyalty.security.cors-allowed-origins (LOYALTY_CORS_ALLOWED_ORIGINS) — это удобно, когда клиентский и операторский интерфейсы развёрнуты на одном домене и достаточно одного списка.

Ограничение частоты запросов

Контур /v1/b2c/** покрыт общим механизмом rate-limit (см. Интеграции): ключ — субъект клиентского токена, счёт ведётся по каждому клиенту отдельно.

Правило Лимит по умолчанию Настройка Код ответа
b2c — весь /v1/b2c/** 120 запросов/мин LOYALTY_RATE_LIMIT_B2C (лимит), LOYALTY_RATE_LIMIT_B2C_MODE (режим, по умолчанию OBSERVE) 429 b2c_rate_limited

В режиме OBSERVE превышение только пишется в метрику и лог, без отказа клиенту; переход в ENFORCE — после обкатки порога на реальном трафике. Ответ 429 несёт заголовок Retry-After и поле retryAfterSeconds в теле.

См. также