PUT /api/admin/v1/model-config — Обновление платформенной экономической модели (admin)
Назначение
Позволяет администратору платформы полностью заместить девять коэффициентов singleton-агрегата
EconomicModelConfig (таб «Глобальный дефолт»).
Инкрементирует ревизию (Version, INV-MC4), штампует актора и поднимает доменное событие правки для
аудит-лога. Изменение действует на все последующие расчёты серий
(../public/get-property-object-investor-series.md,
investor-preview.md) немедленно — результаты не кэшируются и не персистятся.
| Поле | Значение |
|---|
| Сервис/контекст | invest-analytics |
| API area | admin (ADR-0053) |
| Feature group | admin-economic-model |
| Статус | draft |
| Документ | docs/06-services/invest-analytics/api/admin/put-model-config.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Operator(Admin) — администратор платформы (доступ через admin API, m2m client-credentials) |
| Auth policy / Permissions | policy AdminSystemOnly (клиент pin-admin-system); каталожное соответствие — permission invest-analytics.model-config.manage |
| Scope (RBAC) | платформа |
| Tenant isolation | не применяется — конфигурация не tenanted (ADR-0052 п.3) |
| Audit | обязателен: аудит правок admin API — актор (X-Admin-User-Id), значения до/после, результат; штампуются UpdatedByUserId, Version; поднимается доменное событие правки |
HTTP-контракт
| Поле | Значение |
|---|
| Method | PUT |
| Route | /api/admin/v1/model-config |
| Success status | 200 |
| Idempotency header | не применяется — PUT идемпотентен по семантике полного замещения (повтор с тем же телом даёт то же состояние; Version при повторе инкрементируется — допустимо, значения идентичны) |
| Correlation | сквозной trace через traceparent; X-Admin-User-Id — обязателен |
Входные данные / параметры запроса
Тело — полный набор коэффициентов (partial-обновление не поддерживается — форма админки отправляет
всё). Диапазоны — INV-MC2 (дом economic-model-config.md).
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
MarketPremium | body marketPremium | decimal | Да | >= 0 && <= 5; конечное число (INV-MC3) | — |
PriceGrowthMonthly | body priceGrowthMonthly | decimal | Да | > -1 && <= 1 | — |
DailyRentM2 | body dailyRentM2 | decimal | Да | >= 0 | — |
RentGrowthMonthly | body rentGrowthMonthly | decimal | Да | > -1 && <= 1 | — |
ExpenseShare | body expenseShare | decimal | Да | >= 0 && <= 1 | — |
AppreciationTaxMonthly | body appreciationTaxMonthly | decimal | Да | >= 0 && <= 1 (легаси-поле, принимается для совместимости admin UI) | — |
EbitdaTaxMonthly | body ebitdaTaxMonthly | decimal | Да | >= 0 && <= 1 (легаси-поле) | — |
Occupancy | body occupancy | decimal | Да | >= 0 && <= 1 | — |
HorizonMonths | body horizonMonths | int | Да | >= 1 && <= 360 | — |
ExpectedVersion | body expectedVersion | long? | Нет | >= 0; если задан и не равен текущей Version → 409 (защита «двух вкладок» админки; null — без проверки) | — |
AdminUserId | header X-Admin-User-Id | Guid | Да | != Guid.Empty | audit-stamping |
Модель ответа ModelConfigResult
Та же модель, что у get-model-config.md (состояние после
сохранения: новые значения, version инкрементирован, updatedAt/updatedByUserId — текущая правка).
Алгоритм
- Контекст и доступ. Администратор платформы через admin API (policy
AdminSystemOnly); scope —
платформа; требуется право invest-analytics.model-config.manage; заголовок X-Admin-User-Id →
аудит-контекст.
- Проверка входа. Все девять коэффициентов — в диапазонах INV-MC2 и конечны (INV-MC3; тип
decimal
numeric в PG делают NaN недостижимым, guard остаётся правилом контракта) — иначе ValidationError
(400). Существование строки условием не является (upsert).
- Что читаем. Платформенную модель по её well-known-идентификатору. Отсутствие строки допустимо:
первый PUT до применения seed создаёт её.
- Бизнес-правила и переходы. Если
ExpectedVersion задан, строка существует и её Version не совпал
→ конфликт INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT (409). Иначе — маппинг поле-в-поле: все девять
коэффициентов ← тело запроса; Version инкрементируется (+1, или 1 при создании — INV-MC4);
UpdatedByUserId ← администратор; UpdatedAt ← текущее время. При отсутствии строки создаётся новая
(INV-MC1: уникальный индекс ux_model_config_singleton гарантирует ровно одну строку).
- Что меняется. Строка модели создаётся или обновляется. Конкурентная правка двух администраторов в
окне чтения-записи → конфликт сохранения → 409
INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT (админка
перечитывает GET и повторяет).
- Какие факты/события фиксируются. Доменное событие правки
EconomicModelConfigUpdatedEvent →
аудит-лог admin-правок. Интеграционное событие EconomicModelChangedKafkaEvent
(топик pin.invest-analytics.model-changed.v1, ChangeScope = Global, ChangeKind = Upserted) — для
инвалидации и пересчёта витринных метрик доходности (../../events/model-changed.md).
- Согласованность. Обновление модели, штампы ревизии/актора, запись аудита и исходящее событие
model-changed фиксируются в одном согласованном изменении.
- Результат. Модель после сохранения: новые значения, инкрементированная ревизия,
updatedAt/updatedByUserId текущей правки.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Интеграционные события | EconomicModelChangedKafkaEvent (pin.invest-analytics.model-changed.v1, scope Global, Upserted) — триггер пересчёта витринных метрик доходности |
| Аудит | доменное событие EconomicModelConfigUpdatedEvent → аудит-лог admin-правок |
| Кэш / проекции / поиск | none — кэша модели нет (ADR-0056), новые значения действуют со следующего расчёта |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | Нарушение диапазонов INV-MC2 / отсутствующие поля / заголовки | Ошибка валидации запроса | no retry |
FORBIDDEN | 403 | Клиент не pin-admin-system / нет policy | Доступ запрещён | no retry |
INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT | 409 | ExpectedVersion не совпал или конфликт конкурентной правки | Модель изменена другим администратором — обновите данные | retry после GET (перечитать version) |
Совместимость и наблюдаемость
- Новые optional поля тела — additive; изменение диапазонов в сторону сужения, удаление полей —
breaking (migration + rollout). Легаси-поля
appreciationTaxMonthly/ebitdaTaxMonthly принимаются
до deprecation-решения.
- Logs:
admin-model-config-updated (adminUserId, version, изменённые поля — значения в аудит-логе, не
в общий лог). Metrics: invest_analytics_admin_operations_total{operation="put-model-config"},
конфликты …_conflicts_total. Traces — сквозной trace через traceparent. Runbook — расхождение
админок: сверка version через GET.
Acceptance Criteria
- Given модель существует с
Version = n, тело содержит девять валидных коэффициентов и
expectedVersion = n, When администратор шлёт PUT, Then 200 с новыми значениями, version = n+1, updatedAt/updatedByUserId текущей правки; событие model-changed (Global, Upserted)
публикуется атомарно с сохранением.
- Given строки модели нет (первый PUT), When PUT валидного тела, Then строка создаётся с
version = 1 (INV-MC4), уникальность страхует ux_model_config_singleton (INV-MC1).
- Given
expectedVersion = n, но текущая Version = n+1 (правка «из другой вкладки»), When PUT,
Then 409 INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT; состояние не меняется, событие не публикуется.
- Given два администратора шлют PUT в окне чтения-записи, When оба сохраняют, Then проигравший
получает
409, состояние победителя зафиксировано (правка не затирается «вслепую»).
- Given любой коэффициент вне диапазона INV-MC2 или отсутствует, When PUT, Then
400 ValidationError; ни строка, ни событие не меняются.
- Given успешный PUT, When сразу выполняется расчёт серии/preview, Then используются новые
значения немедленно (кэша нет); бейджи доходности каталога подтягиваются в окне пересчёта (< 15 мин)
через цепочку
model-changed → internal recompute → property-object-metrics.v1.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы