Handbook Service
Назначение
handbook владеет репликами нормативных справочников платформы — страны, регионы, города, валюты,
языки — и предоставляет их всем сервисам PIN: публичный lookup для фронтов, internal resolve для
write-path-валидации, собственные Kafka-топики pin.handbook.*.v1 для локальных реплик потребителей.
Сервис — anti-corruption layer над внешним talent.microservices.handbook
(ADR-0063): PIN не ведёт справочные
данные сам, а реплицирует их из источника; downstream-сервисы PIN зависят только от pin.handbook и
никогда не подписываются на топики Talent напрямую.
Metadata
| Поле | Значение |
|---|---|
| Статус | draft |
| Owning team | команда handbook (платформенная) |
| Domain | платформенные справочники (ACL-реплика Talent Handbook) |
| Data classification | Public — справочные данные без PII |
| Runtime criticality | medium (недоступность не ломает чтение витрин — потребители живут на репликах/снапшотах; блокирует write-path-валидацию crm/profile и админ-операции справочника) |
Ownership
| Artifact | Owner/Rule |
|---|---|
| PostgreSQL schema | handbook — единственное хранилище (PostgreSQL-only, без Redis/ES — ADR-0056, ADR-0063 п.7) |
| Tenant isolation | не tenanted — платформенные данные, общие для всех компаний и пользователей |
| API areas | public (lookup, гость), internal (batch-resolve, m2m), admin (archive/restore, sync-state) |
| Owned entities | CountryEntity, RegionEntity, CityEntity, CurrencyEntity, LanguageEntity, HandbookSyncState; snapshot-модели CountrySnapshot/RegionSnapshot/CitySnapshot/CurrencySnapshot (единый дом платформы) |
| Produced events | country-upserted, region-upserted, city-upserted, currency-upserted, language-upserted (pin.handbook.{countries|regions|cities|currencies|languages}.v1) |
| Consumed events | 4 топика Talent handbooks.after-{country|city|currency|language}-information-update (ContractVersion 2) |
| Search indexes | none — lookup и префиксный поиск городов на PostgreSQL |
| Analytics facts | none |
Ответственность И Границы
Делает:
- консюмит upsert-события Talent Handbook, ведёт реплику пяти справочников в схеме
handbook(идемпотентность, version-guard, snapshot-семантика, порядок страна → регион → город — правила domain/talent-replica.md); - derive-ит регион как первоклассную запись PIN из вложенной модели city-событий (у источника собственного region-события нет — ADR-0063 п.4);
- re-publish-ит каждое изменение реплики собственными событиями в compacted-топики
pin.handbook.*.v1(key = id записи, ContractVersion 1, payload = полный snapshot реплики); - отдаёт публичный lookup (только неархивные записи, INV-HND-5) и internal batch-resolve (включая архивные — исторические ссылки не ломаются);
- предоставляет администратору платформы archive/restore записей (soft-архив
IsArchived— компенсация delete-gap источника) и просмотр состояния синка; - выполняет bootstrap/полную пересинхронизацию через Talent
POST sync-handbooks+ reconciliation-отчёт (background/talent-initial-import.md).
Не делает:
| Не делает | Почему / где дом |
|---|---|
| Не владеет справочными данными | Source of truth — Talent Handbook; PIN хранит реплику. Недостающие страны/города/валюты заводятся в источнике, не в PIN |
| Не ведёт CRUD справочных записей | Создание и изменение реплика-полей — только синк источника (INV-HND-1); администратору доступны только archive/restore |
| Не удаляет записи физически | Delete-gap источника (жёсткие удаления без событий) закрыт soft-архивом IsArchived + reconciliation; исторические ссылки резолвимы всегда |
| Не ведёт UI-локали | Locale — строка BCP-47 (ru-RU); дом закрытого списка поддерживаемых локалей — контекст profile; LanguageId для локали интерфейса не вводится (ADR-0063 п.3) |
| Не ведёт таймзоны | TimeZone — строка IANA у потребителей; у источника нет timezone-сущности (ADR-0063 п.3) |
Не реплицирует ContactInfoType | Вне scope v1 — в PIN нет ни одного потребителя; топик handbooks.after-contact-information-update не консюмится; добавление — additive без пересмотра ADR |
| Не вводит мультивалютность | Справочник валют ≠ конверсии/мультивалютные балансы: ADR-0048 (RUB-only) не пересматривается (domain/currency.md) |
| Не строит поисковую инфраструктуру | Lookup/resolve — PostgreSQL-запросы; ES-индексы на справочники не заводятся (ADR-0056, ADR-0063 п.7) |
Scope v1: Country, Region, City, Currency, Language — пять справочников, ничего сверх.
Зависимости
| Зависимость | Тип | Зачем Нужна | Failure mode |
|---|---|---|---|
PostgreSQL (schema handbook) | db | Реплики пяти справочников + handbook.talent_sync_states | Сервис недоступен |
Kafka handbooks.after-* (Talent) | queue (consume) | Наполнение реплики (consumer group pin-handbook-talent-sync) | Реплика устаревает (stale-метрика); lookup/resolve работают по последнему состоянию |
Kafka producer pin.handbook.*.v1 | queue (produce) | Донесение изменений до потребителей (chessboard) | Потребители отстают; события публикуются после восстановления (at-least-once) |
Talent Handbook POST sync-handbooks | external system | Bootstrap и полная пересинхронизация (re-эмит всех строк источника) | Начальный импорт/reconciliation откладываются; текущий поток синка не затронут |
Проекты
src/services/handbook/
Pin.Handbook.Api/
Pin.Handbook.Application/
Pin.Handbook.Domain/
Pin.Handbook.Infrastructure/
Pin.Handbook.Background/
Pin.Handbook.Migrator/
Pin.Handbook.Contracts/
Namespace по агрегатам — Pin.Handbook.Countries, Pin.Handbook.Regions, Pin.Handbook.Cities,
Pin.Handbook.Currencies, Pin.Handbook.Languages.
Основные Сущности
| Entity | Назначение | Source Of Truth | Документ |
|---|---|---|---|
CountryEntity (handbook.countries, Id short) | Страна: ISO-коды, дефолтные валюта/язык; дом INV-HND-1/2/4/5 | Talent Handbook (архив — handbook) | domain/country.md |
RegionEntity (handbook.regions, Id long) | Регион — первоклассная запись PIN, derive из city-событий; монотонность по LastEventAt | Talent Handbook (через city-события) | domain/region.md |
CityEntity (handbook.cities, Id long) | Город — самый объёмный справочник; ключ гео-фильтров каталога и адресов проектов | Talent Handbook | domain/city.md |
CurrencyEntity (handbook.currencies, Id short) | Валюта: IsoCode ISO-4217 — единственное легитимное char(3) спека | Talent Handbook | domain/currency.md |
LanguageEntity (handbook.languages, Id short) | Язык; текущих потребителей в PIN нет (UI-локали — не отсюда) | Talent Handbook | domain/language.md |
HandbookSyncState (handbook.talent_sync_states) | Чекпоинты синка — одна строка на запись источника (AggregateType + ExternalId) | handbook | domain/talent-replica.md |
CountrySnapshot / RegionSnapshot / CitySnapshot / CurrencySnapshot | Единый дом snapshot-моделей всей платформы (деньги = CurrencyId + CurrencySnapshot; гео = id + снапшоты имён) | handbook (модели) | domain/snapshots.md |
ER-обзор
Инварианты справочника: INV-HND-1 SyncOwnsData, INV-HND-2 UniqueIsoAmongActive, INV-HND-4 MonotonicVersion, INV-HND-5 LookupVisibility — объявлены в domain/country.md и действуют для всех записей; INV-HND-3 GeoConsistency объявлен в domain/city.md (аспект региона — domain/region.md).
API
Карта областей; каждая операция документируется отдельным файлом (реестр — api/README.md).
- Public (гость, без авторизации; только неархивные записи — INV-HND-5):
api/public/lookup-references.md — пять GET-выдач
/api/v1/handbook/{countries|regions|cities|currencies|languages}; города — единственная постраничная выдача (тысячи строк: фильтры по стране/региону, префиксный поиск по имени); остальные — короткие полные списки. - Internal (m2m): api/internal/resolve-references.md —
POST /internal/handbook/resolve, batch-резолв id по всем пяти типам за один вызов, включая архивные записи (INV-HND-5); неизвестный id просто отсутствует в ответе (не ошибка). Потребители: write-path-валидацияcrm/profile, мягкая валидацияuser-activity, resolveCurrencyIdна admin-write денежных контрактов. - Admin (администратор платформы, permission
handbook.entries.manage):- api/admin/archive-restore-entry.md —
POST /api/v1/admin/handbook/{type}/{id}/archiveи.../restore: ручной soft-архив/возврат записи + re-publish события; - api/admin/get-sync-state.md —
GET /api/v1/admin/handbook/sync-state: чекпоинтыHandbookSyncState, лаг, DLQ-счётчики.
- api/admin/archive-restore-entry.md —
- Callbacks: not applicable.
Events И Consumers
Входящие топики Talent (consumer group pin-handbook-talent-sync)
| Топик источника | ContractVersion | Потребляется | Эффект |
|---|---|---|---|
handbooks.after-country-information-update | 2 | да | upsert реплики страны → re-publish country-upserted |
handbooks.after-city-information-update | 2 | да | upsert реплики города + derive/upsert региона из вложенной RegionEventModel → re-publish city-upserted (+ region-upserted при фактическом изменении региона) |
handbooks.after-currency-information-update | 2 | да | upsert реплики валюты → re-publish currency-upserted |
handbooks.after-language-information-update | 2 | да | upsert реплики языка → re-publish language-upserted |
handbooks.after-contact-information-update | 2 | нет | ContactInfoType вне scope v1 (нет потребителей в PIN); подключение — additive |
Consumer: consumers/talent-handbook-sync.md. Retry/DLQ:
HANDBOOK_SYNC_COUNTRY_NOT_FOUND — retriable (гео-событие пришло раньше своей страны — retry с backoff);
HANDBOOK_SYNC_GEO_MISMATCH, HANDBOOK_SYNC_ISO_CONFLICT — non-retriable → DLQ
pin.handbook.talent-sync-dlq.v1 + алерт (дефект данных источника, разрешение — правка в Talent + replay).
Идемпотентность и монотонность — version-guard INV-HND-4 (stale-событие → skip + метрика).
Исходящие топики pin.handbook.*.v1
Все пять — compacted, key = id записи, ContractVersion 1, payload = полный snapshot реплики (включая
IsArchived и ReplicaVersion / LastEventAt у региона). Публикация: применённый upsert синка, admin
archive/restore, пересинхронизация. Compacted-хранение позволяет новому потребителю подключиться в любой
момент чтением с offset 0.
| Событие | Топик | Назначение | Контракт |
|---|---|---|---|
country-upserted | pin.handbook.countries.v1 | Реплика стран у потребителей | events/country-upserted.md |
region-upserted | pin.handbook.regions.v1 | Реплика регионов (фильтр-ключ каталога) | events/region-upserted.md |
city-upserted | pin.handbook.cities.v1 | Реплика городов (гео-фильтры, адреса) | events/city-upserted.md |
currency-upserted | pin.handbook.currencies.v1 | Реплика валют (валидация CurrencyId, display) | events/currency-upserted.md |
language-upserted | pin.handbook.languages.v1 | Будущие потребители (сейчас подписчиков нет) | events/language-upserted.md |
Текущий подписчик — chessboard (4 топика, без languages): локальная реплика
chessboard/domain/handbook-replica.md, consumer chessboard/consumers/handbook-sync.md.
Стратегия потребления по сервисам PIN
Канонический паттерн ссылки на справочник в контрактах: <X>Id (тип источника: Country/Currency/
Language — short, Region/City — long) + денормализованное display-имя <X>Name, снятое при записи;
полный snapshot — в payload событий (domain/snapshots.md).
Четыре стратегии потребления справочника (по возрастанию связанности):
- A. Полная реплика через consumer — сервис подписан на
pin.handbook.*.v1, ведёт собственные таблицы-реплики, читает их локально. Единственный носитель —chessboard(hot-path карточек/фильтров + refresh денормализованных имён при переименованиях). - B. Id + денормализованное имя из snapshot-ов событий — сервис не подписан на handbook, а снимает
<X>Id+<X>Nameиз payload доменных событий вышестоящего сервиса (типично — событийchessboard). Большинство read-ориентированных потребителей. - C. Internal resolve на write-path — сервис хранит id + имя, но при записи валидирует id синхронным
вызовом
POST /internal/handbook/resolve. Толькоcrm/profile(и точечно admin-write денежных контрактов). Вызов — вне hot read path (см. инвариант ниже). - D. Не зависит —
Locale/TimeZone— строки,CurrencyIdне используется.
| Сервис | Стратегия | Что использует |
|---|---|---|
chessboard | A — полная реплика через consumer (подписка на pin.handbook.*) | CountryId/RegionId/CityId/CurrencyId: hot-path карточек и фильтров, валидация geo/валют на write-path, refresh денормализованных имён при переименованиях (с re-publish затронутых проектов) |
catalog | B — id + name из snapshot-ов событий chessboard (без реплики и подписки) | CountryId/RegionId/CityId/CurrencyId + имена из событий |
crm, profile | C — internal resolve на write-path (хранят id + денормализованное имя) | валидация CityId/CountryId/CurrencyId синхронным resolve при записи |
user-activity | B/мягкая — CityRefModel{CityId long, CityName} с клиента | CityId; невалидный id не блокирует запись факта |
investor-owner, rewards, promotion, messenger | B — только CurrencyId + CurrencySnapshot из событий; resolve — точечно на admin-write | CurrencyId |
notifications, dev-stats | D — не зависят от handbook | Locale/TimeZone — строки (ADR-0063 п.3) |
identity, news, favorites, selections, support, referral, invest-analytics | D — не затрагиваются | — |
Инвариант hot read path (ADR-0063 п.6): ни один горячий путь чтения не делает синхронных вызовов
handbook. Выдачи витрин, карточки, фильтры и ленты читаются из локальных реплик (chessboard) либо
из денормализованных <X>Id + <X>Name, снятых в снапшоты при записи. Синхронный internal resolve
допустим только на write-path (crm/profile, admin-write денежных контрактов) — редком относительно
чтений и не блокирующем отдачу витрин; недоступность handbook останавливает лишь эти write-валидации
и админ-операции справочника, но не чтение (см. Runtime criticality в Metadata).
Runtime И Эксплуатация
| Область | Требование |
|---|---|
| Health checks | /health/live, /health/ready (PostgreSQL, Kafka consumer lag), /health/startup (Migrator применён) |
| SLO/SLA | Public lookup p95 < 300 мс; internal resolve p95 < 100 мс; лаг синка от источника — минуты (справочники меняются редко) |
| Dashboard | handbook-sync-overview (лаг по потокам, stale-skip, DLQ, авто-restore warn); handbook-overview (lookup/resolve/admin-операции) |
| Alerts | consumer lag > 15 мин; DLQ непуст (GEO_MISMATCH/ISO_CONFLICT); всплеск warn авто-restore |
| Runbooks | background/talent-initial-import.md — bootstrap, полная пересинхронизация, reconciliation-отчёт |
| Feature flags | none |
Безопасность
- Actor types: гость (public lookup, без авторизации), внутренние сервисы (internal resolve, m2m), администратор платформы (admin API), system (consumer синка).
- Permission model:
handbook.entries.manage(archive/restore, просмотр sync-state) — каталог permission.md. - Sensitive data classes: нет — справочные данные публичны, PII отсутствует.
- Audit: admin archive/restore логируется с актором; каждый апсерт несёт
ReplicaVersion/LastEventAtи штампы. - Tenant isolation: не tenanted — платформенные данные (см. Ownership).
Известные Ограничения И Assumptions
- Delete-gap источника: Talent Handbook удаляет записи жёстко и без событий — удалённая запись живёт
в PIN до reconciliation. Компенсация: soft-архив
IsArchived(lookup скрывает, resolve отдаёт), reconciliation-отчёт после полной пересинхронизации, архив подтверждает администратор (UC-HND-001). - Регион — derive из city-событий: собственного region-события у источника нет; переименование региона доезжает только с city-событием любого из его городов, иначе имя устаревает до пересинхронизации. Принято как допустимое для редко меняющихся geo-данных (ADR-0063 п.4).
- Single-language имена: источник ведёт названия одноязычно (русские); мультиязычности справочника в v1 нет.
- Seed-факт источника:
RU = 1,KZ = 2,BY = 3— идентификаторы стабильны, на них допустимо полагаться в конфигурации потребителей (дефолт страны проектаchessboard—CountryId = 1). - Реплика вторична: PIN не может завести запись, которой нет в источнике; недостающие данные правятся в Talent Handbook.
- Порядок прихода событий разных справочников не гарантирован: ссылки
DefaultCurrencyId/DefaultLanguageId— логические (без FK), нерезолвимая ссылка допустима до прихода события соответствующего справочника.
Use Cases
| UC | Название | Документ |
|---|---|---|
| UC-HND-001 | Администратор поддерживает справочник (archive/restore, sync-state) | use-cases/UC-HND-001-admin-maintains-handbook.md |
Связанные Документы
- Домены: domain/country.md, domain/region.md, domain/city.md, domain/currency.md, domain/language.md, domain/snapshots.md, domain/talent-replica.md.
- Consumer/фоновые: consumers/talent-handbook-sync.md, background/talent-initial-import.md.
- Потребитель-реплика:
../chessboard/domain/handbook-replica.md,../chessboard/consumers/handbook-sync.md. - Образец ACL-паттерна: identity/domain/talent-replica.md, identity/consumers/talent-identity-sync.md.
- Правила ссылок на справочники: strict-typed-documentation-rules.md; границы: service-boundaries.md.
- ADR: 0063 — репликация справочников из Talent, 0056 — simplicity-first, PostgreSQL-only, 0048 — RUB-only, денежная безопасность.