UC-PRC-004. Admin: настройка типа объекта недвижимости и помесячных оверрайдов
Назначение
Описывает admin-сценарий настройки двух верхних уровней экономической модели инвест-аналитики: настройку
типа объекта недвижимости (PropertyObjectTypeConfig —
nullable-оверрайды платформенных коэффициентов по группе планировок GroupId) и помесячных оверрайдов
(PropertyObjectTypeMonthOverride — точечные правки
конкретных календарных месяцев, replace-all). Сценарий выполняет администратор платформы через admin API и
получает сохранённые настройки, которые немедленно влияют на расчёт доходности объектов этой группы.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-PRC-004 |
| Контекст | invest-analytics (см. docs/06-services/invest-analytics/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Администратор платформы (ADR-0053; идентификатор администратора фиксируется для audit) |
| Permissions / scope | invest-analytics.property-object-types.view (просмотр), invest-analytics.property-object-types.manage (сохранение/удаление); scope — платформа |
| Tenant | Платформенная настройка; tenant не применяется (ADR-0052 п.3) |
| Record-level | Не применяется |
Предусловие
- Администратор платформы авторизован; доступ к admin area
invest-analyticsесть. - Справочник групп планировок (
LayoutGroup) синхронизирован из Talent, иchessboardдоступен для проверки существования группы (INV-LT1) — см.docs/06-services/chessboard/domain/layout-group.md(будущий путь).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button / API call |
| Триггерящий элемент | Таб настройки типов объектов недвижимости: выбор типа (просмотр), «Сохранить тип» (сохранение настройки типа), грид месяцев + «Сохранить месяцы» (помесячные оверрайды, replace-all) |
Основной Поток
- Администратор открывает таб типов объектов недвижимости и получает список настроенных типов
(../api/admin/list-property-object-types.md, будущий док):
по каждому типу —
groupId, наименование, 9 nullable-коэффициентов,version,updatedAt. Дополнительно показываются группы планировок изchessboard, ещё не имеющие настройки (кандидаты на создание). - Администратор выбирает тип (например,
GroupId = 42, «Люкс»), заполняет часть коэффициентов (DailyRentM2 = 1200,Occupancy = 0.75; остальные оставляет пустыми — «наследовать» платформенный уровень) и сохраняет тип (../api/admin/put-property-object-type.md, будущий док). - Сохранение настройки типа (upsert
PropertyObjectTypeConfig):- Проверить вход:
GroupId > 0;Nameнепустое после trim, ≤ 128 символов (INV-LT4); каждый заполненный (not null) коэффициент — в диапазонах INV-MC2 (доли0..1, темпы(−1..1],DailyRentM2 >= 0,HorizonMonths 1..360) и конечным числом (INV-LT3, безNaN/Infinity);null— валидное значение «наследовать» (INV-LT2). Нарушение →ValidationError. - Проверить существование группы планировок в
chessboard(границы контекстов: FK на чужой контекст не ставится). Группа не найдена →INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_GROUP_NOT_FOUND(INV-LT1, A2);chessboardнедоступен →INVEST_ANALYTICS_GROUP_SOURCE_UNAVAILABLE(A7) — проверку пропускать нельзя. - Маппинг поле-в-поле:
Nameи каждый из 9 nullable-коэффициентов ← одноимённое поле тела (полная замена, включая обнуление ранее заполненных). При созданииId = GroupId(натуральный ключ),Version = 1; при обновленииVersion + 1; фиксируются автор и время правки (actor-stamping). - Конкурентная правка одного типа →
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT(A4). - Фиксируются события: доменное
PropertyObjectTypeConfigUpsertedEvent(audit-лог) и интеграционноеEconomicModelChangedKafkaEvent(ChangeScope = PropertyObjectType,ChangeKind = Upserted,GroupId,Versionтипа; топикpin.invest-analytics.model-changed.v1) — асинхронный триггер пересчёта витринных метрик объектов этого типа (../events/model-changed.md); окно согласованности каталога — см. «Согласованность и что видит пользователь». - Результат: сохранённый тип с
version.
- Проверить вход:
- Администратор открывает грид месяцев типа и получает полный список строк
{year, month, 9 nullable-полей}по возрастанию (Year,Month) (../api/admin/get-month-overrides.md, будущий док); если настройки типа ещё нет →INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND. - Администратор редактирует месяцы (например, «прибивает»
MarketPriceM2 = 350000на 2027-06 иOccupancy = 0.9на 2027-07, удаляет строку 2026-12) и сохраняет — отправляет полный список месяцев типа (replace-all; ../api/admin/put-month-overrides.md, будущий док). - Сохранение месяцев (replace-all
PropertyObjectTypeMonthOverride):- Проверить вход по каждой строке:
Year ∈ [2000..2100],Month ∈ [1..12]; диапазоны значений INV-MO3 (абсолютыMarketPriceM2/DailyRentM2/Ebitda> 0при not null; доли0..1; темпы(−1..1]; месячные налоговые ставки0..1/12); каждая строка содержит хотя бы одно not null-поле оверрайда (INV-MO4, полностью пустой месяц отклоняется); нет двух строк с одинаковой парой (Year,Month) (INV-MO5); размер payload ≤ 600 строк (защита: 50 лет × 12 — за пределами горизонта 360 смысла нет). Нарушение →ValidationError(A6). - Настройка типа должна существовать; иначе →
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND(INV-MO2, A3): сначала сохраняется тип (шаги 2–3), затем месяцы. - Маппинг: каждая строка payload →
PropertyObjectTypeMonthOverride {GroupId, Year, Month, MarketPriceM2, PriceGrowthMonthly, DailyRentM2, RentGrowthMonthly, Occupancy, ExpenseShare, Ebitda, AppreciationTaxMonthly, EbitdaTaxMonthly}— один-в-один, без вычислений; у родителя инкрементируетсяVersionи фиксируются автор/время правки (touch). - Замена набора атомарна (INV-MO6): старые строки удаляются, новые вставляются, родитель тачится — в
одном согласованном изменении; промежуточное состояние «месяцы удалены, новые не вставлены» снаружи
невидимо. Composite PK (
GroupId,Year,Month) — последний рубеж дублей (INV-MO1). Конкурентный replace-all →INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT(A4). - Фиксируется доменное
PropertyObjectTypeMonthOverridesReplacedEvent(audit-лог:GroupId, число строк до/после, автор) и интеграционноеEconomicModelChangedKafkaEvent(ChangeScope = PropertyObjectType,ChangeKind = MonthOverridesReplaced,GroupId,Versionродителя) — асинхронный триггер пересчёта витринных метрик группы. - Результат: сохранённый список месяцев +
versionродителя; грид перерисовывается.
- Проверить вход по каждой строке:
- Правки действуют немедленно: следующий
investor-seriesпо объекту недвижимости этой группы (UC-PRC-002) разрешает коэффициенты через обновлённыйPropertyObjectTypeConfigи применяет новые месяцы поточечно. Для проверки администратор может прогнатьinvestor-previewс этими оверрайдами (UC-PRC-003, шаг 5).
Альтернативные Потоки
A1. Сброс типа к платформенным значениям (удаление настройки)
- Администратор сбрасывает тип к платформенным значениям — удаление настройки (../api/admin/put-property-object-type.md, будущий док; если типа нет → 404).
- Настройка типа удаляется физически (soft-delete не применяется); каскад по внешнему ключу удаляет все
помесячные оверрайды типа; фиксируется доменное
PropertyObjectTypeConfigDeletedEvent(GroupId, автор) в audit-лог и публикуется интеграционноеEconomicModelChangedKafkaEvent(ChangeScope = PropertyObjectType,ChangeKind = Deleted,GroupId,Version) — пересчёт откатывает витринные метрики группы к платформенному уровню. - Расчёты по объектам недвижимости группы откатываются к платформенному уровню (
EconomicModelConfig). Сценарий завершается.
A2. Группа планировок не существует в chessboard
- Группа планировок не найдена в
chessboard→INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_GROUP_NOT_FOUND(INV-LT1); тип не создаётся. UI штатно предлагает только существующие группы — ошибка защищает от ручных вызовов и рассинхрона справочника. Сценарий завершается.
A3. Месяцы без настройки типа
- Сохранение месяцев для
groupIdбез настройки типа →INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND(INV-MO2). Администратор сначала сохраняет тип (шаги 2–3), затем повторяет. Возврат на шаг 5.
A4. Конкурентная правка типа/месяцев
- Два администратора правят один тип одновременно: проигравший получает
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT(для месяцев конфликт возникает на touch родителя в рамках replace-all). - Клиент перезагружает тип и грид (шаги 1, 4) и предлагает повторить. Возврат на шаг 2 или 5.
A5. Очистка всех месяцев типа
- Администратор удаляет все строки грида и сохраняет: payload — пустой список. Это валидный replace-all:
набор месяцев очищается,
countAfter = 0в событии. Расчёт остаётся на уровнях тип/платформа. Возврат на шаг 7.
A6. Невалидные строки месяцев
- Дубль (
Year,Month) в payload (INV-MO5), пустая строка без единого значения (INV-MO4),Month = 13илиMarketPriceM2 = 0(INV-MO3) →ValidationErrorсо списком строк/полей; набор в БД не изменён. Возврат на шаг 5.
A7. chessboard недоступен при сохранении типа
chessboardнедоступен при проверке существования группы →INVEST_ANALYTICS_GROUP_SOURCE_UNAVAILABLE; тип не сохраняется (проверку INV-LT1 пропускать нельзя — иначе возможен мусорныйGroupId). Администратор повторяет позже. Сценарий завершается.
Постусловие
- Настройка типа (
PropertyObjectTypeConfig) содержит заполненные оверрайды иnull-ы «наследовать»;Versionинкрементирован, actor-stamping заполнен. - Помесячные оверрайды (
PropertyObjectTypeMonthOverride) содержат ровно присланный набор месяцев типа (replace-all атомарен; старые строки удалены, дублей нет — composite PK). - Доменные события (
PropertyObjectTypeConfigUpsertedEvent/PropertyObjectTypeMonthOverridesReplacedEvent/PropertyObjectTypeConfigDeletedEvent) доставлены в audit-лог admin-правок. - Расчёты UC-PRC-002 по объектам недвижимости группы немедленно используют новые уровни (кэша нет).
- Интеграционное
EconomicModelChangedKafkaEvent(ChangeScope = PropertyObjectType,GroupId, актуальныйVersion, соответствующийChangeKind) отправлено; витринные метрики каталога по объектам группы пересчитываются асинхронно (окно ≤ 15 мин).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | INV-LT2/LT4, INV-MO3/MO4/MO5: диапазоны, пустое имя, дубли месяцев, пустые строки (A6) | Показать ошибки по строкам/полям |
Unauthorized | 401 | Нет авторизации | Повторная аутентификация |
Forbidden | 403 | Нет invest-analytics.property-object-types.manage (сохранение/удаление) / .view (просмотр) | Заблокировать действие в UI |
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_GROUP_NOT_FOUND | 404 | Группы нет в chessboard (INV-LT1, A2) | Обновить список групп |
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND | 404 | Месяцы для несозданного типа (INV-MO2, A3); удаление несуществующего типа (A1) | Сначала сохранить тип |
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT | 409 | Конкурентная правка типа или replace-all месяцев (A4) | Перезагрузить и повторить |
INVEST_ANALYTICS_GROUP_SOURCE_UNAVAILABLE | 503 | chessboard недоступен при проверке INV-LT1 (A7) | Повторить позже |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
PropertyObjectTypeConfigUpsertedEvent | Создание/обновление настройки типа (шаг 3) | in-proc: audit-лог (GroupId, Version, автор) |
PropertyObjectTypeMonthOverridesReplacedEvent | Replace-all месяцев типа (шаг 6) | in-proc: audit-лог (GroupId, число строк до/после, автор) |
PropertyObjectTypeConfigDeletedEvent | Удаление настройки типа (A1) | in-proc: audit-лог (GroupId, автор) |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
EconomicModelChangedKafkaEvent (ChangeScope = PropertyObjectType, GroupId, Version, ChangeKind ∈ {Upserted, Deleted, MonthOverridesReplaced}) | pin.invest-analytics.model-changed.v1 | Upsert типа (шаг 3 → Upserted) / удаление типа (A1 → Deleted) / replace-all месяцев (шаг 6 → MonthOverridesReplaced); сохранение без реальной дельты (no-op) не публикует | invest-analytics (internal recompute job — пересчёт метрик объектов типа → property-object-metrics.v1, окно ≤ 15 мин); контракт — ../events/model-changed.md |
Согласованность и что видит пользователь
Правки админа распространяются двумя путями с разным окном видимости:
- Синхронно, немедленно (кэша нет): следующий
investor-series(UC-PRC-002) иinvestor-preview(UC-PRC-003) по объекту недвижимости группы читают обновлённые уровни со следующего запроса — админ на превью и пользователь на странице аналитики видят новые числа сразу. - Асинхронно, с задержкой (событийная цепочка):
EconomicModelChangedKafkaEvent→ internal recompute job пересчитывает витринные метрики доходности объектов группы и переиздаётpin.invest-analytics.property-object-metrics.v1→catalogденормализует бейджи (окупаемость, рейтинг, прогнозный рост) карточек и ленты. Окно eventual-consistency — до 15 мин (SLA пересчёта). В этом окне карточки каталога (/lots) могут показывать прежние бейджи доходности, тогда как страница аналитики объекта уже считает по новой модели; расхождение самоустраняется после пересчёта.
Гарантии цепочки:
- Порядок и монотонность. Ключ партиционирования
model-changed—ChangeScope; подписчик применяет событие, только еслиVersionстрого больше последней применённой для этого scope/GroupId. Если админ сохраняет тип, а затем месяцы — публикуются два события с растущимVersionродителя, побеждает последнее. - Идемпотентность. Повторная доставка
model-changed(ключ дедупликации + монотонныйVersion) повторного пересчёта не вызывает; сохранение без реальной дельты (no-op upsert) события не публикует — лишнего пересчёта нет. - Fail-safe. Неизвестное значение
ChangeScope/ChangeKindу подписчика → консервативный полный пересчёт (инвалидируем шире) + предупреждение в лог; событие не теряется. - Атомарность анонса. Доменное audit-событие и
EconomicModelChangedKafkaEventфиксируются атомарно с мутацией настройки — расчётная модель и анонс её изменения не расходятся.
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Сохранение типа с частично заполненными коэффициентами: null-поля наследуются от платформы в расчёте (per-field) | интеграционный тест: тип с одним полем → серия меняет только его |
| AC2 | Несуществующая группа chessboard → 404, тип не создан (INV-LT1) | тест с моком chessboard |
| AC3 | Replace-all атомарен: старый набор удалён, новый вставлен, Version родителя +1 — в одном согласованном изменении (INV-MO6) | тест с падением на вставке новых строк → набор не изменён |
| AC4 | Дубль месяца в payload / пустая строка / Month=13 → 400, БД не тронута (INV-MO3/4/5) | unit-тесты валидации |
| AC5 | Пустой payload очищает все месяцы типа (A5) | интеграционный тест |
| AC6 | Конкурентные replace-all: проигравший получает 409, набор победителя не повреждён (A4) | конкурентный тест оптимистичной блокировки |
| AC7 | Удаление типа каскадно удаляет месяцы; расчёт откатывается к платформенным значениям (A1) | e2e: удаление → investor-series |
| AC8 | После сохранения месяцев investor-series применяет абсолюты по positiveOr и налоговые × 12 именно в заданных месяцах | e2e: сохранение месяцев → сравнение серий до/после |
| AC9 | Все три доменных события попадают в audit-лог с идентификатором администратора | тест audit-подписчика |
| AC10 | Upsert/удаление типа и replace-all месяцев публикуют EconomicModelChangedKafkaEvent с верными ChangeScope=PropertyObjectType, GroupId, ChangeKind и растущим Version; internal recompute переиздаёт property-object-metrics.v1, каталог обновляет бейджи в пределах SLA (≤ 15 мин) | контрактный тест события + интеграционный тест цепочки пересчёта |
| AC11 | No-op-сохранение (без реальной дельты) model-changed не публикует и пересчёт не запускает; повторная доставка события не пересчитывает дважды (монотонный Version) | тест идемпотентности/guard |
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): PUT /api/admin/v1/property-object-types/{groupId}/month-overrides — Replace-all помесячных оверрайдов типа объекта недвижимости (admin)
- Use Cases (2): UC-PRC-002. Расчёт доходности объекта недвижимости с выбором субъекта (юр/физ) и налогового режима, UC-PRC-003. Admin: настройка экономической модели платформы + investor-preview
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы (дома контрактов, будущие доки): ../api/admin/list-property-object-types.md, ../api/admin/put-property-object-type.md, ../api/admin/get-month-overrides.md, ../api/admin/put-month-overrides.md.
- Domain entities: ../domain/property-object-type-config.md (INV-LT1..4), ../domain/property-object-type-month-override.md (INV-MO1..6).
- Семантика коэффициентов: ../domain/economic-model-config.md.
- Применение в расчёте: ../domain/investor-series-models.md.
- Справочник групп:
docs/06-services/chessboard/domain/layout-group.md(будущий путь). - Смежные UC: UC-PRC-002 (потребитель уровней), UC-PRC-003 (платформенный уровень + preview).
- Решения: ADR-0052, ADR-0053, ADR-0056.