Entity: CityEntity
Дом полей справочной записи «Город» контекста handbook. Проекции (lookup-выдачи с пагинацией,
CitySnapshot, событие pin.handbook.cities.v1, гео-поля проектов chessboard, город лида crm,
CityRefModel в user-activity) ссылаются сюда и описывают только дельту.
Назначение
CityEntity — реплика записи города из Talent Handbook
(ADR-0063). Самый объёмный и самый
востребованный справочник контекста: города — ключ гео-фильтров каталога, адресов проектов и пожеланий
лидов. Записи создаёт и обновляет только синк источника; администратору доступны archive/restore.
Классификация
| Поле | Значение |
|---|---|
| Контекст | handbook |
| Kind | Replica (source of truth — Talent Handbook) |
| Source of truth | talent.microservices.handbook, сущность City; признак архива IsArchived — handbook |
| Таблица | handbook.cities |
| Tenant isolation | не tenanted — платформенный справочник |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | long | Да | PK | синк (= id Talent) | Идентификатор города = id записи источника (ADR-0063 п.2). |
CountryId | short | Да | FK → handbook.countries; index ix_cities_country_id | синк | Страна города. |
RegionId | long? | Нет | FK → handbook.regions; index ix_cities_region_id | синк | Регион города; nullable — источник допускает города вне региона (напр., города федерального значения). |
Name | string | Да | varchar(256), min 1; index ix_cities_name (lower(name)) | синк | Название города («Самара»). |
IsArchived | bool | Да | default false | admin / reconciliation | Soft-архив (delete-gap источника, ADR-0063 п.5). |
LastEventAt | DateTimeOffset | Да | — | синк (envelope CreatedAt применённого city-события) | Время последнего применённого события источника. У событий источника нет per-record версии — монотонность обеспечивается по времени публикации события (INV-HND-4, ADR-0063 п.1). |
CreatedAt / UpdatedAt | DateTimeOffset | Да | — | синк / admin | Штампы создания/изменения. |
Связи
| Связь | Тип | Ключ | Delete behavior |
|---|---|---|---|
CountryEntity | reference (родитель) | CountryId → Country.Id | физических удалений нет |
RegionEntity | reference (родитель, опционально) | RegionId → Region.Id | архив региона не каскадирует на города |
Синк применяет цепочку строго по порядку: страна → регион (derive из этого же city-события,
region.md) → город. City-событие, пришедшее раньше события своей страны, ретраится
(HANDBOOK_SYNC_COUNTRY_NOT_FOUND), а не теряется.
Инварианты
INV-HND-1 (создаёт/меняет только синк), INV-HND-2 (не применим — ISO-кодов нет), INV-HND-4 (монотонность
по LastEventAt — envelope CreatedAt события, per-record версии у источника нет), INV-HND-5 (lookup —
только неархивные; resolve — включая архивные) — объявлены в country.md и действуют для
города без изменений. INV-HND-3 GeoConsistency объявлен
здесь (его аспект для derive региона — region.md):
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-HND-3 GeoConsistency | Согласованность цепочки city ↔ region ↔ country: страна города существует; если RegionId задан — регион существует и Region.CountryId = City.CountryId (город не может ссылаться на регион чужой страны) | при применении city-события синка | страны нет → HANDBOOK_SYNC_COUNTRY_NOT_FOUND (retriable); регион чужой страны / рассогласование → HANDBOOK_SYNC_GEO_MISMATCH → DLQ pin.handbook.talent-sync-dlq.v1 (non-retriable) |
Нарушение INV-HND-3 всегда означает дефект данных источника: событие уходит в DLQ, разрешение — правка данных в Talent Handbook и replay (автопочинки на стороне PIN нет).
Жизненный цикл и операции
| Операция | Актор | Предусловие | Эффект | Отказ |
|---|---|---|---|---|
| Upsert (синк) | consumer группы pin-handbook-talent-sync | envelope.CreatedAt > LastEventAt (INV-HND-4); гео-цепочка согласована (INV-HND-3) | создание/перезапись всех реплика-полей (snapshot-семантика) + re-publish city-upserted | stale → skip; страны нет → retry; гео-рассогласование → DLQ |
| Auto-restore (синк) | consumer | запись заархивирована, из источника пришёл её upsert | IsArchived = false + warn-метрика + re-publish | — |
| Archive / Restore | администратор платформы | запись существует | IsArchived переключён; re-publish; повторный вызов — идемпотентный no-op | 404 HANDBOOK_ENTRY_NOT_FOUND |
| Lookup (list/поиск по имени) | любой (включая гостя) | — | неархивные города (INV-HND-5); фильтры по стране/региону, поиск по префиксу имени, пагинация (единственный справочник, где выдача постранична — тысячи строк) | — |
| Resolve (batch by id) | внутренние сервисы (m2m: write-path-валидация crm/profile, мягкая валидация user-activity) | — | записи по id, включая архивные (INV-HND-5) | неизвестный id отсутствует в ответе |
События
| Событие | Топик | Когда публикуется | Payload |
|---|---|---|---|
city-upserted | pin.handbook.cities.v1 (compacted, key = Id, ContractVersion 1) | применённый upsert синка; admin archive/restore; пересинхронизация | полный snapshot реплики, включая IsArchived и LastEventAt — ../events/city-upserted.md |
Текущий потребитель — chessboard (локальная реплика; при переименовании города обновляет
денормализованные CityName проектов и re-publish-ит затронутые проекты).
Ошибки
| Код | HTTP | Условие |
|---|---|---|
HANDBOOK_ENTRY_NOT_FOUND | 404 | точечное чтение / archive / restore несуществующего города |
HANDBOOK_SYNC_COUNTRY_NOT_FOUND | — (синк, retriable) | city-событие пришло раньше события своей страны; retry с backoff, затем DLQ + алерт |
HANDBOOK_SYNC_GEO_MISMATCH | — (синк, non-retriable → DLQ) | нарушение INV-HND-3: регион города принадлежит другой стране либо не существует после ретраев |
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK pk_cities; ix_cities_country_id; ix_cities_region_id; ix_cities_name (lower(name)) — префиксный поиск в lookup (PostgreSQL, без ES — ADR-0063 п.7) |
| Партиционирование / retention | none — тысячи строк; retention бессрочный |
| Concurrency | события применяются последовательно per-key; admin-операции — оптимистическая блокировка |
| Permissions / PII | archive/restore — admin API (handbook.entries.manage); lookup — публичный; resolve — internal m2m. PII нет |
| Audit | LastEventAt + штампы; admin archive/restore логируется с актором |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (6): Реплика справочников handbook в chessboard (chessboard.handbook_*), Реплика Talent Handbook: правила синка + Entity HandbookSyncState, Entity / Aggregate: ActionEvent (TPH-иерархия событий действий), Entity: CountryEntity, Entity: RegionEntity, Snapshot-модели справочника (единый дом платформы)
- API (5): GET /api/v1/handbook/{countries,regions,cities,currencies,languages} — Публичный lookup справочников, Handbook API — реестр операций областей public / internal / admin, POST /api/v1/admin/handbook/{type}/{id}/{archive,restore} — Архив и восстановление справочной записи, POST /internal/handbook/resolve — Batch-резолв справочных записей по id, PUT /api/agent/crm/leads/{leadId}/wishes — Обновить хотелки лида
- Use Cases (3): UC-HND-001. Администратор поддерживает справочник (archive/restore), UC-HND-002. Система синхронизирует реплику справочника из Talent, US-реестр контекста handbook (US-HND)
- События (1): Event Contract: city-upserted (CityUpsertedEvent)
- Консюмеры (1): Consumer: Talent Handbook Sync (pin-handbook-talent-sync)
- Фоновые задачи (1): Background Job: talent-initial-import (bootstrap и reconciliation справочников из Talent Handbook)
- index.md (1): Handbook Service
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- country.md — INV-HND-1/2/4/5; region.md — derive региона из city-событий;
snapshots.md —
CitySnapshot. - talent-replica.md — правила синка; ../consumers/talent-handbook-sync.md — consumer; ../events/city-upserted.md — контракт события.
- API: ../api/public/lookup-references.md (пагинация городов), ../api/internal/resolve-references.md.
- ADR-0063.