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

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.

Metadata

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

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

ПроверкаЗначение
Actor typeOperator(Admin) — администратор платформы (доступ через admin API, m2m client-credentials)
Auth policy / Permissionspolicy AdminSystemOnly; каталожное соответствие — permission invest-analytics.property-object-types.manage
Scope (RBAC)платформа
Tenant isolationне применяется — платформенная настройка (ADR-0052 п.3)
Auditобязателен: аудит правок admin API — актор, groupId, набор до/после; штампуются Version/UpdatedByUserId родителя; поднимается доменное событие

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/admin/v1/property-object-types/{groupId}/month-overrides
Success status200
Idempotency headerне применяется — PUT идемпотентен по семантике полного замещения набора
Correlationсквозной trace через traceparent; X-Admin-User-Id — обязателен

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
GroupIdroutelongДа> 0; существование настройки типа — шаг проверки (404, INV-MO2)
ExpectedVersionbody expectedVersionlong?Нетесли задан и != parent.Version → 409 (по ревизии родителя); null — без проверки
Itemsbody itemsIReadOnlyList<MonthOverrideInputModel>Да (может быть пуст)≤ 600 строк (50 лет × 12); INV-MO4 (непустая строка), INV-MO5 (нет дублей (year, month))replace-all: полный набор
AdminUserIdheader X-Admin-User-IdGuidДа!= Guid.Emptyaudit-stamping родителя

MonthOverrideInputModel

Одна строка запроса = один календарный месяц. Диапазоны — INV-MO3; семантика/единицы полей — дом property-object-type-month-override.md и economic-model-config.md; null = «поле месяца не оверрайдится».

ПолеТипОбязательностьВалидация (INV-MO3)Описание
yearintДа2000..2100Календарный год месяца-оверрайда.
monthintДа1..12Календарный месяц.
marketPriceM2decimal?Нет> 0 при not nullАбсолютная рыночная цена м², руб.
priceGrowthMonthlydecimal?Нет> -1 && <= 1Темп роста цены в месяце.
dailyRentM2decimal?Нет> 0 при not nullАбсолютная аренда руб/м²/сутки.
rentGrowthMonthlydecimal?Нет> -1 && <= 1Темп роста аренды.
occupancydecimal?Нет0..1Заполняемость месяца.
expenseSharedecimal?Нет0..1Доля расходов месяца.
ebitdadecimal?Нет> 0 при not nullАбсолютный накопленный EBITDA/м².
appreciationTaxMonthlydecimal?Нет0..1/12 (годовая = ×12)Месячная ставка налога на прирост.
ebitdaTaxMonthlydecimal?Нет0..1/12 (годовая = ×12)Месячная ставка налога на EBITDA.

INV-MO4: каждая строка обязана содержать хотя бы одно not null-поле оверрайда (полностью пустой месяц отклоняется). INV-MO5: в запросе нет двух строк с одинаковой парой (year, month).

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

Дом модели — парная get-month-overrides.md (та же MonthOverridesResult + MonthOverrideItemModel, проекция полей дома один-в-один, сортировка year ASC, month ASC). Дельта записи: propertyObjectTypeVersionPropertyObjectTypeConfig.Version после touch (значение для expectedVersion следующего PUT).

ПолеТипОбязательностьИсточникОписание
groupIdlongДаecho routeТип объекта недвижимости.
propertyObjectTypeVersionlongДаPropertyObjectTypeConfig.Version (после touch)Ревизия родителя для expectedVersion следующего PUT.
itemsIReadOnlyList<MonthOverrideItemModel>Да (может быть пуст)сохранённые childrenСохранённый набор, сортировка year/month ASC.

Алгоритм

  1. Контекст и доступ. Администратор платформы через admin API (policy AdminSystemOnly); scope — платформа; право invest-analytics.property-object-types.manage; заголовок X-Admin-User-Id → аудит.
  2. Проверка входа. Диапазоны полей строк (INV-MO3), непустая строка (INV-MO4), отсутствие дублей (year, month) (INV-MO5), размер Items ≤ 600, GroupId > 0, X-Admin-User-Id непустой — иначе ValidationError (400).
  3. Предусловия и что читаем. Настройка типа с GroupId должна существовать (INV-MO2) — иначе INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND (404). Если ExpectedVersion задан и не равен ревизии родителя → INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT (409). Считывается число строк до замены (countBefore) — для события.
  4. Бизнес-правила и маппинг. Каждая строка запроса → 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.
  5. Что меняется (порядок, INV-MO6): удалить все оверрайды типа → вставить новый набор → обновить родителя (touch). Composite PK (GroupId, Year, Month) — последний рубеж дублей (INV-MO1); конкурентный replace-all конфликтует по ревизии родителя → 409.
  6. Какие факты/события фиксируются. Доменное событие PropertyObjectTypeMonthOverridesReplacedEvent → аудит-лог admin-правок. Интеграционное событие EconomicModelChangedKafkaEvent (топик pin.invest-analytics.model-changed.v1, ChangeScope = PropertyObjectType, GroupId, ChangeKind = MonthOverridesReplaced) — для пересчёта метрик доходности объектов этого типа (../../events/model-changed.md).
  7. Согласованность. Удаление, вставка нового набора, touch родителя и исходящее событие фиксируются в одном согласованном изменении (атомарная замена; промежуточное состояние «месяцы удалены, новые не вставлены» снаружи невидимо).
  8. Результат. Сохранённый набор + ревизия родителя после touch. Пустой Items на входе — валидная очистка всех месяцев (countAfter = 0; расчёт остаётся на уровнях тип/платформа).

Диаграмма

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

ТипДетали
Интеграционные событияEconomicModelChangedKafkaEvent (pin.invest-analytics.model-changed.v1, scope PropertyObjectType, MonthOverridesReplaced) — триггер пересчёта метрик
Аудитдоменное событие PropertyObjectTypeMonthOverridesReplacedEvent → аудит-лог admin-правок
Кэш / проекции / поискnone — новые оверрайды действуют со следующего расчёта серии

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400INV-MO3/MO4/MO5, payload > 600 строк, невалидные заголовкиОшибка валидации запросаno retry
FORBIDDEN403Клиент не pin-admin-system / нет policyДоступ запрещёнno retry
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND404Настройка типа не создана (INV-MO2)Настройка типа не найдена — сначала создайте типno retry
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT409ExpectedVersion не совпал / конфликт конкурентной правки родителяНастройка изменена другим администратором — обновите данные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 мин).

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

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

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