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
| Поле | Значение |
|---|---|
| ID | UC-PRC-003 |
| Контекст | invest-analytics |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Администратор платформы (ADR-0053: отдельная административная система с собственными учётками; доступ к admin API контекста — m2m-авторизация + actor-id админа для аудита) |
| Permissions / scope | invest-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-knownId = 00000000-0000-0000-0000-000000000001и дефолтами кода.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button / API call |
| Триггерящий элемент | Таб «Глобальный дефолт»: загрузка таба (GET), кнопка «Сохранить» (PUT), кнопка «Предпросмотр» / growth-chart (POST investor-preview) |
Основной Поток
- Админ открывает таб «Глобальный дефолт»; фронт вызывает
GET model-config(../api/admin/get-model-config.md). - GET (read-only): actor = админ, permission
invest-analytics.model-config.view; система читает singleton по well-known Id и возвращает 9 полей модели +version,updatedAt,updatedByUserId,rowVersion(для конкурентного PUT); при отсутствии строки — ответ из дефолтов кода с признакомisSeedDefaults=true(штатно недостижимо). - Админ правит значения (например,
PriceGrowthMonthly 0.02 → 0.025,Occupancy 0.6 → 0.7) и нажимает «Сохранить»; фронт вызываетPUT model-configс телом из 9 полей +rowVersion(../api/admin/put-model-config.md). - Система обрабатывает 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) → 409INVEST_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; фронт обновляет форму (показывает «дату последнего изменения модели»).
- Проверка входа (зеркалит check-constraints, INV-MC2/MC3):
- Админ проверяет эффект: кнопка «Предпросмотр» / growth-chart вызывает
POST investor-previewс произвольными параметрами (../api/admin/investor-preview.md): тело —basePrice,area,pricePerMeter, опциональный полный набор из 9 коэффициентов (не переданные — разрешаются из сохранённой модели по цепочке уровней), список помесячных оверрайдовmonthOverrides[](та же схема, что ../domain/property-object-type-month-override.md),entity,tax. - 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).
- валидация:
- Следующие пользовательские запросы
investor-series(UC-PRC-002) читают обновлённый singleton — правка действует немедленно, без деплоя и без инвалидации кэшей (кэша нет, ADR-0056).
Немедленно vs асинхронно. Живое чтение модели (
investor-series, admininvestor-preview) видит новые коэффициенты со следующего запроса. Витринные бейджи каталога обновляются асинхронно: outbox-событиеmodel-changed→ recompute job → переизданиеproperty-object-metrics.v1→ каталог (окно eventual-consistency до 15 мин). PUT не блокируется downstream-пересчётом: admin-правка завершается по коммиту строки + outbox-записи; сбой пересчёта не откатывает сохранённую модель (retry/DLQ — на стороне подписчика, ../events/model-changed.md). В окне рассинхронизации страница аналитики и карточка каталога одного объекта недвижимости могут показывать разные числа — допустимо.
Альтернативные Потоки
A1. Конкурентная правка двумя админами
- Второй админ сохраняет форму, загруженную до правки первого:
RowVersionтела устарел → 409INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT. - Фронт показывает «модель изменена другим администратором», перезагружает GET (шаг 1) и предлагает повторить правку поверх свежих значений. Возврат на шаг 3.
A2. Значения вне диапазонов
- Например,
Occupancy = 1.2илиHorizonMonths = 0→ 400ValidationErrorсо списком полей; строка в БД не изменена (проверка до загрузки/мутаций). Возврат на шаг 3.
A3. Preview с невалидными параметрами
area <= 0, дубль (Year,Month) вmonthOverrides, неизвестныйtax→ 400 (ValidationError/INVEST_ANALYTICS_INVALID_TAX_PARAMS); расчёт не выполняется. Возврат на шаг 5.
A4. Обращение мимо административной системы
- Запрос к admin API без m2m-авторизации → 401; с m2m, но без permission
invest-analytics.model-config.manage(PUT) → 403. Основная Identity «суперпользователей» не содержит (ADR-0053). Сценарий завершается.
A5. Просмотр без права управления
- Актор с
invest-analytics.model-config.view, но без.manage: GET и investor-preview доступны, PUT → 403. Форма — в режиме «только чтение».
A6. Сервис упал после коммита PUT, но до отправки интеграционного события
- Модель уже сохранена (коммит строки атомарен со вставкой outbox-записи
model-changed). Outbox-диспетчер публикует событие после восстановления (at-least-once) — пересчёт витринных метрик произойдёт с задержкой, данные не теряются. - Пользовательские расчёты (
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 | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | Поля вне диапазонов INV-MC2, не-конечные значения, отсутствует rowVersion; невалидные параметры preview (A2/A3) | Показать ошибки по полям формы |
INVEST_ANALYTICS_INVALID_TAX_PARAMS | 400 | Неизвестный entity/tax в preview (A3) | Исправить параметры |
Unauthorized | 401 | Нет m2m-авторизации административной системы (A4) | Повторная аутентификация |
Forbidden | 403 | Нет invest-analytics.model-config.manage (PUT) / .view (GET, preview) | Скрыть/заблокировать действие в UI |
INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT | 409 | Конкурентная правка: 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), через outbox | invest-analytics (internal recompute job — пересчёт метрик всех объектов → property-object-metrics.v1); контракт — ../events/model-changed.md |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | GET возвращает актуальные 9 полей + version/updatedAt/rowVersion; после seed — дефолты кода | интеграционный тест на seed-БД |
| AC2 | PUT с валидными значениями: строка обновлена, 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-тест валидатора + тест миграции |
| AC6 | Investor-preview: не переданные коэффициенты берутся из сохранённой модели; DeliveryTime=null (аренда с месяца 0); ипотека 21/240 | golden-тест preview |
| AC7 | После PUT следующий investor-series по объекту недвижимости без оверрайдов считает по новым значениям | e2e: PUT → GET investor-series |
| AC8 | Легаси-поля AppreciationTaxMonthly/EbitdaTaxMonthly сохраняются, но не влияют на расчёт (INV-CALC-5) | тест: правка легаси-полей не меняет серию |
| AC9 | PUT завершается независимо от downstream-пересчёта; при недоступном consumer сохранённая модель не откатывается, model-changed доставляется из outbox после восстановления | тест: PUT ок при недоступном подписчике, событие доставлено позже |
| AC10 | В окне до 15 мин после PUT investor-series показывает новые коэффициенты, а бейджи каталога — прежние; после переиздания property-object-metrics.v1 расхождение исчезает | e2e: PUT → сразу investor-series (новое) + карточка каталога (прежнее) → после пересчёта совпали |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-PRC-002. Расчёт доходности объекта недвижимости с выбором субъекта (юр/физ) и налогового режима, UC-PRC-004. Admin: настройка типа объекта недвижимости и помесячных оверрайдов
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы (дома контрактов): ../api/admin/get-model-config.md, ../api/admin/put-model-config.md, ../api/admin/investor-preview.md.
- Domain entity: ../domain/economic-model-config.md (поля, INV-MC1..4, дефолты кода).
- Движок расчёта preview: ../domain/investor-series-models.md.
- Налоги preview: ../domain/tax-entity-params.md.
- Смежные UC: UC-PRC-002 (потребитель модели), UC-PRC-004 (верхние уровни модели).
- Permissions:
docs/06-services/identity/domain/permissions.md(каталог admin-permissions). - Решения: ADR-0052 (не tenanted), ADR-0053 (admin identity), ADR-0056 (без кэша).