PUT /api/admin/v1/property-object-types/{groupId}/month-overrides — Replace-all помесячных оверрайдов типа объекта недвижимости (admin)
Назначение
Позволяет администратору платформы полностью заменить (replace-all) набор помесячных оверрайдов
PropertyObjectTypeMonthOverride типа объекта
недвижимости GroupId (таб «помесячные оверрайды»). Оверрайды — child-сущности агрегата
PropertyObjectTypeConfig: сохраняются только через
родителя, поэтому админка присылает полный список месяцев типа (отсутствие строки в запросе =
удаление месяца). Парная операция чтения — get-month-overrides.md; сценарий —
UC-PRC-004.
| Поле | Значение |
|---|
| Сервис/контекст | invest-analytics |
| API area | admin (ADR-0053) |
| Feature group | admin-property-object-types |
| Статус | draft |
| Документ | docs/06-services/invest-analytics/api/admin/put-month-overrides.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Operator(Admin) — администратор платформы (доступ через admin API, m2m client-credentials) |
| Auth policy / Permissions | policy AdminSystemOnly; каталожное соответствие — permission invest-analytics.property-object-types.manage |
| Scope (RBAC) | платформа |
| Tenant isolation | не применяется — платформенная настройка (ADR-0052 п.3) |
| Audit | обязателен: аудит правок admin API — актор, groupId, набор до/после; штампуются Version/UpdatedByUserId родителя; поднимается доменное событие |
HTTP-контракт
| Поле | Значение |
|---|
| Method | PUT |
| Route | /api/admin/v1/property-object-types/{groupId}/month-overrides |
| Success status | 200 |
| Idempotency header | не применяется — PUT идемпотентен по семантике полного замещения набора |
| Correlation | сквозной trace через traceparent; X-Admin-User-Id — обязателен |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
GroupId | route | long | Да | > 0; существование настройки типа — шаг проверки (404, INV-MO2) | — |
ExpectedVersion | body expectedVersion | long? | Нет | если задан и != parent.Version → 409 (по ревизии родителя); null — без проверки | — |
Items | body items | IReadOnlyList<MonthOverrideInputModel> | Да (может быть пуст) | ≤ 600 строк (50 лет × 12); INV-MO4 (непустая строка), INV-MO5 (нет дублей (year, month)) | replace-all: полный набор |
AdminUserId | header X-Admin-User-Id | Guid | Да | != Guid.Empty | audit-stamping родителя |
Одна строка запроса = один календарный месяц. Диапазоны — INV-MO3; семантика/единицы полей — дом
property-object-type-month-override.md и
economic-model-config.md; null = «поле месяца не оверрайдится».
| Поле | Тип | Обязательность | Валидация (INV-MO3) | Описание |
|---|
year | int | Да | 2000..2100 | Календарный год месяца-оверрайда. |
month | int | Да | 1..12 | Календарный месяц. |
marketPriceM2 | decimal? | Нет | > 0 при not null | Абсолютная рыночная цена м², руб. |
priceGrowthMonthly | decimal? | Нет | > -1 && <= 1 | Темп роста цены в месяце. |
dailyRentM2 | decimal? | Нет | > 0 при not null | Абсолютная аренда руб/м²/сутки. |
rentGrowthMonthly | decimal? | Нет | > -1 && <= 1 | Темп роста аренды. |
occupancy | decimal? | Нет | 0..1 | Заполняемость месяца. |
expenseShare | decimal? | Нет | 0..1 | Доля расходов месяца. |
ebitda | decimal? | Нет | > 0 при not null | Абсолютный накопленный EBITDA/м². |
appreciationTaxMonthly | decimal? | Нет | 0..1/12 (годовая = ×12) | Месячная ставка налога на прирост. |
ebitdaTaxMonthly | decimal? | Нет | 0..1/12 (годовая = ×12) | Месячная ставка налога на EBITDA. |
INV-MO4: каждая строка обязана содержать хотя бы одно not null-поле оверрайда (полностью пустой месяц
отклоняется). INV-MO5: в запросе нет двух строк с одинаковой парой (year, month).
Модель ответа MonthOverridesResult
Дом модели — парная get-month-overrides.md (та же
MonthOverridesResult + MonthOverrideItemModel, проекция полей дома один-в-один, сортировка year
ASC, month ASC). Дельта записи: propertyObjectTypeVersion — PropertyObjectTypeConfig.Version
после touch (значение для expectedVersion следующего PUT).
| Поле | Тип | Обязательность | Источник | Описание |
|---|
groupId | long | Да | echo route | Тип объекта недвижимости. |
propertyObjectTypeVersion | long | Да | PropertyObjectTypeConfig.Version (после touch) | Ревизия родителя для expectedVersion следующего PUT. |
items | IReadOnlyList<MonthOverrideItemModel> | Да (может быть пуст) | сохранённые children | Сохранённый набор, сортировка year/month ASC. |
Алгоритм
- Контекст и доступ. Администратор платформы через admin API (policy
AdminSystemOnly); scope —
платформа; право invest-analytics.property-object-types.manage; заголовок X-Admin-User-Id → аудит.
- Проверка входа. Диапазоны полей строк (INV-MO3), непустая строка (INV-MO4), отсутствие дублей
(year, month) (INV-MO5), размер Items ≤ 600, GroupId > 0, X-Admin-User-Id непустой — иначе
ValidationError (400).
- Предусловия и что читаем. Настройка типа с
GroupId должна существовать (INV-MO2) — иначе
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND (404). Если ExpectedVersion задан и не равен ревизии
родителя → INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT (409). Считывается число строк до замены
(countBefore) — для события.
- Бизнес-правила и маппинг. Каждая строка запроса →
PropertyObjectTypeMonthOverride (поле-в-поле,
без вычислений): GroupId, Year, Month, MarketPriceM2, PriceGrowthMonthly, DailyRentM2,
RentGrowthMonthly, Occupancy, ExpenseShare, Ebitda, AppreciationTaxMonthly, EbitdaTaxMonthly.
Touch родителя: Version + 1, UpdatedByUserId ← администратор, UpdatedAt ← текущее время. Родитель
поднимает доменное событие PropertyObjectTypeMonthOverridesReplacedEvent(GroupId, countBefore, countAfter, UpdatedByUserId), где countAfter = Items.Count.
- Что меняется (порядок, INV-MO6): удалить все оверрайды типа → вставить новый набор → обновить
родителя (touch). Composite PK (
GroupId, Year, Month) — последний рубеж дублей (INV-MO1);
конкурентный replace-all конфликтует по ревизии родителя → 409.
- Какие факты/события фиксируются. Доменное событие
PropertyObjectTypeMonthOverridesReplacedEvent →
аудит-лог admin-правок. Интеграционное событие EconomicModelChangedKafkaEvent
(топик pin.invest-analytics.model-changed.v1, ChangeScope = PropertyObjectType, GroupId,
ChangeKind = MonthOverridesReplaced) — для пересчёта метрик доходности объектов этого типа
(../../events/model-changed.md).
- Согласованность. Удаление, вставка нового набора, touch родителя и исходящее событие фиксируются в
одном согласованном изменении (атомарная замена; промежуточное состояние «месяцы удалены, новые не
вставлены» снаружи невидимо).
- Результат. Сохранённый набор + ревизия родителя после touch. Пустой
Items на входе — валидная
очистка всех месяцев (countAfter = 0; расчёт остаётся на уровнях тип/платформа).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Интеграционные события | EconomicModelChangedKafkaEvent (pin.invest-analytics.model-changed.v1, scope PropertyObjectType, MonthOverridesReplaced) — триггер пересчёта метрик |
| Аудит | доменное событие PropertyObjectTypeMonthOverridesReplacedEvent → аудит-лог admin-правок |
| Кэш / проекции / поиск | none — новые оверрайды действуют со следующего расчёта серии |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | INV-MO3/MO4/MO5, payload > 600 строк, невалидные заголовки | Ошибка валидации запроса | no retry |
FORBIDDEN | 403 | Клиент не pin-admin-system / нет policy | Доступ запрещён | no retry |
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND | 404 | Настройка типа не создана (INV-MO2) | Настройка типа не найдена — сначала создайте тип | no retry |
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT | 409 | ExpectedVersion не совпал / конфликт конкурентной правки родителя | Настройка изменена другим администратором — обновите данные | retry после GET |
Совместимость и наблюдаемость
- Additive-поля item-модели — без breaking; удаление/переименование/смена типа — breaking (синхронно
с домом сущности и парной GET). Replace-all — контракт: клиент обязан присылать полный набор
(отсутствие строки = удаление месяца), менять семантику нельзя без версии.
- Logs:
admin-month-overrides-replaced (adminUserId, groupId, countBefore, countAfter).
Metrics: invest_analytics_admin_operations_total{operation="put-month-overrides"}, конфликты.
Traces — сквозной trace через traceparent.
Acceptance Criteria
- Given тип
GroupId настроен, тело содержит 3 валидных месяца и expectedVersion совпадает, When
администратор шлёт PUT, Then 200 с сохранённым набором (3, сортировка year/month ASC),
propertyObjectTypeVersion = ревизия родителя после touch (+1); событие model-changed
(PropertyObjectType, MonthOverridesReplaced, GroupId) публикуется атомарно.
- Given
items = [] (пустой набор), When PUT, Then 200, countAfter = 0 — все месяцы типа
очищены; расчёт откатывается на уровни тип/платформа.
- Given в теле две строки с одинаковой парой
(year, month) (INV-MO5) или полностью пустая строка
(INV-MO4) или значение вне INV-MO3 или Items > 600, When PUT, Then 400 ValidationError; ничего не сохраняется.
- Given настройки типа с
GroupId нет (INV-MO2), When PUT, Then 404 INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND (сначала создайте тип).
- Given
expectedVersion не равен ревизии родителя или конкурентный replace-all, When PUT,
Then 409 INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT.
- Given идёт replace-all (удаление всех строк → вставка нового набора → touch родителя, INV-MO6),
When параллельный GET, Then промежуточное состояние «месяцы удалены, новые не вставлены» снаружи
невидимо (атомарная замена).
- Given успешный replace-all, When дальше считаются серии объектов этого типа, Then новые
оверрайды действуют со следующего расчёта; бейджи каталога затронутых объектов — в окне пересчёта (< 15 мин).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы