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

Справочники и НСИ

Модуль CM Ocean Loyalty использует гибридную модель справочников (НСИ). Большинство справочников ведётся в самом модуле: операционные словари — общим механизмом записей, системные сущности — в таблицах своих подсистем, а справочники со сложной структурой (теги, метки, сегменты, география, мерчанты, каталог товаров) — на собственных экранах со своим API и своими ролями. Подключение внешней системы НСИ платформы (dictionary-api) в режиме только чтения сохранено, но в текущем реестре внешних справочников нет. Единая точка входа — раздел «Администрирование → Справочники» во фронтенде и API-фасад /v1/admin/dictionaries: фасад маршрутизирует запрос к нужному источнику, а для справочников со своим экраном служит только точкой входа.

Гибридная модель: источники данных

Каждый справочник в реестре имеет признак источника (source), признак редактируемости (editable) и вид экрана (ui: GENERIC — общий экран записей, OWN_SCREEN — собственный экран). От них зависит, можно ли изменять записи и где находится «система-владелец».

Источник Где хранится Доступ Назначение
LOCAL Локальная таблица reference_entry модуля Чтение и запись (CRUD + ЖЦ) через фасад Операционные словари, которыми владеет модуль лояльности
LOCAL (адаптер) Таблицы подсистем модуля (валюты, план счетов, единицы измерения, механики) Валюты, план счетов, единицы — только чтение; механики — создание, правка, удаление без ЖЦ Системные сущности, у которых уже есть своя «система-владелец» внутри модуля
LOCAL (свой экран) Собственные таблицы фичи Через свой API и свою пару ролей; фасад отвечает 409 dictionary_own_screen Теги, метки акций, сегменты, города, торговые точки, мерчанты, категории товаров, SKU
REMOTE Внешний dictionary-api платформы Только чтение Корпоративные мастер-данные НСИ; сейчас в реестре таких справочников нет

Логика маршрутизации

Единый фасад справочников определяет источник по реестру и направляет запрос:

  • LOCAL — в локальный сервис справочников (reference_entry): полный CRUD, жизненный цикл и аудит;
  • LOCAL-адаптеры (currency, system-account, unit) — в соответствующие сервисы модуля в режиме «только чтение» (система-владелец — там); механики (mechanic) — в сервис механик;
  • справочники со своим экраном — никуда: чтение записей и журнала, запись и смена статуса по их ключу отвечают 409 dictionary_own_screen;
  • REMOTE — во внешний dictionary-api в режиме «только чтение».

Внешний источник и его недоступность

Подключение внешнего dictionary-api задаётся переменными LOYALTY_DICTIONARY_ENABLED (по умолчанию false) и LOYALTY_DICTIONARY_BASE_URL. Категории товаров и SKU, раньше приходившие оттуда, стали собственными справочниками продукта (см. «Каталог товаров»), поэтому в текущем реестре REMOTE-справочников нет. Если такой справочник появится, а dictionary-api будет отключён или недоступен, реестр и ответ по записям вернут признак available=false и пустой список записей вместо ошибки, чтобы недоступность внешнего источника не блокировала работу с остальными справочниками.

Запись только для редактируемых справочников общего экрана

Любая операция изменения через фасад (создание/обновление записи, смена статуса, удаление) допустима только для справочников общего экрана с источником LOCAL и признаком editable=true. Попытка изменить справочник только для просмотра отклоняется 409 dictionary_read_only с причиной: у локального — «он ведётся в своём разделе», у внешнего — «он мастерится во внешней системе».

Жизненный цикл записи: DRAFT / ACTIVE / ARCHIVED

Записи локальных справочников (reference_entry) имеют статус жизненного цикла. Это позволяет готовить значения заранее и выводить устаревшие из обращения без физического удаления.

flowchart LR
    DRAFT["DRAFT\n(черновик)"] --> ACTIVE["ACTIVE\n(действующая)"]
    ACTIVE --> ARCHIVED["ARCHIVED\n(в архиве)"]
    ARCHIVED --> ACTIVE
    DRAFT --> ARCHIVED
Статус Назначение
DRAFT Запись подготовлена, но ещё не введена в действие
ACTIVE Действующее значение справочника
ARCHIVED Значение выведено из обращения, сохранено для истории

Смена статуса выполняется отдельной операцией перехода. Недопустимое значение статуса (не из набора DRAFT / ACTIVE / ARCHIVED) отклоняется с ошибкой валидации (422 dictionary_status_invalid), недопустимый переход между статусами — 409 dictionary_status_transition.

Архивная запись не редактируется напрямую

Запись в статусе ARCHIVED нельзя изменить операцией «создать/обновить» — нужно сначала вернуть её в ACTIVE переходом ЖЦ. Прямая правка архивной записи отклоняется 409 dictionary_entry_archived.

Код записи нормализуется

Код записи (code) обрезается по пробелам в начале и в конце в одном месте сервиса — одинаково при сохранении, смене статуса и удалении. Иначе одна и та же логическая запись то находилась бы по ключу, то нет, в зависимости от лишнего пробела на конце строки в запросе.

Запрет удаления используемой записи

DELETE /v1/admin/dictionaries/{key}/entries/{code} отклоняется 409 dictionary_entry_in_use, если на запись ссылается хотя бы одна акция. Учитываются только ссылки, однозначно видные в схеме:

Справочник Что считается ссылкой
Группы акций (promotion-group) Поле «Группа» акции (promotion.group_name)
Типы событий (event-type) Тип события акции (promotion.event_type)
Регионы (geo-region), каналы (channel) Значение в условии акции «Регион» / «Канал продаж» — в любой из зон, включая исключающую
Любой справочник общего экрана Значение в добавленном условии акции со способом «Из справочника», если условие построено на этом справочнике
Механики (mechanic) Акции, созданные на механике (409 mechanic_in_use)

Ссылки считаются по всем акциям, включая завершённые. Решение считается одним запросом на весь справочник и приходит уже готовым в списке записей (usedByPromotions, deletable) — экран не должен пересчитывать правило самостоятельно. У справочников-адаптеров (валюты, план счетов, единицы) этих полей нет. Удаление несуществующего кода отвечает 404 dictionary_entry_not_found, а не 204.

Справочники со своим экраном

Часть справочников ведётся не общим механизмом записей reference_entry, а собственным API и собственной парой ролей: их записи устроены сложнее, чем «код — название — порядок — статус». Общие роли LOYALTY_DICTIONARY_READ/WRITE эти справочники не открывают.

Справочник (dict_key) API Роли Экран в интерфейсе
Теги клиентов (tag) /v1/tags/** LOYALTY_TAG_READ / WRITE «Справочник тегов» — см. Теги клиента
Метки акций (promotion-label) /v1/promotion-labels/** LOYALTY_PROMOTION_LABEL_READ / WRITE «Справочник меток акций» — см. Метки акций
Сегменты клиентов (customer-segment) /v1/customer-segments/**, /v1/admin/customer-segments/fills LOYALTY_SEGMENT_READ / WRITE «Сегменты клиентов» — см. Справочники (НСИ)
Города (city) /v1/admin/cities LOYALTY_CITY_READ / WRITE Нет — ведение через API
Торговые точки (store) /v1/admin/stores LOYALTY_STORE_READ / WRITE Нет — ведение через API
Мерчанты (merchant) /v1/admin/merchants LOYALTY_MERCHANT_READ / WRITE Нет — ведение через API
Категории товаров (product-category) /v1/admin/product-categories LOYALTY_PRODUCT_CATALOG_READ / WRITE Нет — ведение через API и CSV-импорт
SKU / артикулы (sku) /v1/admin/product-items LOYALTY_PRODUCT_CATALOG_READ / WRITE Нет — ведение через API и CSV-импорт

Любой вызов generic-эндпоинта справочников (GET/POST/DELETE .../entries*, журнал .../audit) на ключе такого справочника отвечает 409 dictionary_own_screen, а запись такого справочника в наборе generic-переноса попадает в ошибки. В общем реестре (GET /v1/admin/dictionaries) строка такого справочника видна только пользователю с read-ролью на сам справочник, а не по общему LOYALTY_DICTIONARY_READ.

Справочник валют программы (/v1/admin/currencies) и пары обмена валют (/v1/admin/exchange-pairs) тоже ведутся отдельным API вне /admin/dictionaries под ролями LOYALTY_DICTIONARY_READ/WRITE (список, карточка, история курса, единая базовая денежная валюта); в общем реестре валюты по-прежнему представлены адаптером currency в режиме только чтения. Подробности — «Справочник валют программы».

География: города и торговые точки

Сущность Поля Статусы
Город code, name (обязательны), description, regionCode — код действующей записи справочника «Регионы» (geo-region), timezone — идентификатор часового пояса IANA (например, Asia/Novosibirsk) ACTIVE, ARCHIVED
Торговая точка code, name (обязательны), cityCode, address ACTIVE, CLOSED
  • Регион у точки не хранится — он выводится через город, поэтому перенос города в другой регион не расходится с точками задним числом.
  • Точку не удаляют, а закрывают (POST /v1/admin/stores/{code}/status): на неё ссылаются условия отработавших акций. Архивация города с действующими точками отклоняется 422 city_has_stores.
  • Прочие отказы: 409 city_code_duplicate / store_code_duplicate, 422 city_region_unknown, 422 city_timezone_invalid, 422 store_city_unknown.
  • Покупка несёт код точки (externalStoreCode в POST /v1/events/purchase, storeCode в POST /v1/integration/pos/receipts); до проверки условий акции он разрешается в код точки, города и региона. Покупка с нераспознанным кодом точки считается метрикой loyalty.purchase.store.unresolved.
  • Часовой пояс города используется для определения пояса клиента по городу; города профиля, для которых пояс не определился, отдаёт GET /v1/admin/timezones/unmatched-cities (LOYALTY_DICTIONARY_READ).

Мерчанты

Мерчант — точка приёма партнёрских платежей. Поля: code — MID из процессинга (уникален, хранится без изменений), name (обязательны), description, partnerCode, mcc, cityCode; статусы ACTIVE и CLOSED — мерчанта закрывают, а не удаляют. Регион выводится через город, как у торговой точки. Отказы: 409 merchant_code_duplicate, 422 merchant_partner_unknown, 422 merchant_city_unknown.

Справочник мерчантов питает условие акции «Мерчант» (значения условия в форме акции) и поиск партнёра по MID для партнёрских транзакций.

Каталог товаров

Сущность Поля Статусы
Категория товаров code, name (обязательны), level — уровень от 1 до 3, parentCode; в ответе — коды предков, itemsCount, источник (FORM / IMPORT) и lastSeenAt ACTIVE, ARCHIVED
Товар (SKU) code, name (обязательны), description, категории трёх уровней (category1Code…category3Code), unitCode, brand, пять настраиваемых полей extra1…extra5 ACTIVE, ARCHIVED
  • Категория не удаляется, пока у неё есть дочерние категории (422 product_category_has_children), товары с этой категорией (422 product_category_has_items) или ссылки из условий акций (409 dictionary_entry_in_use). Товар, на который ссылаются условия акций, тоже не удаляется (409 dictionary_entry_in_use).
  • Прочие отказы: 409 product_category_code_duplicate / product_item_code_duplicate, 422 product_category_level_invalid, 422 product_category_parent_unknown, 422 product_category_cycle.
  • CSV-импорт. POST .../import/validate — проверка файла без записи (итог: всего строк total, к созданию ready, к обновлению toUpdate, с ошибками invalid, список проблем problems); POST .../import?mode= — загрузка в режиме SKIP_INVALID (строки с ошибками пропускаются) или ALL_OR_NOTHING (любая ошибка отменяет загрузку). Строка с известным кодом обновляет запись; пустая ячейка значение не стирает, отсутствие записи в файле её не архивирует, порядок строк не важен.
  • При отборе акции по категории товар ищется по артикулу, и сравнение идёт по его собственным категориям всех трёх уровней: выбор категории верхнего уровня подхватывает вложенные. Если товар не найден или категорий у него нет, используется категория из события покупки. Расхождение категории события с категорией товара считается метрикой loyalty.purchase.category.mismatch и покупку не блокирует.

Сегменты клиентов

Сегменты заводит и называет только приём из Batch Flow (/v1/integration/customer-segments/**, роль LOYALTY_SEGMENT_INTAKE): регистрация сегмента открывает заливку, пачки участников копятся отдельно, завершение заменяет состав одной транзакцией — до завершения действует прежний состав. Создать или переименовать сегмент в продукте нельзя (editable=false); на экране «Сегменты клиентов» его можно архивировать, вернуть из архива и удалить.

  • Архивный сегмент не предлагается в полях выбора, но продолжает работать там, где уже выбран.
  • Удаление отклоняется 409 segment_in_use, пока сегмент стоит в условиях запланированной, активной или приостановленной акции. История входов и выходов и журнал заливок после удаления остаются.
  • Список сегментов (GET /v1/customer-segments) читается любой из ролей LOYALTY_SEGMENT_READ, LOYALTY_PROMOTION_READ, LOYALTY_DICTIONARY_READ, LOYALTY_CUSTOMER_READ — он нужен полям выбора сегмента в акциях, партнёрских предложениях, ограничениях списания и каталоге клиентов.

Справочники в форме акции

Форма акции читает значения условий не через админский фасад, а через свой API под правом LOYALTY_PROMOTION_READ — маркетологу не нужны роли справочников:

Метод и путь Назначение
GET /v1/promotions/dictionaries/{key}/entries?search=&page=&size= Опубликованные записи справочника с поиском, страницами до 100 записей
POST /v1/promotions/dictionaries/{key}/entries/lookup Проверка списка кодов: найденные записи (в любом статусе) и ненайденные коды

Доступен любой справочник реестра — и общего экрана, и со своим экраном (города, точки, мерчанты, категории, SKU, сегменты, теги, метки), — каждый из своего источника; ключ вне реестра — 404 dictionary_not_found. У записи есть поле context — готовая строка, собранная сервером через « — »: путь от корня и уровень у категории, путь категорий и бренд у товара, город у точки, партнёр и город у мерчанта. По ней различают одноимённые записи. Поиск идёт по коду и названию, а у категорий, товаров и торговых точек — и по названиям из контекста (предки, категории и бренд, город). У категорий товаров дополнительно приходят level и ancestorCodes (коды предков от корня).

Перенос справочников между контурами

Записи локальных операционных справочников (reference_entry) переносятся между контурами (например, со стенда на промышленный контур) через API — двумя шагами: выгрузка в исходном контуре и загрузка (с предпросмотром) в целевом.

Метод и путь Назначение Роль
GET /v1/admin/dictionaries/export?dictKeys= Выгрузить записи локальных справочников (все или по ключам) в JSON LOYALTY_DICTIONARY_READ
POST /v1/admin/dictionaries/import?dryRun= Загрузить набор; dryRun=true — предпросмотр без записи LOYALTY_DICTIONARY_WRITE

Справочники с собственными таблицами (валюты, единицы измерения, системные счета, механики) и внешние (REMOTE) справочники в этот перенос не входят — у механик есть свой перенос (см. GET/POST /v1/admin/mechanics/export, /import), остальные ведутся в своих разделах или вовне. Справочники со своим экраном в generic-перенос тоже не входят. Теги и метки акций переносятся собственным бандлом (.../export, .../import на своём базовом пути) — одним механизмом для обоих справочников: вид бандла проверяется, и бандл тегов не загрузится в метки (и наоборот) — tag_bundle_invalid / label_bundle_invalid. Недопустимое значение в бандле отклоняет импорт целиком.

Запись опознаётся по натуральному ключу (dictKey + code): отсутствующая создаётся, существующая пропускается (не перезаписывается), ошибка одной записи не отменяет перенос остальных. Проверки записи те же, что при заведении в админке: справочник должен быть в реестре, вестись на общем экране и быть редактируемым, код и название обязательны. В отличие от переноса акций, статус записи переносится как есть, а не сбрасывается в черновик — акция тратит бюджет и деньги, поэтому приезжает выключенной, а запись справочника — данные. Предпросмотр (dryRun=true) — тот же запрос импорта, ничего не записывает, отдельной ручки предпросмотра намеренно нет, чтобы предпросмотр и применение не расходились; права на предпросмотр — те же, что на запись. Ответ: {created[], skipped[], errors[{entry, message}], dryRun}.

Аудит изменений

Все изменения локальных справочников фиксируются в журнале аудита (dictionary_audit): для каждой записи сохраняются код записи, действие, исполнитель (оператор), детализация и момент времени. Исполнитель определяется из токена доступа (имя пользователя из claim preferred_username, при отсутствии — субъект токена). Журнал доступен по справочнику и отсортирован от свежих изменений к старым.

Единицы измерения

Единицы измерения вынесены в данные (а не в код) и доступны через адаптер unit в режиме только чтения. Каждая единица описывается кодом, наименованием, символом, разрядностью количества (decimals) и измерением (dimension) для группировки. Поставляемый набор:

Код Наименование Символ Измерение
LITER Литр л VOLUME
M3 Кубометр м³ VOLUME
KG Килограмм кг MASS
GRAM Грамм г MASS
PIECE Штука шт COUNT
KWH Киловатт-час кВт·ч ENERGY
KM Километр км DISTANCE
NIGHT Ночь ноч COUNT

Единицы измерения используются механикой начисления за натуральный объём (UNIT_BONUS): баллы начисляются как «количество × ставка» (например, за литры или штуки).

Перечень справочников

Реестр справочников доступен через API и фронтенд. Локальные операционные словари (reference_entry) поставляются с предзаполненными значениями:

Справочник (dict_key) Источник Содержание
promotion-group LOCAL Группы/папки акций (Новые, Сезонные, Реферальные, Партнёрские, Welcome)
event-type LOCAL Типы событий (Транзакция, Пополнение, Посещение, Регистрация в ПЛ, Заполнение анкеты)
channel LOCAL Каналы (Сайт, Мобильное приложение, Касса, Колл-центр) — значения условия акции «Канал продаж»
operation-reason LOCAL Причины ручных операций (ручное начисление/списание, компенсация, корректировка)
geo-region LOCAL Географические регионы (Центральный, Северо-Западный, Южный и др.) — регион города и условие акции «Регион»
point-type LOCAL Типы баллов (Стандартные, Приветственные, Акционные, Партнёрские)
mechanic LOCAL (адаптер) Каталог механик начисления — редактируемый, без статусов ЖЦ
currency LOCAL (адаптер) Валюты/типы баллов — только чтение, ведутся в справочнике валют
system-account LOCAL (адаптер) Системные счета (план счетов) — только чтение
unit LOCAL (адаптер) Единицы измерения — только чтение
product-category LOCAL (свой экран) Категории товаров — роль LOYALTY_PRODUCT_CATALOG_READ
sku LOCAL (свой экран) SKU / артикулы — роль LOYALTY_PRODUCT_CATALOG_READ
customer-segment LOCAL (свой экран) Сегменты клиентов из системы кампаний — роль LOYALTY_SEGMENT_READ, только архивация и удаление
tag LOCAL (свой экран) Теги клиента — роль LOYALTY_TAG_READ; см. Теги клиента
promotion-label LOCAL (свой экран) Метки акций — роль LOYALTY_PROMOTION_LABEL_READ; см. Метки акций
city LOCAL (свой экран) Города программы — роль LOYALTY_CITY_READ
store LOCAL (свой экран) Торговые точки (магазины) программы — роль LOYALTY_STORE_READ
merchant LOCAL (свой экран) Мерчанты — точки приёма партнёрских платежей (MID) — роль LOYALTY_MERCHANT_READ

У механики, помимо кода, названия, типа и диаграммы RT Flow, есть признак «начисляет баллы» (accruesPoints): его задают только через API; не передан при создании — true, при правке значение не меняется.

API справочников

Все эндпоинты находятся под префиксом /v1/admin/dictionaries. Чтение требует роли LOYALTY_DICTIONARY_READ, изменение — LOYALTY_DICTIONARY_WRITE.

Метод и путь Назначение Роль
GET /v1/admin/dictionaries Реестр справочников (источник, редактируемость, доступность, вид экрана) LOYALTY_DICTIONARY_READ
GET /v1/admin/dictionaries/{key}/entries Записи справочника; у записей — usedByPromotions и deletable LOYALTY_DICTIONARY_READ
GET /v1/admin/dictionaries/{key}/audit Журнал изменений справочника LOYALTY_DICTIONARY_READ
POST /v1/admin/dictionaries/{key}/entries Создать/обновить запись (только LOCAL) LOYALTY_DICTIONARY_WRITE
POST /v1/admin/dictionaries/{key}/entries/{code}/status Сменить статус записи (ЖЦ) LOYALTY_DICTIONARY_WRITE
DELETE /v1/admin/dictionaries/{key}/entries/{code} Удалить запись (только LOCAL) LOYALTY_DICTIONARY_WRITE
GET /v1/admin/dictionaries/export?dictKeys= Выгрузить локальные справочники для переноса между контурами LOYALTY_DICTIONARY_READ
POST /v1/admin/dictionaries/import?dryRun= Загрузить набор; dryRun=true — предпросмотр без записи LOYALTY_DICTIONARY_WRITE

Пример: создание/обновление записи LOCAL-справочника

curl -X POST https://<host>/v1/admin/dictionaries/promotion-group/entries \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
        "code": "FLASH",
        "label": "Флеш-акции",
        "description": "Краткосрочные акции выходного дня",
        "sortOrder": 60
      }'
{
  "code": "FLASH",
  "label": "Флеш-акции",
  "description": "Краткосрочные акции выходного дня",
  "sortOrder": 60,
  "status": "DRAFT"
}

Новая запись создаётся в статусе DRAFT; правка существующей записи статус не меняет. Признаки usedByPromotions и deletable ответ на одиночную правку не считает — они приходят в списке записей.

Пример: смена статуса записи

curl -X POST https://<host>/v1/admin/dictionaries/promotion-group/entries/FLASH/status \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "status": "ARCHIVED" }'

Кэширование справочников

План счетов, валюты, субсчета и переопределения операционной конфигурации кэшируются в памяти каждой реплики на срок loyalty.reference-cache.ttl (по умолчанию 30s, см. «Конфигурация» → «Кеш справочников») — правка на своей реплике действует сразу, соседние увидят её не позже чем через TTL. Тот же срок у разрешения кода торговой точки в город и регион и у классификации товара по каталогу при проверке условий акции; эти кэши по записи не сбрасываются, поэтому правка точки, города или товара доходит до проверки условий не позже чем через TTL.

В дорожной карте

Трансляция изменений справочников во внешние потребители (outbox → Kafka → SDE), а также расширенное редактирование адаптерных справочников (например, единиц измерения с обобщёнными атрибутами) запланированы в дорожной карте и в текущей версии не реализованы. Экранов ведения городов, торговых точек, мерчантов и каталога товаров в интерфейсе сейчас нет — эти справочники ведутся через API (см. выше).