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

UC-PRC-003. Admin: настройка экономической модели платформы + investor-preview

Назначение

Администратор платформы управляет платформенным уровнем экономической модели доходности (таб «Глобальный дефолт»): смотрит GET model-config, сохраняет PUT model-config (singleton EconomicModelConfig) и проверяет эффект POST investor-preview — расчёт серий по произвольным параметрам до/после сохранения. Growth-chart рисует серии gross-profit/ebitda/price-per-meter из того же ответа. Контекст invest-analytics.

Metadata

ПолеЗначение
IDUC-PRC-003
Контекстinvest-analytics
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАдминистратор платформы (ADR-0053: отдельная административная система с собственными учётками; доступ к admin API контекста — m2m-авторизация + actor-id админа для аудита)
Permissions / scopeinvest-analytics.model-config.view (GET), invest-analytics.model-config.manage (PUT); preview — invest-analytics.model-config.view (read-only расчёт); scope — платформа (каталог admin-permissions — docs/06-services/identity/domain/permissions.md)
Tenantне применяется — платформенные данные (ADR-0052 п.3), не tenanted
Record-levelне применяется — singleton-конфигурация

Предусловие

  • Административная система аутентифицировала админа; m2m-канал к admin API invest-analytics настроен (ADR-0053).
  • Мигратор применил схему invest_analytics; seed-миграция создала singleton-строку model_config с well-known Id = 00000000-0000-0000-0000-000000000001 и дефолтами кода.

Триггер

ПолеЗначение
ИсточникUI button / API call
Триггерящий элементТаб «Глобальный дефолт»: загрузка таба (GET), кнопка «Сохранить» (PUT), кнопка «Предпросмотр» / growth-chart (POST investor-preview)

Основной Поток

  1. Админ открывает таб «Глобальный дефолт»; фронт вызывает GET model-config (../api/admin/get-model-config.md).
  2. GET (read-only): actor = админ, permission invest-analytics.model-config.view; система читает singleton по well-known Id и возвращает 9 полей модели + version, updatedAt, updatedByUserId, rowVersion (для конкурентного PUT); при отсутствии строки — ответ из дефолтов кода с признаком isSeedDefaults=true (штатно недостижимо).
  3. Админ правит значения (например, PriceGrowthMonthly 0.02 → 0.025, Occupancy 0.6 → 0.7) и нажимает «Сохранить»; фронт вызывает PUT model-config с телом из 9 полей + rowVersion (../api/admin/put-model-config.md).
  4. Система обрабатывает PUT:
    • Проверка входа (зеркалит check-constraints, INV-MC2/MC3): MarketPremium ∈ [0..5]; PriceGrowthMonthly ∈ (−1..1]; DailyRentM2 >= 0; RentGrowthMonthly ∈ (−1..1]; ExpenseShare ∈ [0..1]; AppreciationTaxMonthly ∈ [0..1], EbitdaTaxMonthly ∈ [0..1] (легаси-поля — валидируются и сохраняются, в расчёте не участвуют); Occupancy ∈ [0..1]; HorizonMonths ∈ [1..360]; все значения — конечные числа; RowVersion обязателен для конкурентного контроля.
    • Загрузка: singleton по well-known Id; отсутствует → ветка создания через upsert по well-known Id (INV-MC1: единственность гарантирует unique-индекс ux_model_config_singleton).
    • Маппинг поле-в-поле: каждое из 9 полей ← одноимённое поле тела (полная замена, частичного PATCH нет); Version ← Version + 1 (INV-MC4); UpdatedByUserId ← actor; UpdatedAt ← now().
    • Конкурентность: правка другим админом (устаревший RowVersion) → 409 INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT (A1).
    • События: доменное EconomicModelConfigUpdatedEvent (in-proc: audit-лог admin-правок) + интеграционное EconomicModelChangedKafkaEvent (pin.invest-analytics.model-changed.v1, ChangeScope = Global) через outbox — триггер пересчёта витринных метрик доходности (../events/model-changed.md). Обновление строки + stamping + outbox-запись — в одном согласованном изменении.
    • Результат: сохранённые 9 полей + новые version, updatedAt, rowVersion; фронт обновляет форму (показывает «дату последнего изменения модели»).
  5. Админ проверяет эффект: кнопка «Предпросмотр» / growth-chart вызывает POST investor-preview с произвольными параметрами (../api/admin/investor-preview.md): тело — basePrice, area, pricePerMeter, опциональный полный набор из 9 коэффициентов (не переданные — разрешаются из сохранённой модели по цепочке уровней), список помесячных оверрайдов monthOverrides[] (та же схема, что ../domain/property-object-type-month-override.md), entity, tax.
  6. Preview — тот же расчётный движок, что UC-PRC-002, с отличиями входа:
    • валидация: basePrice >= 0, area > 0, pricePerMeter > 0; коэффициенты — диапазоны INV-MC2; оверрайды — INV-MO3/MO5 (диапазоны + нет дублей (Year,Month)); entity/tax — как UC-PRC-002;
    • chessboard не вызывается (параметры объекта недвижимости — из тела); DeliveryTime = null (аренда с месяца 0); ипотека — константы 21 / 240;
    • историческая часть price-per-meter — синтетика (реального PropertyObjectId нет);
    • мутаций, событий нет (read-only расчёт); результат — тот же InvestorSeriesResult (growth-chart рисует gross-profit/ebitda/price-per-meter).
  7. Следующие пользовательские запросы investor-series (UC-PRC-002) читают обновлённый singleton — правка действует немедленно, без деплоя и без инвалидации кэшей (кэша нет, ADR-0056).

Немедленно vs асинхронно. Живое чтение модели (investor-series, admin investor-preview) видит новые коэффициенты со следующего запроса. Витринные бейджи каталога обновляются асинхронно: outbox-событие model-changed → recompute job → переиздание property-object-metrics.v1 → каталог (окно eventual-consistency до 15 мин). PUT не блокируется downstream-пересчётом: admin-правка завершается по коммиту строки + outbox-записи; сбой пересчёта не откатывает сохранённую модель (retry/DLQ — на стороне подписчика, ../events/model-changed.md). В окне рассинхронизации страница аналитики и карточка каталога одного объекта недвижимости могут показывать разные числа — допустимо.

Альтернативные Потоки

A1. Конкурентная правка двумя админами

  1. Второй админ сохраняет форму, загруженную до правки первого: RowVersion тела устарел → 409 INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT.
  2. Фронт показывает «модель изменена другим администратором», перезагружает GET (шаг 1) и предлагает повторить правку поверх свежих значений. Возврат на шаг 3.

A2. Значения вне диапазонов

  1. Например, Occupancy = 1.2 или HorizonMonths = 0 → 400 ValidationError со списком полей; строка в БД не изменена (проверка до загрузки/мутаций). Возврат на шаг 3.

A3. Preview с невалидными параметрами

  1. area <= 0, дубль (Year,Month) в monthOverrides, неизвестный tax → 400 (ValidationError / INVEST_ANALYTICS_INVALID_TAX_PARAMS); расчёт не выполняется. Возврат на шаг 5.

A4. Обращение мимо административной системы

  1. Запрос к admin API без m2m-авторизации → 401; с m2m, но без permission invest-analytics.model-config.manage (PUT) → 403. Основная Identity «суперпользователей» не содержит (ADR-0053). Сценарий завершается.

A5. Просмотр без права управления

  1. Актор с invest-analytics.model-config.view, но без .manage: GET и investor-preview доступны, PUT → 403. Форма — в режиме «только чтение».

A6. Сервис упал после коммита PUT, но до отправки интеграционного события

  1. Модель уже сохранена (коммит строки атомарен со вставкой outbox-записи model-changed). Outbox-диспетчер публикует событие после восстановления (at-least-once) — пересчёт витринных метрик произойдёт с задержкой, данные не теряются.
  2. Пользовательские расчёты (investor-series) и admin-preview видят новую модель сразу (читают строку, не событие). Никаких действий администратора не требуется. Возврата в основной поток нет — правка уже применена.

Постусловие

  • invest_analytics.model_config содержит единственную строку с новыми значениями; Version инкрементирован; UpdatedByUserId/UpdatedAt указывают на автора и время правки.
  • Доменное событие EconomicModelConfigUpdatedEvent доставлено in-proc в audit-лог admin-правок.
  • Все последующие расчёты (UC-PRC-002, UC-PRC-004-preview, UC-PRC-005) используют новые платформенные значения — для объектов недвижимости без оверрайдов типа/месяца эффект немедленный.
  • Preview ничего не изменил (read-only).

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
ValidationError400Поля вне диапазонов INV-MC2, не-конечные значения, отсутствует rowVersion; невалидные параметры preview (A2/A3)Показать ошибки по полям формы
INVEST_ANALYTICS_INVALID_TAX_PARAMS400Неизвестный entity/tax в preview (A3)Исправить параметры
Unauthorized401Нет m2m-авторизации административной системы (A4)Повторная аутентификация
Forbidden403Нет invest-analytics.model-config.manage (PUT) / .view (GET, preview)Скрыть/заблокировать действие в UI
INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT409Конкурентная правка: RowVersion устарел (A1)Перезагрузить GET, повторить правку

Доменные События

СобытиеКогда поднимаетсяSubscribers
EconomicModelConfigUpdatedEventУспешное PUT model-config (шаг 4)in-proc: audit-лог admin-правок (ConfigId, Version, UpdatedByUserId)

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
EconomicModelChangedKafkaEvent (ChangeScope = Global)pin.invest-analytics.model-changed.v1Успешное PUT model-config (шаг 4), через outboxinvest-analytics (internal recompute job — пересчёт метрик всех объектов → property-object-metrics.v1); контракт — ../events/model-changed.md

Acceptance Criteria

#КритерийПроверка
AC1GET возвращает актуальные 9 полей + version/updatedAt/rowVersion; после seed — дефолты кодаинтеграционный тест на seed-БД
AC2PUT с валидными значениями: строка обновлена, Version + 1, actor-stamping, событие в audit-логинтеграционный тест + проверка audit
AC3В таблице всегда ровно одна строка (unique ux_model_config_singleton, upsert по well-known Id)тест параллельных PUT на пустой таблице
AC4Конкурентная правка → 409, значения проигравшего не затирают победителя (A1)тест конкурентной правки (два акта с одним rowVersion)
AC5Все диапазоны INV-MC2 отклоняются и проверкой входа (400), и check-constraint-ами (обход API)unit-тест валидатора + тест миграции
AC6Investor-preview: не переданные коэффициенты берутся из сохранённой модели; DeliveryTime=null (аренда с месяца 0); ипотека 21/240golden-тест preview
AC7После PUT следующий investor-series по объекту недвижимости без оверрайдов считает по новым значениямe2e: PUT → GET investor-series
AC8Легаси-поля AppreciationTaxMonthly/EbitdaTaxMonthly сохраняются, но не влияют на расчёт (INV-CALC-5)тест: правка легаси-полей не меняет серию
AC9PUT завершается независимо от downstream-пересчёта; при недоступном consumer сохранённая модель не откатывается, model-changed доставляется из outbox после восстановлениятест: PUT ок при недоступном подписчике, событие доставлено позже
AC10В окне до 15 мин после PUT investor-series показывает новые коэффициенты, а бейджи каталога — прежние; после переиздания property-object-metrics.v1 расхождение исчезаетe2e: PUT → сразу investor-series (новое) + карточка каталога (прежнее) → после пересчёта совпали

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

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

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