Реплика справочников handbook в chessboard (chessboard.handbook_*)
Дом полей и правил локальной реплики справочников handbook внутри контекста chessboard: страны,
регионы, города, валюты. Consumer handbook-sync и write-path-операции
проектов ссылаются сюда, не переописывая поля и правила.
Назначение
chessboard — единственный сервис PIN с локальной репликой справочников
(ADR-0063 п.6): гео и валюта
участвуют в hot-path карточек/фильтров и в каждой мутации проекта, поэтому валидация ссылок
CountryId/RegionId/CityId/CurrencyId на write-path выполняется по локальным таблицам без
сетевого вызова в handbook. Вторая задача реплики — источник актуальных имён для refresh
денормализованных полей проектов RegionName/CityName (project.md) при переименованиях в
справочнике с переопубликацией project-upserted, чтобы проекции catalog догнали новое имя.
Реплика проекционная, а не полная копия handbook: хранятся только поля, которые chessboard реально
использует. Справочник языков (pin.handbook.languages.v1) не потребляется — у chessboard нет ссылок на
языки. Ведение данных остаётся в Talent Handbook → handbook; в chessboard реплику пишет только consumer.
Классификация
| Поле | Значение |
|---|---|
| Контекст | chessboard |
| Kind | Replica (source of truth — handbook; транзитивно — Talent Handbook, ADR-0063) |
| Source of truth | контекст handbook: country / region / city / currency |
| Таблицы | chessboard.handbook_countries, chessboard.handbook_regions, chessboard.handbook_cities, chessboard.handbook_currencies |
| Наполнение | только consumer группы pin-chessboard-handbook-sync (handbook-sync) из compacted-топиков `pin.handbook.{countries |
| Tenant isolation | не tenanted — платформенный справочник, общий для всех застройщиков |
Поля
Идентификаторы — id источника (short/long, без Guid, ADR-0063 п.2); значения полей — полный snapshot
из payload событий pin.handbook.* (контракты: country-upserted,
region-upserted,
city-upserted,
currency-upserted).
HandbookCountryReplica (chessboard.handbook_countries)
| Поле | Тип | Обязательность | Ограничения | Источник (payload country-upserted) | Описание |
|---|---|---|---|---|---|
Id | short | Да | PK | Id | Идентификатор страны (= id источника). |
Name | string | Да | varchar(64), min 1 | FullName (display-конвенция Name = FullName, snapshots.md) | Название страны («Российская Федерация»). |
IsoCode2 | string | Да | char(2), upper | IsoCode2 | Код ISO 3166-1 alpha-2 (RU) — display/интеграции. |
IsoCode3 | string | Да | char(3), upper | IsoCode3 | Код ISO 3166-1 alpha-3 (RUS). |
IsArchived | bool | Да | default false | IsArchived | Soft-архив источника: запись выведена из оборота (HR-5). |
ReplicaVersion | long | Да | >= 1 | ReplicaVersion | Монотонный version-guard (HR-2). |
SyncedAt | DateTimeOffset | Да | — | время применения события | Штамп применения; основа метрики staleness. |
Поля ShortName/DefaultCurrencyId/DefaultLanguageId события chessboard не читает и не хранит.
Seed-факт источника: Россия — CountryId = 1 (дефолт страны проекта при создании).
HandbookRegionReplica (chessboard.handbook_regions)
| Поле | Тип | Обязательность | Ограничения | Источник (payload region-upserted) | Описание |
|---|---|---|---|---|---|
Id | long | Да | PK | Id | Идентификатор региона (= id источника). |
CountryId | short | Да | логическая ссылка на handbook_countries (без FK, HR-4), index ix_handbook_regions_country_id | CountryId | Страна региона — проверка согласованности гео на write-path. |
Name | string | Да | varchar(256), min 1 | Name | Название региона («Самарская область») — источник refresh Project.RegionName (HR-6). |
IsArchived | bool | Да | default false | IsArchived | Soft-архив (HR-5). |
LastEventAt | DateTimeOffset | Да | — | LastEventAt | Роль версии: у региона нет ReplicaVersion источника — time-based guard (HR-2, контракт region-upserted). |
SyncedAt | DateTimeOffset | Да | — | время применения события | Штамп применения. |
HandbookCityReplica (chessboard.handbook_cities)
| Поле | Тип | Обязательность | Ограничения | Источник (payload city-upserted) | Описание |
|---|---|---|---|---|---|
Id | long | Да | PK | Id | Идентификатор города (= id источника). |
CountryId | short | Да | логическая ссылка (без FK, HR-4), index ix_handbook_cities_country_id | CountryId | Страна города. |
RegionId | long? | Нет | логическая ссылка (без FK), index ix_handbook_cities_region_id (where not null) | RegionId | Регион города; null — город вне региона (города федерального значения). |
Name | string | Да | varchar(256), min 1 | Name | Название города («Самара») — источник refresh Project.CityName (HR-6). |
IsArchived | bool | Да | default false | IsArchived | Soft-архив (HR-5). |
ReplicaVersion | long | Да | >= 1 | ReplicaVersion | Монотонный version-guard (HR-2). |
SyncedAt | DateTimeOffset | Да | — | время применения события | Штамп применения. |
Вложенные снапшоты Country/Region payload-а chessboard не хранит: актуальные имена страны/региона
ведутся собственными строками handbook_countries/handbook_regions по их топикам.
HandbookCurrencyReplica (chessboard.handbook_currencies)
| Поле | Тип | Обязательность | Ограничения | Источник (payload currency-upserted) | Описание |
|---|---|---|---|---|---|
Id | short | Да | PK | Id | Идентификатор валюты (= id источника). |
Name | string | Да | varchar(64), min 1 | Name | Название валюты («Российский рубль»). |
IsoCode | string | Да | char(3), upper, ISO-4217 | IsoCode | Display-код (RUB) — источник поля IsoCode CurrencySnapshot исходящих событий chessboard. |
Sign | string? | Нет | varchar(16) | Sign | Знак валюты («₽») — для CurrencySnapshot. |
IsArchived | bool | Да | default false | IsArchived | Soft-архив (HR-5). |
ReplicaVersion | long | Да | >= 1 | ReplicaVersion | Монотонный version-guard (HR-2). |
SyncedAt | DateTimeOffset | Да | — | время применения события | Штамп применения. |
Name/IsoCode/Sign нужны chessboard не только для валидации CurrencyId: из них собирается
CurrencySnapshot в исходящих событиях с денежными полями (снимается на момент публикации, правила
snapshots.md). Справочник валют не означает мультивалютность —
денежные операции остаются RUB-only (ADR-0048).
Связи
Логические ссылки (без FK, HR-4) связывают гео-сущности реплики; валюта (HandbookCurrencyReplica)
самостоятельна — гео-таблицы не хранят ссылку на неё (DefaultCurrencyId источника не читается, см.
выше).
Правила ведения реплики (HR-1..HR-7)
| Правило | Содержание |
|---|---|
| HR-1 SingleWriter | Реплику пишет только consumer pin-chessboard-handbook-sync. HTTP-операций мутации реплики в chessboard нет (ни developer, ни admin); ведение данных — Talent Handbook, archive/restore — admin API handbook. |
| HR-2 VersionGuard | Событие применяется только при payload.ReplicaVersion > хранимой версии записи (регион — payload.LastEventAt > хранимого LastEventAt); устаревшее/повторное событие — идемпотентный skip + метрика stale. Компакция и полная пересинхронизация handbook дают «скачки» версии — каждое событие самодостаточно. |
| HR-3 SnapshotSemantics | Payload — полный snapshot: применённое событие перезаписывает все поля записи реплики; отсутствие optional-поля (RegionId города, Sign валюты) означает «значение очищено», а не «оставить прежнее». |
| HR-4 NoFk | Ссылки CountryId/RegionId внутри реплики — логические, без FK: порядок между топиками справочника не гарантирован (город может приехать раньше своей страны/региона). Нерезолвимая ссылка внутри реплики допустима и не блокирует применение события; полнота гео-цепочки проверяется только на write-path проектов. |
| HR-5 ArchiveSemantics | Архивная запись (IsArchived = true) не предлагается в lookup-выдачах chessboard и не принимается при создании/изменении проекта (CHESSBOARD_GEO_REF_ARCHIVED); существующие ссылки проектов/объектов не ломаются и не чистятся — чтение карточек и резолв уже сохранённых id работают включая архивные записи. Физических удалений в реплике нет. |
| HR-6 NameRefresh | При применённом upsert-е региона/города, изменившем Name, chessboard в том же согласованном изменении обновляет денормализованные RegionName/CityName всех проектов с этим RegionId/CityId и переопубликует project-upserted каждого затронутого проекта — подробности ниже. |
| HR-7 Bootstrap | Начальное наполнение — чтение всех четырёх compacted-топиков pin.handbook.*.v1 с offset 0 (каждый топик хранит последнюю запись каждого id бессрочно); отдельного HTTP-импорта нет. Порядок топиков не важен (HR-4). Re-emit при пересинхронизации handbook — идемпотентный no-op (HR-2), кроме записей с фактическими изменениями. |
Использование на write-path проектов
Валидация справочных ссылок выполняется по реплике при создании/изменении проекта (операции
api/developers/create-project/update-project) и при изменении валютных ссылок цен. Публикация проекта
требует резолвленных гео-id (INV-PRJ-3, project.md).
| Проверка | Условие | Ошибка | HTTP |
|---|---|---|---|
| Ссылка существует | CountryId/RegionId/CityId/CurrencyId найден в реплике (после fallback-резолва, см. ниже) | CHESSBOARD_GEO_REF_UNKNOWN | 422 |
| Ссылка не архивна | запись не IsArchived — только при создании/изменении значения поля; уже сохранённое значение при правке других полей не перепроверяется (HR-5) | CHESSBOARD_GEO_REF_ARCHIVED | 422 |
| Гео-цепочка согласована | city.CountryId = указанной стране; при заданном RegionId — city.RegionId = указанному региону и region.CountryId = указанной стране | CHESSBOARD_CITY_REGION_MISMATCH | 422 |
Коды распространяются на любую справочную ссылку chessboard, включая CurrencyId (для валюты применимы
первые две проверки). Обработка ошибок для пользователя: обновить lookup-справочник (public lookup
handbook) и поправить выбор; retry не поможет.
Если id не найден в локальной реплике, write-path делает разовый синхронный batch-резолв в handbook
(resolve-references) — это закрывает окно
eventual-consistency между появлением записи в справочнике и её доездом до реплики. Найденная запись
применяется к валидации (в реплику не пишется — реплика ведётся только событиями, HR-1); не найденная —
отказ CHESSBOARD_GEO_REF_UNKNOWN. Недоступность handbook при fallback не роняет операцию только в
одном случае — id есть в реплике; при отсутствии и в реплике, и ответа resolve — отказ как «не найдено».
Refresh денормализованных имён (HR-6)
Проекты хранят канонический паттерн ссылки «id + display-имя»: RegionId + RegionName, CityId +
CityName (project.md; имена снимаются из реплики при записи проекта). Переименование в
справочнике должно доехать до карточек и фильтров — за это отвечает consumer:
- Триггер. Применённый upsert
region-upserted/city-upserted, при которомNameизменилось относительно хранимого в реплике (включая первое появление записи, если проекты уже ссылаются на id — самопочинка после ручных разборов). - Эффект. В одном согласованном изменении с upsert-ом реплики: у всех проектов с этим
RegionId/CityId, чьёRegionName/CityNameотличается от нового имени, — поле обновляется, версия проекта растёт, и для каждого затронутого проекта переопубликуетсяproject-upserted(полный snapshot, key =project.Id) — проекцииcatalog, измеренияdev-statsи поисковый индекс догоняют новое имя штатным путём, без специального «события переименования». - Условие «отличается» делает refresh идемпотентным: replay топика и re-emit пересинхронизации не порождают повторных обновлений и лишних событий.
- Не триггерит refresh: archive/restore без смены имени; переименование страны (
CountryNameв проекте не денормализуется); переименование валюты (CurrencySnapshotисходящих событий фиксируется на момент публикации и не «догоняется» — правило 3 snapshots.md). - Объём. Переименование региона затрагивает до всех проектов региона (масштаб платформы — тысячи
проектов, ADR-0056): обновление выполняется батчево одним изменением, события — по одному на проект;
витрина каталога догоняет асинхронно в пределах штатного окна применения
project-upserted.
Согласованность и гарантии для потребителей chessboard
- Read-after-write внутри chessboard: write-path валидация видит реплику в состоянии на момент
запроса; окно отставания от
handbook— доставка Kafka + применение (SLA консюмера < 1 мин, алерт по лагу — handbook-sync); окно закрывается fallback-резолвом. - Для downstream chessboard ничего не меняется:
catalog/dev-stats/crmполучают гео и валюту из snapshot-ов событий chessboard (RegionId+RegionName,CityId+CityName,CurrencyId+CurrencySnapshot) и наpin.handbook.*не подписываются (стратегия потребления — ADR-0063 п.6). - Устаревание имени региона в источнике (гэп region-событий Talent) наследуется репликой и закрывается
полной пересинхронизацией
handbook(ADR-0063 п.4) — chessboard дополнительных компенсаций не делает.
Ошибки
| Код | HTTP | Условие |
|---|---|---|
CHESSBOARD_GEO_REF_UNKNOWN | 422 | справочная ссылка (CountryId/RegionId/CityId/CurrencyId) не найдена ни в реплике, ни fallback-резолвом |
CHESSBOARD_GEO_REF_ARCHIVED | 422 | при создании/изменении указана архивная запись справочника (HR-5) |
CHESSBOARD_CITY_REGION_MISMATCH | 422 | город/регион/страна рассогласованы (гео-цепочка не сходится) |
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK по Id каждой таблицы; ix_handbook_regions_country_id; ix_handbook_cities_country_id; ix_handbook_cities_region_id (where not null). Уникальность ISO-кодов не дублируется — гарантируется источником (INV-HND-2, country.md) |
| Партиционирование / retention | none — суммарно тысячи строк; retention бессрочный (физических удалений нет, HR-5) |
| Concurrency | события применяются последовательно per-key (партиции compacted-топиков); гонка refresh-а с правкой проекта разрешается оптимистической блокировкой проекта (retry консюмера с перечитыванием) |
| Permissions / PII | реплика — internal-данные chessboard, собственного API не имеет; lookup справочников для UI — публичный API handbook. PII нет |
| Audit | ReplicaVersion/LastEventAt + SyncedAt; изменения проекта при refresh штампуются system-actor-ом консюмера |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Entity / Aggregate: ProjectEntity, Entity / Aggregate: PropertyObjectEntity, Enum-справочники контекста chessboard
- API (6): GET /api/v1/developers/projects — Поиск/список проектов застройщика, GET /api/v1/developers/projects/{projectId} — Карточка проекта застройщика, GET /api/v1/public/projects — Лента published-проектов (поиск + фильтры + сортировки), GET /api/v1/public/projects/{projectId} — Карточка проекта (ограниченная/полная), POST /api/v1/developers/projects — Создание проекта, PUT /api/v1/developers/projects/{projectId} — Обновление проекта
- Use Cases (1): UC-CHS-001. Создание и публикация проекта застройщиком
- События (3): Event Contract: pin.chessboard.project-upserted (ProjectUpsertedKafkaEvent), Event Contract: pin.chessboard.property-object-price-changed (PropertyObjectPriceChangedKafkaEvent), Event Contract: pin.chessboard.property-object-upserted (PropertyObjectUpsertedKafkaEvent)
- Консюмеры (3): Consumer: Chessboard Projection Sync (pin-catalog-chessboard-sync), Consumer: Handbook Sync (pin-chessboard-handbook-sync), Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- Поиск (1): Search Document: ProjectSearchDocument
- index.md (1): Chessboard Service (Pin.Chessboard)
Связанные документы
- Consumer-наполнение: ../consumers/handbook-sync.md.
- Source of truth: country · region · city · currency; snapshot-модели — snapshots.md; инварианты справочника INV-HND-1..5 — country.md.
- Контракты входных событий: country-upserted, region-upserted, city-upserted, currency-upserted.
- Fallback-резолв: resolve-references.md.
- Поля refresh: project.md (
RegionId/RegionName/CityId/CityName); переопубликуемое событие: ../events/project-upserted.md. - ADR-0063 п.6 (стратегия «реплика только в chessboard»), ADR-0056 (simplicity-first), ADR-0048 (RUB-only).