Справочники и НСИ¶
Модуль 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 (см. выше).