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

PUT /api/admin/v1/lots/{propertyObjectId}/override — Создание/изменение оверрайда объекта недвижимости (admin)

Назначение

Полный upsert оверрайда отображения объекта недвижимости (PropertyObjectDisplayOverride, display-override.md): администратор платформы перекрывает витринные поля объекта недвижимости — видимость, цену, заголовок, бейдж, надёжность, окупаемость, рейтинг — не трогая source of truth в chessboard. Семантика PUT — полное состояние оверрайда: поле со значением = переопределено, поле null = не переопределено; каждое переданное состояние целиком заменяет прежнее. Эффективные поля карточки пересчитываются в той же транзакции (INV-6 display-override).

Снятие оверрайда — симметричная операция DELETE /api/admin/v1/lots/{propertyObjectId}/override (отдельный документ не заводится, дельта описана здесь): soft-delete записи (DeletedAt/DeletedByUserId), пересчёт эффективных полей карточки из SourceSnapshot (тот же поток пересчёта с «оверрайд отсутствует»), ответ 200 с PropertyObjectOverrideMutationResult (OverrideId снятого, Removed = true); отсутствие живого оверрайда → 404 CATALOG_OVERRIDE_NOT_FOUND.

Metadata

ПолеЗначение
Сервис/контекстcatalog
API areaadmin (ADR-0053)
Feature groupadmin-overrides
Статусdraft
Документdocs/06-services/catalog/api/admin/put-property-object-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), propertyObjectId, диф полей, Comment, результат; actor-stamping CreatedByUserId/UpdatedByUserId

HTTP-контракт

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

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

Все override-поля — правило «null = не переопределено»; типы/ограничения — дом display-override.md.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
PropertyObjectIdrouteGuidДа!= Guid.Empty; существование карточки — шаг 2 (INV-2 TargetKnown)
IsVisiblebodybool?Нет
Pricebodydecimal?Нет> 0 при заполнении (INV-5)
Titlebodystring?Нет1..512 символов после trim
Badgebodystring?Нет≤ 64 символов
ReliabilityValuebodyshort?Нет0..ReliabilityMax; пара с ReliabilityMax — оба null или оба заданы (INV-4)
ReliabilityMaxbodyshort?Нет1..100
PaybackValuebodydecimal?Нет> 0; пара с PaybackPeriod (INV-4)
PaybackPeriodbodystring? (enum PaybackPeriod)Нет∈ {Years, Months}
Ratingbodydecimal?Нет0.0..10.0
Commentbodystring?Нет≤ 1024 символовinternal — только admin-область и audit
ExpectedVersionbodylong?Нет>= 1; optimistic-check против PropertyObjectDisplayOverride.Version (защита «два админа»)
AdminUserIdheader X-Admin-User-IdGuidДа!= Guid.Emptyaudit-stamping
IdempotencyKeyheader Idempotency-KeyGuidДа!= Guid.Empty

Кросс-полевая валидация: хотя бы одно override-поле (IsVisibleRating) не null — INV-3 AtLeastOneField → CATALOG_OVERRIDE_EMPTY (пустой PUT не является способом снятия; снятие — DELETE).

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

ПолеТипОбязательностьИсточникMaskingОписание
OverrideIdGuidДаPropertyObjectDisplayOverride.IdИдентификатор оверрайда (созданного или обновлённого).
PropertyObjectIdGuidДаPropertyObjectDisplayOverride.PropertyObjectIdОбъект недвижимости.
CreatedboolДавычислениеtrue — создан новый; false — обновлён существующий.
RemovedboolДавычислениеtrue только в delete-дельте.
VersionlongДаPropertyObjectDisplayOverride.VersionВерсия после мутации.
EffectivePropertyObjectOverrideEffectiveBlockДаCatalogPropertyObjectCard после пересчётаЭффективные значения витрины (модель — get-property-object-override.md).
CardStatusstring (enum PropertyObjectCardStatus)ДаCatalogPropertyObjectCard.Status после пересчётаИтоговая видимость (IsVisible=falseHidden).

Алгоритм

  1. Контекст и доступ. Актор — администратор платформы (admin API, m2m pin-admin-system, policy AdminSystemOnly); заголовки X-Admin-User-Id, Idempotency-Key. Повтор по ключу admin:property-object-override:{IdempotencyKey} → сохранённый PropertyObjectOverrideMutationResult (200).
  2. Проверка входа. Диапазоны/длины полей; парность INV-4 (ReliabilityValue ⇔ ReliabilityMax, PaybackValue ⇔ PaybackPeriod), нарушение → CATALOG_OVERRIDE_INCOMPLETE_PAIR; Price > 0 (INV-5), иначе CATALOG_OVERRIDE_INVALID_PRICE; хотя бы одно override-поле не null (INV-3 AtLeastOneField), иначе CATALOG_OVERRIDE_EMPTY; прочие нарушения формата → ValidationError. Существование цели (INV-2 TargetKnown): карточка объекта недвижимости есть; нет → CATALOG_OVERRIDE_TARGET_NOT_FOUND (404). Состояние цели: card.Status ∈ {Published, Hidden}; DelistedCATALOG_OVERRIDE_TARGET_DELISTED (409) — правка удалённого источником объекта бессмысленна. Optimistic-check: при заданном ExpectedVersion и существующем оверрайде несовпадение версий → CONCURRENCY_CONFLICT (409).
  3. Что читаем. Живой оверрайд объекта недвижимости (есть → update, нет → create; единственность — INV-1 SingleLiveOverride + unique partial index ux_property_object_overrides_property_object_id), оверрайд корпуса объекта (нужен для полного пересчёта эффективных полей) и карточку проекта объекта (для пересчёта агрегатов INV-2 project-card).
  4. Правила и переходы. Create: новый оверрайд, маппинг поле-в-поле body → одноимённые поля сущности, CreatedByUserId = AdminUserId, Version = 1. Update: полная замена всех override-полей значениями body (включая затирание в null), UpdatedByUserId = AdminUserId, Version++. В обоих случаях фиксируется доменное событие PropertyObjectDisplayOverrideChangedEvent (Id, PropertyObjectId, набор изменённых полей). Пересчёт проекции (INV-6, INV-3 property-object-card): для каждого оверрайдимого поля Effective = Override(поле) ?? SourceSnapshot(поле); PricePerMeter = round(Price / AreaTotal, 2); пересчёт видимости (INV-1 property-object-card: IsVisible=falseStatus = Hidden; возврат в Published — при выполнении всех условий INV-1). Фиксируется PropertyObjectCardRecomputedEvent (Id, ProjectId, Price, Status, PropertyObjectStatus), который в том же коммите пересчитывает агрегаты карточки проекта (MinPrice/MaxPrice/PropertyObjectsTotal/PropertyObjectsAvailable/Payback, INV-2 project-card).
  5. Что меняется. Создаётся/обновляется оверрайд, обновляются карточка объекта недвижимости и (если агрегаты изменились) карточка проекта. Порядок: оверрайд → карточка объекта недвижимости → карточка проекта. Правка не должна затирать чужую (конкурентный доступ); конфликт с конкурирующим консюмером → retry администратором платформы с тем же Idempotency-Key.
  6. События. Доменные PropertyObjectDisplayOverrideChangedEvent, PropertyObjectCardRecomputedEvent, ProjectCardRecomputedEvent — фиксируются в том же коммите. Интеграционные события не публикуются (оверрайд — внутренняя механика витрины, обоснование — display-override.md, раздел «События»).
  7. Согласованность. Оверрайд + пересчитанные карточки + отметка идемпотентности + audit — в одном согласованном изменении (INV-6 RecomputeInTx; один контекст, одна БД — simplicity-first ADR-0056).
  8. Результат. PropertyObjectOverrideMutationResult (200; Effective/CardStatus — из пересчитанной карточки, консистентно транзакции); отказ → ProblemDetails с кодами таблицы ниже.
Приоритет видимости

IsVisible=true в оверрайде объекта не делает объект видимым, если его корпус скрыт (BuildingOverride.IsVisible=false) или проект в Hidden/Delisted — корпусной и проектный уровни строже (INV-1 property-object-card); возврат в Published происходит только при одновременном выполнении всех условий INV-1 (источник PropertyObjectStatus != Hidden, оверрайды объекта и корпуса не запрещают показ, проект Published).

Диаграмма

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

ТипДетали
Проекции / поискпересчёт catalog.property_object_cards (эффективные поля, Status, NameVector — generated column пересчитается по Title) и агрегатов catalog.project_cards в той же транзакции

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

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Диапазоны/длины/enum/заголовкиОшибка валидации запросаno retry
CATALOG_OVERRIDE_EMPTY400Все override-поля null (INV-3)Оверрайд не содержит ни одного переопределённого поляno retry
CATALOG_OVERRIDE_INCOMPLETE_PAIR400Нарушена парность Reliability/Payback (INV-4)Парные поля должны задаваться вместеno retry
CATALOG_OVERRIDE_INVALID_PRICE400Price <= 0 (INV-5)Цена оверрайда должна быть больше нуля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_DELISTED409card.Status = DelistedОбъект недвижимости удалён источником; правка невозможнаno retry
CONCURRENCY_CONFLICT409ExpectedVersion не совпал или гонка версий с консюмером/вторым админомДанные изменены параллельно; обновите формуretry с тем же Idempotency-Key после перечитывания

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

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

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

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

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