POST /api/admin/v1/projections/rebuild — Принудительный пересчёт карточек по id (admin)
Назначение
Runbook-инструмент устранения расхождений витрины: администратор платформы принудительно
пересобирает денормализованные карточки (catalog-property-object-card.md /
catalog-project-card.md) по списку id, когда SourceSnapshot
уже консистентен, а эффективные поля/Status/агрегаты витрины разошлись (пропущенный пересчёт после
редкой гонки консюмеров, ручное вмешательство в БД, откат миграции). Пересчёт не ходит в источники:
он идемпотентно переигрывает те же функции проекции, что и консюмеры/пересчёты оверрайдов, поверх уже
сохранённых SourceSnapshot + живых оверрайдов + активных правил ранжирования + активных покупок
продвижения. Штатный путь синхронизации — консюмеры chessboard/invest-analytics/rewards; этот
endpoint — точечная ручная починка по runbook расхождений SourceSnapshot.ChessboardVersion
(README каталога, Runtime; chessboard-projection-sync.md).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | catalog |
| API area | admin (ADR-0053; m2m) |
| Feature group | admin-projections |
| Статус | draft |
| Документ | docs/06-services/catalog/api/admin/rebuild-projections.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | ExternalSystem (администратор платформы через admin API; m2m client-credentials — ADR-0053) |
| Auth policy / Permissions | policy AdminSystemOnly (клиент pin-admin-system); каталожное соответствие — permission catalog.overrides.manage (эксплуатация проекций — под тем же admin-scope; identity/domain/permission.md) |
| Scope (RBAC) | платформа (карточки не tenanted — ADR-0052 п.3) |
| Audit | обязателен: deep audit admin-операции — actor (X-Admin-User-Id), TargetKind, полный список Ids, RebuiltCount, предупреждения, ResetAnalyticsAsOf, результат |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/admin/v1/projections/rebuild |
| Success status | 200 (пересчёт выполнен; частично отсутствующие id — в Warnings, не ошибка) |
| Idempotency header | Idempotency-Key (Guid; обязателен — администратор платформы ретраит по таймауту) |
| Correlation | сквозной trace через traceparent; X-Admin-User-Id (Guid админа) — обязателен |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TargetKind | body | string (enum RebuildTargetKind) | Да | ∈ {Project, PropertyObject} — уровень пересобираемых карточек | — |
Ids | body | Guid[] | Да | 1..MaxBatchSize (default 200); distinct (дубликаты схлопываются + warn); каждый != Guid.Empty; батч ограничен ради одной транзакции и предсказуемого runbook-прогона | — |
ResetAnalyticsAsOf | body | bool | Нет (default false) | true → обнулить SourceSnapshot.AnalyticsAsOf пересобранных карточек, чтобы метрики доехали replay-ем/следующими событиями invest-analytics (см. invest-analytics-enrichment.md, Идемпотентность и replay) | — |
AdminUserId | header X-Admin-User-Id | Guid | Да | != Guid.Empty | audit-stamping |
IdempotencyKey | header Idempotency-Key | Guid | Да | != Guid.Empty | — |
Ids — идентификаторы конкретных карточек (= id в chessboard): для TargetKind=Project — id
project_cards, для TargetKind=PropertyObject — id property_object_cards.
Модель ответа RebuildProjectionsResult
Именованная container-модель; коллекции ограничены размером входного батча (≤ MaxBatchSize).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
TargetKind | string (enum RebuildTargetKind) | Да | вход | — | Уровень пересчёта. |
RequestedCount | int | Да | вычисление | — | Размер Ids после distinct. |
RebuiltCount | int | Да | вычисление | — | Фактически пересчитанных карточек. |
RebuiltIds | IReadOnlyList<Guid> | Да | пересчёт | — | Id успешно пересобранных карточек. |
Warnings | IReadOnlyList<RebuildProjectionWarning> | Да | вычисление | — | Пропущенные id (не найдено / делистинг). Пустой список — все пересчитаны. |
RebuildProjectionWarning
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | вход | — | Пропущенный или свёрнутый id. |
Reason | string (enum RebuildWarningReason) | Да | вычисление | — | NotFound (карточки нет) / Delisted (карточка Delisted — не пересобирается) / Duplicate (id встречался во входе более одного раза — свёрнут в один пересчёт). |
Алгоритм
- Контекст и доступ. Актор — администратор платформы (admin API, m2m
pin-admin-system, policyAdminSystemOnly); заголовкиX-Admin-User-Id,Idempotency-Key. Повтор по ключуadmin:projections-rebuild:{IdempotencyKey}→ сохранённыйRebuildProjectionsResult(200). - Проверка входа.
TargetKind∈ {Project,PropertyObject};Idsне пуст → иначеCATALOG_REBUILD_EMPTY_BATCH;Ids.Count ≤ MaxBatchSize(проверяется по исходному входу, доdistinct) → иначеCATALOG_REBUILD_BATCH_TOO_LARGE; повторяющиеся id схлопываются (RequestedCount= число уникальных id), каждый повтор →Warnings(Duplicate); каждый!= Guid.Empty→ иначеValidationError. Существование целей — не жёсткая ошибка: отсутствующие id уходят вWarnings(NotFound), батч применяется частично. Состояние:Delisted-карточки →Warnings(Delisted), не пересобираются (INV-7 property-object-card / делистинг project-card — удалённый источником объект не воскрешается ручным rebuild-ом). Итог:RequestedCount(уникальные) =RebuiltCount+ число уникальных id вWarnings(NotFound|Delisted). - Что читаем (батч по id, порядок: карточка → её оверрайды/правила/покупки → родительский проект):
TargetKind=PropertyObject: карточки объектов недвижимости поIds(отсутствующие →Warnings); на каждую — живой оверрайд объекта недвижимости, живой оверрайд её корпуса, карточку её проекта; активные правила ранжирования объекта недвижимости/проекта и активные покупки продвижения, затрагивающие карточку.TargetKind=Project: карточки проектов поIds; на каждую — живые оверрайды корпусов проекта, объекты недвижимости проекта в статусеPublished(для агрегатов), активное правило ранжирования и активные проектные покупки.
- Правила и переходы (единственная функция пересчёта — та же, что у консюмеров и мутаций
оверрайдов; маппинг поле-в-поле):
- Эффективные поля:
Effective = Override(поле) ?? SourceSnapshot(поле)(INV-3 property-object-card; корпуснойComplexName → Nameдля project-card);PricePerMeter = round(Price / AreaTotal, 2). - Видимость:
Statusпо INV-1 (PropertyObjectStatus != Hidden∧ оверрайдыIsVisible != false∧ проектPublished); каскад скрытия проекта на объекты недвижимости — INV-5 project-card. DeveloperPriority=Active-правило объекта недвижимости?? Active-правило проекта?? 0(INV-5 ranking-rule).PromotionBoost= ΣBoostWeightактивных покупок, затрагивающих карточку (проектныеProjectBoost+ объекты недвижимостиPropertyObjectBoostс этим объектом недвижимости; INV-6 promotion).ResetAnalyticsAsOf = true→SourceSnapshot.AnalyticsAsOf = null(метрики применит replayinvest-analytics, guardAsOf > nullпройдёт).- Фиксируются доменные события: объект недвижимости →
PropertyObjectCardRecomputedEvent(Id,ProjectId,Price,Status,PropertyObjectStatus), обработчик в том же коммите пересчитывает агрегаты проекта (MinPrice/MaxPrice/PropertyObjectsTotal/PropertyObjectsAvailable/Payback— INV-2 project-card); проект →ProjectCardRecomputedEvent(Id,Status,MinPrice,MaxPrice,PropertyObjectsAvailable) + прямой пересчёт агрегатов поPublished-объектам недвижимости.
- Эффективные поля:
- Что меняется. Обновляются все пересчитанные карточки и затронутые родительские
project_cards.RebuiltCount/RebuiltIds= карточки, дошедшие до обновления. Правка не должна затирать чужую (конкурентный доступ): конфликт с конкурирующим консюмером →CONCURRENCY_CONFLICT, retry всей операцией с тем жеIdempotency-Key(пересчёт идемпотентен и детерминирован). - События. Доменные
PropertyObjectCardRecomputedEvent/ProjectCardRecomputedEvent— фиксируются в том же коммите. Интеграционные события не публикуются (витрина — конечная точка данных; обоснование — catalog-property-object-card.md, раздел «События»). - Согласованность. Все пересчитанные карточки батча + пересчитанные агрегаты проектов + отметка
идемпотентности + deep-audit — в одном согласованном изменении (весь батч — одна транзакция,
≤ MaxBatchSizeстрок; simplicity-first ADR-0056). Конфликт версий откатывает батч целиком. - Результат.
RebuildProjectionsResult(200;RebuiltCount,RebuiltIds,Warnings— консистентно транзакции); отказ → ProblemDetails с кодами таблицы ниже.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Проекции / поиск | пересчёт catalog.property_object_cards/catalog.project_cards (эффективные поля, Status, DeveloperPriority, PromotionBoost, агрегаты, NameVector — generated column) в одной транзакции; при ResetAnalyticsAsOf — обнуление SourceSnapshot.AnalyticsAsOf (доедет replay-ем) |
Интеграционные события не публикуются; внешних вызовов нет (пересчёт из локальных SourceSnapshot +
оверрайдов/правил/покупок, в источники не ходит); уведомлений нет.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | TargetKind вне enum / Id = Guid.Empty | Ошибка валидации запроса | no retry |
CATALOG_REBUILD_EMPTY_BATCH | 400 | Ids пуст | Список id для пересчёта пуст | no retry |
CATALOG_REBUILD_BATCH_TOO_LARGE | 400 | Ids.Count > MaxBatchSize | Превышен размер батча пересчёта | no retry (разбить батч) |
UNAUTHORIZED | 401 | Нет/невалиден m2m-токен или X-Admin-User-Id | Требуется авторизация | no retry |
FORBIDDEN | 403 | Клиент не pin-admin-system / нет catalog.overrides.manage | Доступ запрещён | no retry |
CONCURRENCY_CONFLICT | 409 | Гонка версий с консюмером/вторым админом | Данные изменены параллельно; повторите | retry с тем же Idempotency-Key |
Отсутствующие (NotFound) и Delisted id возвращаются в Warnings при 200 — это штатный сценарий
runbook-сверки, а не отказ запроса.
Совместимость и наблюдаемость
- Новые optional поля запроса/ответа — additive (без breaking change); удаление/переименование/смена
типа — breaking (migration + rollout).
RebuildTargetKind/RebuildWarningReason— enum строкой, unknown-значение запроса →ValidationError. - Logs:
admin-projections-rebuild(TargetKind,RequestedCount,RebuiltCount, число warnings,ResetAnalyticsAsOf,adminUserId; сами id — в audit, не в plain-логе); metrics:catalog_admin_operations_total{operation="rebuild-projections"}, duration,catalog_projection_recompute_total{trigger="rebuild"}; traces: span приёма → span обработки → spans обращений к хранилищу; correlation — сквозной trace черезtraceparent. Каждый вызов — в deep audit. Dashboard catalog-overview; runbook — сверка/rebuild проекций по id при расхожденииSourceSnapshot.ChessboardVersion.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): Catalog Admin API — реестр методов и общие правила области
- Консюмеры (2): Consumer: Chessboard Projection Sync (pin-catalog-chessboard-sync), Consumer: Invest-Analytics Enrichment (pin-catalog-analytics-enrichment)
- registries (1): Реестр API-методов PIN
Связанные документы
- Проекции: catalog-property-object-card.md (INV-1/INV-3 — видимость и
эффективные поля), catalog-project-card.md (INV-2 —
агрегаты); правила пересчёта: display-override.md,
ranking-rule.md (INV-5), ../../consumers/promotion-boost.md (
PromotionBoost— консюмерpromotion). - Штатная синхронизация: chessboard-projection-sync.md
(runbook
SourceSnapshot.ChessboardVersion), invest-analytics-enrichment.md (ResetAnalyticsAsOf→ replay метрик). - Соседние admin-операции: put-property-object-override.md, put-building-override.md.
- ADR-0053 (m2m + deep audit), ADR-0056 (пересчёт в транзакции); README каталога (Runtime, Runbooks).