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

Сущность / агрегат: EconomicModelConfig

Платформенный уровень экономической модели доходности. Проекции (admin API «Глобальный дефолт», расчётные коэффициенты InvestorCoefficients) ссылаются сюда и перечисляют только дельту.

Назначение

EconomicModelConfig — singleton-конфигурация владельцев платформы PIN: базовые коэффициенты расчёта доходности объекта недвижимости (надбавка к прайсу, месячный рост цены, аренда/м², рост аренды, доля расходов, заполняемость, горизонт прогноза). Является нижним уровнем трёхуровневого разрешения коэффициентов: PropertyObjectTypeMonthOverride (месяц) → PropertyObjectTypeConfig (тип объекта недвижимости) → EconomicModelConfig (платформа) → hard-coded дефолты (см. таблицу дефолтов ниже). Алгоритм разрешения — investor-series-models.md, раздел «Разрешение эффективных коэффициентов». Owning-контекст — invest-analytics.

Классификация

ПолеЗначение
Контекстinvest-analytics
Kindагрегат (конфигурация; CRUD-модель — самостоятельной бизнес-логики нет)
Source of truthinvest-analytics
Таблицаinvest_analytics.model_config
Tenant isolationне tenanted — платформенные данные (ADR-0052 п.3); запись — только permission уровня платформы

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPK; well-known значение 00000000-0000-0000-0000-000000000001 (seed)миграцияИдентификатор singleton-строки.
MarketPremiumdecimalДаnumeric(9,6); check >= 0 AND <= 5admin APIНадбавка рыночной цены к прайсу объекта недвижимости, доля (дефолт 0.1 = +10%): стартовая рыночная цена м² = PricePerMeter × (1 + MarketPremium).
PriceGrowthMonthlydecimalДаnumeric(9,6); check > -1 AND <= 1admin APIМесячный темп роста рыночной цены м², доля (дефолт 0.02 = +2%/мес).
DailyRentM2decimalДаnumeric(12,2); check >= 0admin APIСтавка аренды, руб/м²/сутки (дефолт 800).
RentGrowthMonthlydecimalДаnumeric(9,6); check > -1 AND <= 1admin APIМесячный темп роста ставки аренды, доля (дефолт 0.01).
ExpenseSharedecimalДаnumeric(9,6); check >= 0 AND <= 1admin APIДоля операционных расходов от выручки аренды (дефолт 0.4): EBITDA/м² = выручка/м² × (1 − ExpenseShare).
AppreciationTaxMonthlydecimalДаnumeric(9,6); check >= 0 AND <= 1admin APIЛегаси-поле (дефолт 13/1200 ≈ 0.010833): месячная ставка налога на прирост стоимости. В серверном расчёте по объекту недвижимости не используется — ставка берётся из налогового режима (tax-entity-params.md); месячная ставка участвует только через помесячный оверрайд (× 12). Хранится для совместимости admin UI; кандидат на deprecation.
EbitdaTaxMonthlydecimalДаnumeric(9,6); check >= 0 AND <= 1admin APIЛегаси-поле (дефолт 13/1200): месячная ставка налога на EBITDA. Семантика и судьба — как у AppreciationTaxMonthly.
OccupancydecimalДаnumeric(9,6); check >= 0 AND <= 1admin APIЗаполняемость (доля занятых суток месяца; дефолт 0.6).
HorizonMonthsintДаcheck >= 1 AND <= 360admin APIГоризонт прогноза в месяцах (дефолт 60): серия строится для monthIndex 0..HorizonMonths включительно.
VersionlongДа>= 1appНомер ревизии модели: инкремент при каждом сохранении; монотонный guard события model-changed.
CreatedAt / UpdatedAtDateTimeOffsetДаappШтампы (UpdatedAt показывается в админке как «дата последнего изменения модели»).
CreatedByUserId / UpdatedByUserIdGuid?Нетidentity-contextКто создал (seed → null) / последним изменил.

Оптимистичная блокировка: конкурентная правка двумя админами не должна затирать чужую (см. «Индексы, хранение, безопасность» → 409 INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT).

Дефолты кода (fallback последнего уровня)

Если singleton-строка отсутствует (до первого сохранения админом), расчёт использует захардкоженные дефолты; seed-миграция создаёт строку с теми же значениями, поэтому в рантайме fallback срабатывать не должен:

ПолеДефолт
MarketPremium0.1
PriceGrowthMonthly0.02
DailyRentM2800
RentGrowthMonthly0.01
ExpenseShare0.4
AppreciationTaxMonthly0.010833 (13/1200)
EbitdaTaxMonthly0.010833 (13/1200)
Occupancy0.6
HorizonMonths60

Связи

СвязьТипКлючПравило загрузкиDelete behavior
PropertyObjectTypeConfig (логическая: оверрайдит поля модели)reference (без FK — уровни независимы)при расчёте — по well-known Id; тип объекта загружается отдельно по GroupId

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-MC1 SingletonВ таблице ровно одна строкаunique-индекс ux_model_config_singleton по константному выражению ((true)); запись — upsert по well-known Idнарушение невозможно на уровне БД
INV-MC2 RangesДиапазоны всех коэффициентов из таблицы полей (доли 0..1, темпы (-1..1], HorizonMonths 1..360, DailyRentM2 >= 0)валидация admin-обновления + check-constraintsValidationError
INV-MC3 FiniteNumbersВсе значения — конечные числа (запрет NaN/Infinity)тип numeric + валидацияValidationError
INV-MC4 VersionMonotonicКаждое успешное сохранение инкрементирует Versionпри сохранении admin-правки

Доменные и интеграционные события

СобытиеТипКогдаSnapshot/поля
EconomicModelConfigUpdatedEventдоменноеУспешное admin-обновлениеConfigId, Version, UpdatedByUserId — для audit-лога

Интеграционное событие: правка singleton (EconomicModelConfigUpdatedEvent) публикует EconomicModelChangedKafkaEvent (pin.invest-analytics.model-changed.v1, ChangeScope = Global) через outbox — internal recompute job пересчитывает витринные метрики доходности всех объектов недвижимости и переиздаёт pin.invest-analytics.property-object-metrics.v1 (../events/model-changed.md). Сами коэффициенты в payload не уходят (подписчик перечитывает модель); синхронного кэша между сервисами нет (ADR-0056).

Окно согласованности и промежуточное состояние

Правка singleton не пересчитывает витрину синхронно. Последовательность и что видит пользователь:

  1. Admin-правка коммитится атомарно с outbox-записью model-changed (ChangeScope = Global).
  2. Расчётные операции (investor-series, admin investor-preview) читают модель вживую — новые коэффициенты действуют со следующего запроса, без деплоя и без инвалидации кэша (кэша нет, ADR-0056).
  3. Витринные бейджи доходности каталога (окупаемость/рейтинг/рост) обновляются асинхронно: recompute job пересчитывает метрики всех объектов недвижимости и переиздаёт property-object-metrics.v1 — окно eventual-consistency до 15 мин. В этом окне страница аналитики объекта недвижимости и карточка в ленте каталога могут показывать рассинхронизированные числа — это допустимо по контракту (витрина — denormalized-конечная точка).

Монотонность: подписчик отбрасывает пересчёт по model-changed с версией ≤ уже применённой того же scope (Version строки — guard); повторная доставка admin-правки не откатывает более свежий пересчёт. Правка без дельты (no-op upsert) события не публикует — лишнего пересчёта нет.

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK (Id); ux_model_config_singleton unique по ((true)) — гарантия одной строки
Партиционирование / retentionнет — одна строка, живёт вечно
Concurrencyоптимистичная блокировка — конфликт двух админов → 409 INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT; Version — ревизии
Permissions / PIIЧтение — внутреннее (расчёт) + admin invest-analytics.model-config.view; запись — admin invest-analytics.model-config.manage (администратор платформы, ADR-0053, отдельная admin-область API). PII нет
Auditactor-stamping (UpdatedByUserId) + Version; каждая правка — EconomicModelConfigUpdatedEvent в лог

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

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

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