Перейти к основному содержимому

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 classificationPublic — справочные данные без PII
Runtime criticalitymedium (недоступность не ломает чтение витрин — потребители живут на репликах/снапшотах; блокирует write-path-валидацию crm/profile и админ-операции справочника)

Ownership

ArtifactOwner/Rule
PostgreSQL schemahandbook — единственное хранилище (PostgreSQL-only, без Redis/ES — ADR-0056, ADR-0063 п.7)
Tenant isolationне tenanted — платформенные данные, общие для всех компаний и пользователей
API areaspublic (lookup, гость), internal (batch-resolve, m2m), admin (archive/restore, sync-state)
Owned entitiesCountryEntity, RegionEntity, CityEntity, CurrencyEntity, LanguageEntity, HandbookSyncState; snapshot-модели CountrySnapshot/RegionSnapshot/CitySnapshot/CurrencySnapshot (единый дом платформы)
Produced eventscountry-upserted, region-upserted, city-upserted, currency-upserted, language-upserted (pin.handbook.{countries|regions|cities|currencies|languages}.v1)
Consumed events4 топика Talent handbooks.after-{country|city|currency|language}-information-update (ContractVersion 2)
Search indexesnone — lookup и префиксный поиск городов на PostgreSQL
Analytics factsnone

Ответственность И Границы

Делает:

  • консюмит 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.*.v1queue (produce)Донесение изменений до потребителей (chessboard)Потребители отстают; события публикуются после восстановления (at-least-once)
Talent Handbook POST sync-handbooksexternal systemBootstrap и полная пересинхронизация (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/5Talent Handbook (архив — handbook)domain/country.md
RegionEntity (handbook.regions, Id long)Регион — первоклассная запись PIN, derive из city-событий; монотонность по LastEventAtTalent Handbook (через city-события)domain/region.md
CityEntity (handbook.cities, Id long)Город — самый объёмный справочник; ключ гео-фильтров каталога и адресов проектовTalent Handbookdomain/city.md
CurrencyEntity (handbook.currencies, Id short)Валюта: IsoCode ISO-4217 — единственное легитимное char(3) спекаTalent Handbookdomain/currency.md
LanguageEntity (handbook.languages, Id short)Язык; текущих потребителей в PIN нет (UI-локали — не отсюда)Talent Handbookdomain/language.md
HandbookSyncState (handbook.talent_sync_states)Чекпоинты синка — одна строка на запись источника (AggregateType + ExternalId)handbookdomain/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.mdPOST /internal/handbook/resolve, batch-резолв id по всем пяти типам за один вызов, включая архивные записи (INV-HND-5); неизвестный id просто отсутствует в ответе (не ошибка). Потребители: write-path-валидация crm/profile, мягкая валидация user-activity, resolve CurrencyId на admin-write денежных контрактов.
  • Admin (администратор платформы, permission handbook.entries.manage):
    • api/admin/archive-restore-entry.mdPOST /api/v1/admin/handbook/{type}/{id}/archive и .../restore: ручной soft-архив/возврат записи + re-publish события;
    • api/admin/get-sync-state.mdGET /api/v1/admin/handbook/sync-state: чекпоинты HandbookSyncState, лаг, DLQ-счётчики.
  • Callbacks: not applicable.

Events И Consumers

Входящие топики Talent (consumer group pin-handbook-talent-sync)

Топик источникаContractVersionПотребляетсяЭффект
handbooks.after-country-information-update2даupsert реплики страны → re-publish country-upserted
handbooks.after-city-information-update2даupsert реплики города + derive/upsert региона из вложенной RegionEventModel → re-publish city-upserted (+ region-upserted при фактическом изменении региона)
handbooks.after-currency-information-update2даupsert реплики валюты → re-publish currency-upserted
handbooks.after-language-information-update2даupsert реплики языка → re-publish language-upserted
handbooks.after-contact-information-update2нет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-upsertedpin.handbook.countries.v1Реплика стран у потребителейevents/country-upserted.md
region-upsertedpin.handbook.regions.v1Реплика регионов (фильтр-ключ каталога)events/region-upserted.md
city-upsertedpin.handbook.cities.v1Реплика городов (гео-фильтры, адреса)events/city-upserted.md
currency-upsertedpin.handbook.currencies.v1Реплика валют (валидация CurrencyId, display)events/currency-upserted.md
language-upsertedpin.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 не используется.
СервисСтратегияЧто использует
chessboardA — полная реплика через consumer (подписка на pin.handbook.*)CountryId/RegionId/CityId/CurrencyId: hot-path карточек и фильтров, валидация geo/валют на write-path, refresh денормализованных имён при переименованиях (с re-publish затронутых проектов)
catalogB — id + name из snapshot-ов событий chessboard (без реплики и подписки)CountryId/RegionId/CityId/CurrencyId + имена из событий
crm, profileC — internal resolve на write-path (хранят id + денормализованное имя)валидация CityId/CountryId/CurrencyId синхронным resolve при записи
user-activityB/мягкаяCityRefModel{CityId long, CityName} с клиентаCityId; невалидный id не блокирует запись факта
investor-owner, rewards, promotion, messengerB — только CurrencyId + CurrencySnapshot из событий; resolve — точечно на admin-writeCurrencyId
notifications, dev-statsD — не зависят от handbookLocale/TimeZone — строки (ADR-0063 п.3)
identity, news, favorites, selections, support, referral, invest-analyticsD — не затрагиваются

Инвариант 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/SLAPublic lookup p95 < 300 мс; internal resolve p95 < 100 мс; лаг синка от источника — минуты (справочники меняются редко)
Dashboardhandbook-sync-overview (лаг по потокам, stale-skip, DLQ, авто-restore warn); handbook-overview (lookup/resolve/admin-операции)
Alertsconsumer lag > 15 мин; DLQ непуст (GEO_MISMATCH/ISO_CONFLICT); всплеск warn авто-restore
Runbooksbackground/talent-initial-import.md — bootstrap, полная пересинхронизация, reconciliation-отчёт
Feature flagsnone

Безопасность

  • 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 — идентификаторы стабильны, на них допустимо полагаться в конфигурации потребителей (дефолт страны проекта chessboardCountryId = 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

Связанные Документы