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

PUT /api/admin/v1/buildings/{buildingId}/override — Создание/изменение оверрайда корпуса (admin)

Назначение

Полный upsert корпусного оверрайда (BuildingDisplayOverride, display-override.md): администратор перекрывает название комплекса, звёздность, срок сдачи, готовность, адрес, дистанцию и видимость корпуса поверх данных chessboard. Семантика PUT — полное состояние (поле null = не переопределено). Правка каскадна: пересчитываются эффективные поля карточки проекта и всех объектов недвижимости корпуса в той же транзакции (INV-6 display-override); IsVisible=false скрывает все объекты недвижимости корпуса (INV-1 property-object-card).

Снятие оверрайда — симметричная операция DELETE /api/admin/v1/buildings/{buildingId}/override (отдельный документ не заводится, дельта описана здесь): soft-delete + тот же каскадный пересчёт из SourceSnapshot; ответ 200 (Removed = true); отсутствие живого оверрайда → 404 CATALOG_OVERRIDE_NOT_FOUND.

Metadata

ПолеЗначение
Сервис/контекстcatalog
API areaadmin (ADR-0053)
Feature groupadmin-overrides
Статусdraft
Документdocs/06-services/catalog/api/admin/put-building-override.md

Актор и доступ

ПроверкаЗначение
Actor typeExternalSystem (администратор платформы через admin API; m2m client-credentials — ADR-0053)
Auth policy / Permissionspolicy AdminSystemOnly (клиент pin-admin-system); каталожное соответствие — permission catalog.overrides.manage (identity/domain/permission.md)
Scope (RBAC)платформа (не tenanted — ADR-0052 п.3)
Auditобязателен: deep audit admin-мутаций — actor (X-Admin-User-Id), buildingId, диф полей, Comment, результат; actor-stamping CreatedByUserId/UpdatedByUserId

HTTP-контракт

ПолеЗначение
MethodPUT (delete-дельта: DELETE, тот же route)
Route/api/admin/v1/buildings/{buildingId}/override
Success status200
Idempotency headerIdempotency-Key (Guid; обязателен)
Correlationсквозной trace через traceparent; X-Admin-User-Id — обязателен

Входные данные / параметры запроса

Типы/ограничения — дом display-override.md.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
BuildingIdrouteGuidДа!= Guid.Empty; существование корпуса в витрине — шаг 2 (INV-2 TargetKnown)
IsVisiblebodybool?Нетfalse каскадно скрывает объекты недвижимости корпуса
ComplexNamebodystring?Нет1..256 символов после trim
Starsbodyshort?Нет1..5
DeliveryDatebodyDateOnly?Нет
ReadinessPercentbodyshort?Нет0..100
Addressbodystring?Нет≤ 512 символов
DistanceMinutesbodyint?Нет>= 0
Commentbodystring?Нет≤ 1024 символовinternal — только admin-область и audit
ExpectedVersionbodylong?Нет>= 1; optimistic-check против BuildingDisplayOverride.Version
AdminUserIdheader X-Admin-User-IdGuidДа!= Guid.Emptyaudit-stamping
IdempotencyKeyheader Idempotency-KeyGuidДа!= Guid.Empty

Кросс-полевая валидация: хотя бы одно override-поле (IsVisibleDistanceMinutes) не null — INV-3 AtLeastOneField → CATALOG_OVERRIDE_EMPTY.

Модель ответа BuildingOverrideMutationResult

ПолеТипОбязательностьИсточникMaskingОписание
OverrideIdGuidДаBuildingDisplayOverride.IdОверрайд (созданный/обновлённый/снятый).
BuildingIdGuidДаBuildingDisplayOverride.BuildingIdКорпус.
ProjectIdGuidДаBuildingDisplayOverride.ProjectIdПроект (денормализован в оверрайде).
CreatedboolДавычислениеСоздан новый / обновлён существующий.
RemovedboolДавычислениеtrue только в delete-дельте.
VersionlongДаBuildingDisplayOverride.VersionВерсия после мутации.
EffectiveBuildingOverrideEffectiveBlockДаCatalogProjectCard после пересчётаЭффективные значения (модель — get-building-override.md).
ProjectCardStatusstring (enum ProjectCardStatus)ДаCatalogProjectCard.Status после пересчётаИтоговая видимость проекта.
PropertyObjectsRecomputedintДасчётчик мутаций шага 5Сколько карточек объектов недвижимости корпуса пересчитано.

Алгоритм

  1. Контекст и доступ. Актор — администратор платформы (admin API, m2m pin-admin-system, policy AdminSystemOnly); заголовки X-Admin-User-Id, Idempotency-Key. Повтор по ключу admin:building-override:{IdempotencyKey} → сохранённый результат (200).
  2. Проверка входа. Диапазоны/длины (Stars 1..5, ReadinessPercent 0..100, `DistanceMinutes

    = 0, длины строк) → ValidationError; хотя бы одно override-поле не null(INV-3 AtLeastOneField), иначеCATALOG_OVERRIDE_EMPTY. Существование цели (INV-2 TargetKnown): корпус есть в витрине (хотя бы один объект недвижимости с этим BuildingId); нет → CATALOG_OVERRIDE_TARGET_NOT_FOUND(404). Состояние: карточка проекта корпусаDelistedCATALOG_OVERRIDE_TARGET_DELISTED(409). Optimistic-check: заданныйExpectedVersionне совпал с версией живого оверрайда →CONCURRENCY_CONFLICT` (409).

  3. Что читаем. Живой оверрайд корпуса (есть → update, нет → create; единственность — INV-1 SingleLiveOverride + unique partial index ux_building_overrides_building_id), карточку проекта корпуса, объекты недвижимости корпуса (кроме Delisted) и их собственные оверрайды объектов недвижимости — для полного каскада Effective = PropertyObjectOverride ?? BuildingOverride ?? SourceSnapshot по корпусным полям.
  4. Правила и переходы. Create: новый оверрайд со всеми переданными полями, ProjectId из проекта корпуса, CreatedByUserId = AdminUserId, Version = 1. Update: полная замена полей (PUT-семантика), UpdatedByUserId = AdminUserId, Version++. Фиксируется доменное событие BuildingDisplayOverrideChangedEvent (Id, BuildingId, ProjectId, набор изменённых полей). Каскадный пересчёт (INV-6): карточка проекта получает эффективные Name = ComplexName ?? SourceSnapshot.Name, Address, Stars, DeliveryDate, ReadinessPercent, DistanceMinutes и пересчитывает видимость (INV-1 project-card: все корпуса скрыты оверрайдами → Hidden); каждый объект недвижимости корпуса получает корпусные поля (Stars, DeliveryDate, ReadinessPercent, DistanceMinutes) и пересчитывает видимость (INV-1 property-object-card: BuildingOverride.IsVisible=falseHidden); по каждому объекту фиксируется PropertyObjectCardRecomputedEvent, который в том же коммите пересчитывает агрегаты карточки проекта (MinPrice/MaxPrice/PropertyObjectsTotal/PropertyObjectsAvailable/Payback — INV-2 project-card). Приоритет видимости: BuildingOverride.IsVisible=false скрывает объект недвижимости независимо от его собственного оверрайда — оверрайд объекта с IsVisible=true не «воскрешает» объект скрытого корпуса (корпусной уровень строже); наоборот, при возврате корпуса в видимое состояние объект снова оценивается по INV-1 property-object-card (учитывая его собственный оверрайд и PropertyObjectStatus источника).
  5. Что меняется. Создаётся/обновляется оверрайд, обновляются карточка проекта и все карточки объектов недвижимости корпуса (счётчик → PropertyObjectsRecomputed). Порядок: оверрайд → проект → объекты недвижимости. Правка не должна затирать чужую (конкурентный доступ); конфликт с консюмером → retry администратором платформы с тем же Idempotency-Key.
  6. События. Доменные BuildingDisplayOverrideChangedEvent, PropertyObjectCardRecomputedEvent (по объекту недвижимости), ProjectCardRecomputedEvent — фиксируются в том же коммите. Интеграционные события не публикуются (display-override.md, «События»).
  7. Согласованность. Оверрайд + карточка проекта + карточки объектов недвижимости + отметка идемпотентности + audit — в одном согласованном изменении (INV-6 RecomputeInTx). Объём батча ограничен размером корпуса (сотни объектов недвижимости) — приемлемо для одной транзакции (ADR-0056).
  8. Результат. BuildingOverrideMutationResult (200); отказ → ProblemDetails.

Диаграмма

Побочные эффекты

ТипДетали
Проекции / поискпересчёт catalog.project_cards (эффективные поля, Status, NameVector по Name/Address) и всех catalog.property_object_cards корпуса (корпусные поля, Status) в той же транзакции

Интеграционные события не публикуются (только доменные — фиксируются вместе с пересчётом); внешних вызовов и уведомлений нет.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Диапазоны/длины/заголовкиОшибка валидации запросаno retry
CATALOG_OVERRIDE_EMPTY400Все override-поля null (INV-3)Оверрайд не содержит ни одного переопределённого поляno retry
UNAUTHORIZED401Нет/просрочен m2m client-credentials токенТребуется авторизацияno retry
FORBIDDEN403Клиент не pin-admin-system / нет catalog.overrides.manageДоступ запрещёнno retry
CATALOG_OVERRIDE_TARGET_NOT_FOUND404Корпус отсутствует в витрине (INV-2)Корпус не найден в каталогеno retry
CATALOG_OVERRIDE_NOT_FOUND404Delete-дельта: живого оверрайда нетОверрайд не найденno retry
CATALOG_OVERRIDE_TARGET_DELISTED409Проект корпуса DelistedПроект удалён источником; правка невозможнаno retry
CONCURRENCY_CONFLICT409ExpectedVersion не совпал или гонка версийДанные изменены параллельно; обновите формуretry с тем же Idempotency-Key

Совместимость и наблюдаемость

  • Новые optional override-поля — additive; удаление/переименование/смена типа — breaking (migration + rollout).
  • Logs: admin-building-override-upserted (buildingId, overrideId, created/removed, propertyObjectsRecomputed, adminUserId); metrics: catalog_admin_operations_total{operation="put-building-override"}, duration, catalog_projection_recompute_total{trigger="override"} (+ размер батча объектов недвижимости); traces: span приёма → span обработки → span обращения к хранилищу; correlation — сквозной trace через traceparent. Deep audit каждого вызова. Dashboard catalog-overview; runbook — rebuild проекций по id.

Обратные ссылки

Автоссылки: где используется этот документ.

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