Сущность / агрегат: 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 truth | invest-analytics |
| Таблица | invest_analytics.model_config |
| Tenant isolation | не tenanted — платформенные данные (ADR-0052 п.3); запись — только permission уровня платформы |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK; well-known значение 00000000-0000-0000-0000-000000000001 (seed) | миграция | Идентификатор singleton-строки. |
MarketPremium | decimal | Да | numeric(9,6); check >= 0 AND <= 5 | admin API | Надбавка рыночной цены к прайсу объекта недвижимости, доля (дефолт 0.1 = +10%): стартовая рыночная цена м² = PricePerMeter × (1 + MarketPremium). |
PriceGrowthMonthly | decimal | Да | numeric(9,6); check > -1 AND <= 1 | admin API | Месячный темп роста рыночной цены м², доля (дефолт 0.02 = +2%/мес). |
DailyRentM2 | decimal | Да | numeric(12,2); check >= 0 | admin API | Ставка аренды, руб/м²/сутки (дефолт 800). |
RentGrowthMonthly | decimal | Да | numeric(9,6); check > -1 AND <= 1 | admin API | Месячный темп роста ставки аренды, доля (дефолт 0.01). |
ExpenseShare | decimal | Да | numeric(9,6); check >= 0 AND <= 1 | admin API | Доля операционных расходов от выручки аренды (дефолт 0.4): EBITDA/м² = выручка/м² × (1 − ExpenseShare). |
AppreciationTaxMonthly | decimal | Да | numeric(9,6); check >= 0 AND <= 1 | admin API | Легаси-поле (дефолт 13/1200 ≈ 0.010833): месячная ставка налога на прирост стоимости. В серверном расчёте по объекту недвижимости не используется — ставка берётся из налогового режима (tax-entity-params.md); месячная ставка участвует только через помесячный оверрайд (× 12). Хранится для совместимости admin UI; кандидат на deprecation. |
EbitdaTaxMonthly | decimal | Да | numeric(9,6); check >= 0 AND <= 1 | admin API | Легаси-поле (дефолт 13/1200): месячная ставка налога на EBITDA. Семантика и судьба — как у AppreciationTaxMonthly. |
Occupancy | decimal | Да | numeric(9,6); check >= 0 AND <= 1 | admin API | Заполняемость (доля занятых суток месяца; дефолт 0.6). |
HorizonMonths | int | Да | check >= 1 AND <= 360 | admin API | Горизонт прогноза в месяцах (дефолт 60): серия строится для monthIndex 0..HorizonMonths включительно. |
Version | long | Да | >= 1 | app | Номер ревизии модели: инкремент при каждом сохранении; монотонный guard события model-changed. |
CreatedAt / UpdatedAt | DateTimeOffset | Да | — | app | Штампы (UpdatedAt показывается в админке как «дата последнего изменения модели»). |
CreatedByUserId / UpdatedByUserId | Guid? | Нет | — | identity-context | Кто создал (seed → null) / последним изменил. |
Оптимистичная блокировка: конкурентная правка двумя админами не должна затирать чужую (см. «Индексы,
хранение, безопасность» → 409 INVEST_ANALYTICS_MODEL_CONFIG_CONFLICT).
Дефолты кода (fallback последнего уровня)
Если singleton-строка отсутствует (до первого сохранения админом), расчёт использует захардкоженные дефолты; seed-миграция создаёт строку с теми же значениями, поэтому в рантайме fallback срабатывать не должен:
| Поле | Дефолт |
|---|---|
MarketPremium | 0.1 |
PriceGrowthMonthly | 0.02 |
DailyRentM2 | 800 |
RentGrowthMonthly | 0.01 |
ExpenseShare | 0.4 |
AppreciationTaxMonthly | 0.010833 (13/1200) |
EbitdaTaxMonthly | 0.010833 (13/1200) |
Occupancy | 0.6 |
HorizonMonths | 60 |
Связи
| Связь | Тип | Ключ | Правило загрузки | 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-constraints | ValidationError |
| 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 не пересчитывает витрину синхронно. Последовательность и что видит пользователь:
- Admin-правка коммитится атомарно с outbox-записью
model-changed(ChangeScope = Global). - Расчётные операции (
investor-series, admininvestor-preview) читают модель вживую — новые коэффициенты действуют со следующего запроса, без деплоя и без инвалидации кэша (кэша нет, ADR-0056). - Витринные бейджи доходности каталога (окупаемость/рейтинг/рост) обновляются асинхронно: 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 нет |
| Audit | actor-stamping (UpdatedByUserId) + Version; каждая правка — EconomicModelConfigUpdatedEvent в лог |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (4): Налоговые параметры расчёта: EntityKind, TaxRegime, Расчётные модели: InvestorMonth, AnalyticsSeries, AsideMetrics, Сущность / агрегат: PropertyObjectTypeConfig, Сущность: PropertyObjectTypeMonthOverride
- API (8): GET /api/admin/v1/model-config — Чтение платформенной экономической модели (admin), GET /api/admin/v1/property-object-types — Список настроек типов объектов недвижимости (admin), GET /api/admin/v1/property-object-types/{groupId}/month-overrides — Помесячные оверрайды типа объекта недвижимости (admin), GET /api/v1/property-objects/{propertyObjectId}/investor-series — Серии доходности объекта недвижимости, POST /api/admin/v1/investor-preview — Расчёт серий доходности по произвольным параметрам (admin preview / growth-chart), PUT /api/admin/v1/model-config — Обновление платформенной экономической модели (admin), PUT /api/admin/v1/property-object-types/{groupId} — Upsert настройки типа объекта недвижимости (admin), PUT /api/admin/v1/property-object-types/{groupId}/month-overrides — Replace-all помесячных оверрайдов типа объекта недвижимости (admin)
- Use Cases (3): UC-PRC-002. Расчёт доходности объекта недвижимости с выбором субъекта (юр/физ) и налогового режима, UC-PRC-003. Admin: настройка экономической модели платформы + investor-preview, UC-PRC-004. Admin: настройка типа объекта недвижимости и помесячных оверрайдов
- События (3): События invest-analytics: карта контрактов, Event Contract: pin.invest-analytics.model-changed (EconomicModelChangedKafkaEvent), Event Contract: pin.invest-analytics.property-object-metrics-updated (PropertyObjectMetricsUpdatedKafkaEvent)
- index.md (1): InvestAnalytics Service
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- property-object-type-config.md, property-object-type-month-override.md — верхние уровни разрешения коэффициентов.
- investor-series-models.md — как коэффициенты входят в формулы.
- tax-entity-params.md — почему налоговые поля модели легаси.
- Admin-операции: ../api/admin/get-model-config.md, ../api/admin/put-model-config.md.