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

PUT /api/admin/v1/model-config — Обновление платформенной экономической модели (admin)

Назначение

Позволяет администратору платформы полностью заместить девять коэффициентов singleton-агрегата EconomicModelConfig (таб «Глобальный дефолт»). Инкрементирует ревизию (Version, INV-MC4), штампует актора и поднимает доменное событие правки для аудит-лога. Изменение действует на все последующие расчёты серий (../public/get-property-object-investor-series.md, investor-preview.md) немедленно — результаты не кэшируются и не персистятся.

Metadata

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

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

ПроверкаЗначение
Actor typeOperator(Admin) — администратор платформы (доступ через admin API, m2m client-credentials)
Auth policy / Permissionspolicy 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-контракт

ПолеЗначение
MethodPUT
Route/api/admin/v1/model-config
Success status200
Idempotency headerне применяется — PUT идемпотентен по семантике полного замещения (повтор с тем же телом даёт то же состояние; Version при повторе инкрементируется — допустимо, значения идентичны)
Correlationсквозной trace через traceparent; X-Admin-User-Id — обязателен

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

Тело — полный набор коэффициентов (partial-обновление не поддерживается — форма админки отправляет всё). Диапазоны — INV-MC2 (дом economic-model-config.md).

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
MarketPremiumbody marketPremiumdecimalДа>= 0 && <= 5; конечное число (INV-MC3)
PriceGrowthMonthlybody priceGrowthMonthlydecimalДа> -1 && <= 1
DailyRentM2body dailyRentM2decimalДа>= 0
RentGrowthMonthlybody rentGrowthMonthlydecimalДа> -1 && <= 1
ExpenseSharebody expenseSharedecimalДа>= 0 && <= 1
AppreciationTaxMonthlybody appreciationTaxMonthlydecimalДа>= 0 && <= 1 (легаси-поле, принимается для совместимости admin UI)
EbitdaTaxMonthlybody ebitdaTaxMonthlydecimalДа>= 0 && <= 1 (легаси-поле)
Occupancybody occupancydecimalДа>= 0 && <= 1
HorizonMonthsbody horizonMonthsintДа>= 1 && <= 360
ExpectedVersionbody expectedVersionlong?Нет>= 0; если задан и не равен текущей Version → 409 (защита «двух вкладок» админки; null — без проверки)
AdminUserIdheader X-Admin-User-IdGuidДа!= Guid.Emptyaudit-stamping

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

Та же модель, что у get-model-config.md (состояние после сохранения: новые значения, version инкрементирован, updatedAt/updatedByUserId — текущая правка).

Алгоритм

  1. Контекст и доступ. Администратор платформы через admin API (policy AdminSystemOnly); scope — платформа; требуется право invest-analytics.model-config.manage; заголовок X-Admin-User-Id → аудит-контекст.
  2. Проверка входа. Все девять коэффициентов — в диапазонах INV-MC2 и конечны (INV-MC3; тип decimal
    • numeric в PG делают NaN недостижимым, guard остаётся правилом контракта) — иначе ValidationError (400). Существование строки условием не является (upsert).
  3. Что читаем. Платформенную модель по её well-known-идентификатору. Отсутствие строки допустимо: первый PUT до применения seed создаёт её.
  4. Бизнес-правила и переходы. Если ExpectedVersion задан, строка существует и её Version не совпал → конфликт INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT (409). Иначе — маппинг поле-в-поле: все девять коэффициентов ← тело запроса; Version инкрементируется (+1, или 1 при создании — INV-MC4); UpdatedByUserId ← администратор; UpdatedAt ← текущее время. При отсутствии строки создаётся новая (INV-MC1: уникальный индекс ux_model_config_singleton гарантирует ровно одну строку).
  5. Что меняется. Строка модели создаётся или обновляется. Конкурентная правка двух администраторов в окне чтения-записи → конфликт сохранения → 409 INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT (админка перечитывает GET и повторяет).
  6. Какие факты/события фиксируются. Доменное событие правки EconomicModelConfigUpdatedEvent → аудит-лог admin-правок. Интеграционное событие EconomicModelChangedKafkaEvent (топик pin.invest-analytics.model-changed.v1, ChangeScope = Global, ChangeKind = Upserted) — для инвалидации и пересчёта витринных метрик доходности (../../events/model-changed.md).
  7. Согласованность. Обновление модели, штампы ревизии/актора, запись аудита и исходящее событие model-changed фиксируются в одном согласованном изменении.
  8. Результат. Модель после сохранения: новые значения, инкрементированная ревизия, updatedAt/updatedByUserId текущей правки.

Диаграмма

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

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

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Нарушение диапазонов INV-MC2 / отсутствующие поля / заголовкиОшибка валидации запросаno retry
FORBIDDEN403Клиент не pin-admin-system / нет policyДоступ запрещёнno retry
INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT409ExpectedVersion не совпал или конфликт конкурентной правкиМодель изменена другим администратором — обновите данные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.

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

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

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