POST /internal/handbook/resolve — Batch-резолв справочных записей по id
Назначение
Единственная internal-операция контекста handbook: батч-резолв справочных записей всех пяти типов по их
id для внутренних сервисов PIN. Возвращает snapshot-модели (snapshots.md) с
признаком IsArchived — включая архивные записи (INV-HND-5): исторические ссылки не ломаются, а
решение «допустима ли архивная/отсутствующая запись» принимает вызывающий. Основное применение —
write-path-валидация справочных id (crm, profile, admin-write денежных контрактов) и fallback-резолв
при отставании локальной реплики (chessboard).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | handbook |
| API area | internal (m2m) |
| Feature group | resolve |
| Статус | draft |
| Документ | docs/06-services/handbook/api/internal/resolve-references.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | ServiceAccount (m2m service token, client-credentials) — не user JWT |
| Auth policy / Permissions | policy InternalServiceOnly; allowlist потребителей internal-области: chessboard, crm, profile, rewards, promotion, investor-owner; user-permissions не применяются |
| Scope (RBAC) | internal (m2m) |
| Tenant isolation | не применяется — справочники платформенные |
| Audit | не пишется (не-PII данные); вызовы видны в метриках по потребителям |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST (тело — до 500 id; операция семантически read-only, безопасна для повтора) |
| Route | /internal/handbook/resolve |
| Success status | 200 — всегда при валидном входе, независимо от того, сколько id найдено |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса ResolveReferencesRequest
Все списки опциональны; суммарное число id по всем спискам (после удаления дубликатов внутри каждого списка) — от 1 до 500. Дубликаты внутри списка допустимы и схлопываются без ошибки.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения |
|---|---|---|---|---|
CountryIds | body | short[]? | Нет | id стран |
RegionIds | body | long[]? | Нет | id регионов |
CityIds | body | long[]? | Нет | id городов |
CurrencyIds | body | short[]? | Нет | id валют |
LanguageIds | body | short[]? | Нет | id языков |
Суммарно 0 id (все списки отсутствуют/пусты) → VALIDATION_FAILED (защита от бессмысленных вызовов при
багах маппинга у потребителя); суммарно > 500 → VALIDATION_FAILED (вызывающий разбивает на батчи).
Модель ответа ResolveReferencesResponse
Пять коллекций — всегда присутствуют (пустые для незапрошенных типов). Элемент = snapshot-модель
(snapshots.md) + IsArchived; для гео-типов дополнительно — ссылки на
родителей (нужны гео-валидации потребителей: согласованность город ↔ регион ↔ страна). Неизвестный id
просто отсутствует в соответствующей коллекции — ошибкой это не является.
| Поле | Тип | Обязательность | Описание |
|---|---|---|---|
Countries | ResolvedCountry[] | Да (может быть пустым) | Найденные страны из CountryIds. |
Regions | ResolvedRegion[] | Да | Найденные регионы из RegionIds. |
Cities | ResolvedCity[] | Да | Найденные города из CityIds. |
Currencies | ResolvedCurrency[] | Да | Найденные валюты из CurrencyIds. |
Languages | ResolvedLanguage[] | Да | Найденные языки из LanguageIds. |
ResolvedCountry
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
CountryId | short | Да | CountrySnapshot | Идентификатор страны. |
IsoCode2 | string | Да | CountrySnapshot | ISO 3166-1 alpha-2 — юридический след (документы profile). |
Name | string | Да | CountrySnapshot (Name = FullName) | Display-имя. |
IsArchived | bool | Да | CountryEntity.IsArchived | Запись выведена из оборота (в lookup не выдаётся). |
ResolvedRegion
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
RegionId | long | Да | RegionSnapshot | Идентификатор региона. |
Name | string | Да | RegionSnapshot | Название региона. |
CountryId | short | Да | RegionEntity.CountryId | Страна региона (сверка гео-цепочки потребителем). |
IsArchived | bool | Да | RegionEntity.IsArchived | Признак архива. |
ResolvedCity
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
CityId | long | Да | CitySnapshot | Идентификатор города. |
Name | string | Да | CitySnapshot | Название города. |
CountryId | short | Да | CityEntity.CountryId | Страна города. |
RegionId | long? | Нет | CityEntity.RegionId | Регион города (null — город вне региона). |
IsArchived | bool | Да | CityEntity.IsArchived | Признак архива. |
ResolvedCurrency
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
CurrencyId | short | Да | CurrencySnapshot | Идентификатор валюты. |
IsoCode | string | Да | CurrencySnapshot | Код ISO-4217 (RUB). |
Name | string | Да | CurrencySnapshot | Название валюты. |
Sign | string? | Нет | CurrencySnapshot | Знак валюты. |
IsArchived | bool | Да | CurrencyEntity.IsArchived | Признак архива. |
ResolvedLanguage
Платформенной snapshot-модели языка нет (потребителей у языков в PIN нет — language.md); состав resolve-элемента определяется здесь.
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
LanguageId | short | Да | LanguageEntity.Id | Идентификатор языка. |
Name | string | Да | .Name | Название языка. |
IsoCode2 | string | Да | .IsoCode2 | ISO 639-1. |
IsoCode3 | string | Да | .IsoCode3 | ISO 639-2/3. |
IsArchived | bool | Да | .IsArchived | Признак архива. |
Потребители и их сценарии
| Потребитель | Сценарий вызова | Реакция на отсутствие/архив |
|---|---|---|
chessboard | fallback-резолв при отставании локальной реплики (write-path проектов/цен) | отсутствие → CHESSBOARD_GEO_REF_UNKNOWN; архив → CHESSBOARD_GEO_REF_ARCHIVED; рассогласование город↔регион → CHESSBOARD_CITY_REGION_MISMATCH (коды — контракты chessboard) |
crm | валидация CityId пожеланий лида (update-lead-wishes) | отсутствие → CRM_CITY_UNKNOWN |
profile | валидация CountryId документов персон; IsoCode2 — для инвариантов документов и юридического следа | отсутствие → PROFILE_COUNTRY_UNKNOWN |
rewards, promotion, investor-owner | валидация CurrencyId на admin-write денежных контрактов | отсутствие/архив → отказ операции по правилам вызывающего |
Резолв исторических ссылок (чтение старых документов/событий) допускает IsArchived = true без отказа;
на write-path потребители, как правило, отклоняют и отсутствующие, и архивные записи — это правило
вызывающего, handbook его не навязывает.
Алгоритм
- Контекст и доступ. Вызывает m2m service-account из allowlist internal-области; policy
InternalServiceOnly. Tenant и user-permissions не применяются. - Проверка входа. Суммарное число id по всем спискам после дедупликации —
1..500; иначеVALIDATION_FAILED(400). - Выборка. Записи каждого типа по спискам id — включая архивные (INV-HND-5). Несуществующие id не являются ошибкой и никак не отмечаются — соответствующего элемента просто нет в ответе (вызывающий сравнивает запрошенное с полученным).
- Результат.
200+ResolveReferencesResponse: пять коллекций (пустые — для типов, по которым id не запрашивались или ничего не найдено). Порядок элементов не гарантируется — сопоставление по id.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | нет/невалиден m2m service token | без тела | обновить токен |
| — (authz) | 403 | токен вне allowlist internal-области | без тела | no retry |
VALIDATION_FAILED | 400 | суммарно 0 id либо > 500 id | «От 1 до 500 идентификаторов суммарно» | no retry (разбить на батчи) |
200 с найденным подмножеством. HANDBOOK_ENTRY_NOT_FOUND в batch-resolve не используется (семантика
отсутствия — пропуск элемента, INV-HND-5).
Acceptance Criteria
- Смешанный батч. Given существующие
CountryIds=[1],CityIds=[10, 999999](второй город не существует); When resolve; Then200:Countriesсодержит страну 1,Cities— только город 10; элемента для 999999 нет;Regions/Currencies/Languages— пустые массивы. - Архивная запись резолвится. Given город заархивирован администратором; When resolve по его id; Then
200, город присутствует вCitiesсIsArchived = trueи актуальнымиName/CountryId/RegionId. - Гео-цепочка. Given город с регионом; When resolve; Then элемент города несёт
CountryIdиRegionId— потребитель может сверить согласованность без дополнительных вызовов. - Пустой запрос. Given все списки отсутствуют или пусты; When resolve; Then
400 VALIDATION_FAILED. - Превышение лимита. Given суммарно 501 id; When resolve; Then
400 VALIDATION_FAILED. - Дубликаты. Given
CurrencyIds=[1,1,1]; When resolve; Then200, валюта 1 присутствует вCurrenciesровно один раз (дубликаты схлопнуты, не ошибка). - Чужой сервис. Given m2m-токен сервиса вне allowlist; When resolve; Then
403. - Повтор безопасен. Given потребитель ретраит вызов; When resolve дважды; Then оба ответа идентичны (read-only, состояний не меняет).
Совместимость и наблюдаемость
Расширения resolve-моделей — только additive (новые optional-поля), синхронно с правилами snapshots.md; удаление/переименование/смена типа — breaking сразу для всех потребителей allowlist (согласованный rollout). Неизвестные поля ответа потребитель игнорирует. Семантика «неизвестный id отсутствует в ответе» — контракт: добавление признаков «почему не найден» — additive-расширение, замена пропуска на ошибку — breaking.
- SLO: p99 < 100 мс при батче 500 id (операция стоит на write-path потребителей); PostgreSQL,
point-чтения по PK (ADR-0063 п.7). При недоступности
handbookпотребитель решает сам: блокировать операцию (строгая валидацияcrm/profile) или деградировать (мягкая валидация, локальная репликаchessboard). - Logs:
handbook-resolve requested(потребитель из токена, счётчики запрошено/найдено по типам — без перечисления id); metrics:handbook_resolve_total{consumer},handbook_resolve_ids_total{kind,outcome=found|missing}, latency p99; traces — OTel (traceparent); dashboardhandbook-overview; alert — всплескoutcome=missing(рассинхрон данных потребителя или отставание синка).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (9): Реплика справочников handbook в chessboard (chessboard.handbook_*), Entity / Aggregate: AgencyCommissionPolicyEntity, Entity / Aggregate: DealEntity, Entity / Aggregate: PersonEntity, Entity / Aggregate: PromotionPackage, Entity: CityEntity, Entity: CountryEntity, Entity: PersonDocumentEntity, Snapshot-модели справочника (единый дом платформы)
- API (15): GET /api/v1/handbook/{countries,regions,cities,currencies,languages} — Публичный lookup справочников, Handbook API — реестр операций областей public / internal / admin, POST /api/admin/v1/promotion-packages — Создание пакета продвижения (admin), POST /api/agent/crm/fixations — Отправить заявку на фиксацию, POST /api/agent/crm/leads — Создать лид вручную, POST /api/developer/crm/booking-programs — Создать программу бронирования, POST /api/v1/admin/portfolio/properties — Создание записи портфеля вручную (admin, компенсирующее), POST /api/v1/agency/commission-policies — Создание комиссионной политики агентства (черновик), POST /api/v1/profile/persons — Создание персоны (createPerson), POST /api/v1/profile/persons/{personId}/documents — Добавление документа персоны (addPersonDocument), PUT /api/admin/v1/promotion-packages/{packageId} — Изменение/публикация/архивация пакета (admin), PUT /api/agent/crm/leads/{leadId}/wishes — Обновить хотелки лида (+3)
- Use Cases (2): UC-HND-001. Администратор поддерживает справочник (archive/restore), US-реестр контекста handbook (US-HND)
- События (1): Event Contract: currency-upserted (CurrencyUpsertedEvent)
- Консюмеры (1): Consumer: синхронизация из Talent Owners (pin-investor-owner-talent-owners-sync)
- index.md (2): Handbook Service, Profile Service (Pin.Profile)
- registries (1): Реестр API-методов PIN
Связанные документы
- Snapshot-модели (дом полей): snapshots.md; реплики: country.md, region.md, city.md, currency.md, language.md; INV-HND-5 — country.md.
- Публичный lookup (только неархивные): lookup-references.md.
- Стратегии потребления по сервисам: ADR-0063 п.6.
- Реестр операций и правила областей: ../README.md.