Личный кабинет клиента (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.
Почему отдельная цепочка
Административный контур авторизуется по ролям 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 отсутствует). Подробности содержимого карты и настройка сертификатов — в
Карта участника в мобильном кошельке и
Конфигурация.
Пример запроса обзора:
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 в теле.
См. также¶
- Описание личного кабинета со стороны конечного пользователя — Личный кабинет клиента: обзор
- Достижения и бейджи
- Карта участника в мобильном кошельке
- Партнёрские предложения и взаиморасчёты