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 area | admin (ADR-0053) |
| Feature group | admin-overrides |
| Статус | draft |
| Документ | docs/06-services/catalog/api/admin/put-property-object-override.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | ExternalSystem (администратор платформы через admin API; m2m client-credentials — ADR-0053) |
| Auth policy / Permissions | policy 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-контракт
| Поле | Значение |
|---|---|
| Method | PUT (delete-дельта: DELETE, тот же route) |
| Route | /api/admin/v1/lots/{propertyObjectId}/override |
| Success status | 200 |
| Idempotency header | Idempotency-Key (Guid; обязателен — администратор платформы ретраит по таймауту) |
| Correlation | сквозной trace через traceparent; X-Admin-User-Id (Guid админа) — обязателен |
Входные данные / параметры запроса
Все override-поля — правило «null = не переопределено»; типы/ограничения — дом
display-override.md.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
PropertyObjectId | route | Guid | Да | != Guid.Empty; существование карточки — шаг 2 (INV-2 TargetKnown) | — |
IsVisible | body | bool? | Нет | — | — |
Price | body | decimal? | Нет | > 0 при заполнении (INV-5) | — |
Title | body | string? | Нет | 1..512 символов после trim | — |
Badge | body | string? | Нет | ≤ 64 символов | — |
ReliabilityValue | body | short? | Нет | 0..ReliabilityMax; пара с ReliabilityMax — оба null или оба заданы (INV-4) | — |
ReliabilityMax | body | short? | Нет | 1..100 | — |
PaybackValue | body | decimal? | Нет | > 0; пара с PaybackPeriod (INV-4) | — |
PaybackPeriod | body | string? (enum PaybackPeriod) | Нет | ∈ {Years, Months} | — |
Rating | body | decimal? | Нет | 0.0..10.0 | — |
Comment | body | string? | Нет | ≤ 1024 символов | internal — только admin-область и audit |
ExpectedVersion | body | long? | Нет | >= 1; optimistic-check против PropertyObjectDisplayOverride.Version (защита «два админа») | — |
AdminUserId | header X-Admin-User-Id | Guid | Да | != Guid.Empty | audit-stamping |
IdempotencyKey | header Idempotency-Key | Guid | Да | != Guid.Empty | — |
Кросс-полевая валидация: хотя бы одно override-поле (IsVisible…Rating) не null —
INV-3 AtLeastOneField → CATALOG_OVERRIDE_EMPTY (пустой PUT не является способом снятия; снятие —
DELETE).
Модель ответа PropertyObjectOverrideMutationResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
OverrideId | Guid | Да | PropertyObjectDisplayOverride.Id | — | Идентификатор оверрайда (созданного или обновлённого). |
PropertyObjectId | Guid | Да | PropertyObjectDisplayOverride.PropertyObjectId | — | Объект недвижимости. |
Created | bool | Да | вычисление | — | true — создан новый; false — обновлён существующий. |
Removed | bool | Да | вычисление | — | true только в delete-дельте. |
Version | long | Да | PropertyObjectDisplayOverride.Version | — | Версия после мутации. |
Effective | PropertyObjectOverrideEffectiveBlock | Да | CatalogPropertyObjectCard после пересчёта | — | Эффективные значения витрины (модель — get-property-object-override.md). |
CardStatus | string (enum PropertyObjectCardStatus) | Да | CatalogPropertyObjectCard.Status после пересчёта | — | Итоговая видимость (IsVisible=false → Hidden). |
Алгоритм
- Контекст и доступ. Актор — администратор платформы (admin API, m2m
pin-admin-system, policyAdminSystemOnly); заголовкиX-Admin-User-Id,Idempotency-Key. Повтор по ключуadmin:property-object-override:{IdempotencyKey}→ сохранённыйPropertyObjectOverrideMutationResult(200). - Проверка входа. Диапазоны/длины полей; парность 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};Delisted→CATALOG_OVERRIDE_TARGET_DELISTED(409) — правка удалённого источником объекта бессмысленна. Optimistic-check: при заданномExpectedVersionи существующем оверрайде несовпадение версий →CONCURRENCY_CONFLICT(409). - Что читаем. Живой оверрайд объекта недвижимости (есть → update, нет → create; единственность —
INV-1 SingleLiveOverride + unique partial index
ux_property_object_overrides_property_object_id), оверрайд корпуса объекта (нужен для полного пересчёта эффективных полей) и карточку проекта объекта (для пересчёта агрегатов INV-2 project-card). - Правила и переходы. 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=false→Status = Hidden; возврат вPublished— при выполнении всех условий INV-1). ФиксируетсяPropertyObjectCardRecomputedEvent(Id,ProjectId,Price,Status,PropertyObjectStatus), который в том же коммите пересчитывает агрегаты карточки проекта (MinPrice/MaxPrice/PropertyObjectsTotal/PropertyObjectsAvailable/Payback, INV-2 project-card). - Что меняется. Создаётся/обновляется оверрайд, обновляются карточка объекта недвижимости и (если
агрегаты изменились) карточка проекта. Порядок: оверрайд → карточка объекта недвижимости → карточка
проекта. Правка не должна затирать чужую (конкурентный доступ); конфликт с конкурирующим консюмером →
retry администратором платформы с тем же
Idempotency-Key. - События. Доменные
PropertyObjectDisplayOverrideChangedEvent,PropertyObjectCardRecomputedEvent,ProjectCardRecomputedEvent— фиксируются в том же коммите. Интеграционные события не публикуются (оверрайд — внутренняя механика витрины, обоснование — display-override.md, раздел «События»). - Согласованность. Оверрайд + пересчитанные карточки + отметка идемпотентности + audit — в одном согласованном изменении (INV-6 RecomputeInTx; один контекст, одна БД — simplicity-first ADR-0056).
- Результат.
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 |
|---|---|---|---|---|
ValidationError | 400 | Диапазоны/длины/enum/заголовки | Ошибка валидации запроса | no retry |
CATALOG_OVERRIDE_EMPTY | 400 | Все override-поля null (INV-3) | Оверрайд не содержит ни одного переопределённого поля | no retry |
CATALOG_OVERRIDE_INCOMPLETE_PAIR | 400 | Нарушена парность Reliability/Payback (INV-4) | Парные поля должны задаваться вместе | no retry |
CATALOG_OVERRIDE_INVALID_PRICE | 400 | Price <= 0 (INV-5) | Цена оверрайда должна быть больше нуля | no retry |
UNAUTHORIZED | 401 | Нет/просрочен m2m client-credentials токен | Требуется авторизация | no retry |
FORBIDDEN | 403 | Клиент не pin-admin-system / нет catalog.overrides.manage | Доступ запрещён | no retry |
CATALOG_OVERRIDE_TARGET_NOT_FOUND | 404 | Карточка объекта недвижимости отсутствует (INV-2) | Объект недвижимости не найден в каталоге | no retry |
CATALOG_OVERRIDE_NOT_FOUND | 404 | Delete-дельта: живого оверрайда нет | Оверрайд не найден | no retry |
CATALOG_OVERRIDE_TARGET_DELISTED | 409 | card.Status = Delisted | Объект недвижимости удалён источником; правка невозможна | no retry |
CONCURRENCY_CONFLICT | 409 | ExpectedVersion не совпал или гонка версий с консюмером/вторым админом | Данные изменены параллельно; обновите форму | 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 при расхождении.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (4): Catalog Admin API — реестр методов и общие правила области, GET /api/admin/v1/lots/{propertyObjectId}/override — Просмотр оверрайда объекта недвижимости (admin), POST /api/admin/v1/projections/rebuild — Принудительный пересчёт карточек по id (admin), PUT /api/admin/v1/buildings/{buildingId}/override — Создание/изменение оверрайда корпуса (admin)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: display-override.md (дом полей + INV-1..6), catalog-property-object-card.md (INV-1/INV-3 — видимость и эффективные поля), catalog-project-card.md (INV-2 — агрегаты).
- Чтение: get-property-object-override.md; корпус: put-building-override.md.
- ADR-0053 (m2m + deep audit), ADR-0056 (пересчёт в транзакции).