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

UC-PRC-004. Admin: настройка типа объекта недвижимости и помесячных оверрайдов

Назначение

Описывает admin-сценарий настройки двух верхних уровней экономической модели инвест-аналитики: настройку типа объекта недвижимости (PropertyObjectTypeConfig — nullable-оверрайды платформенных коэффициентов по группе планировок GroupId) и помесячных оверрайдов (PropertyObjectTypeMonthOverride — точечные правки конкретных календарных месяцев, replace-all). Сценарий выполняет администратор платформы через admin API и получает сохранённые настройки, которые немедленно влияют на расчёт доходности объектов этой группы.

Metadata

ПолеЗначение
IDUC-PRC-004
Контекстinvest-analytics (см. docs/06-services/invest-analytics/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАдминистратор платформы (ADR-0053; идентификатор администратора фиксируется для audit)
Permissions / scopeinvest-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)

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

  1. Администратор открывает таб типов объектов недвижимости и получает список настроенных типов (../api/admin/list-property-object-types.md, будущий док): по каждому типу — groupId, наименование, 9 nullable-коэффициентов, version, updatedAt. Дополнительно показываются группы планировок из chessboard, ещё не имеющие настройки (кандидаты на создание).
  2. Администратор выбирает тип (например, GroupId = 42, «Люкс»), заполняет часть коэффициентов (DailyRentM2 = 1200, Occupancy = 0.75; остальные оставляет пустыми — «наследовать» платформенный уровень) и сохраняет тип (../api/admin/put-property-object-type.md, будущий док).
  3. Сохранение настройки типа (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.
  4. Администратор открывает грид месяцев типа и получает полный список строк {year, month, 9 nullable-полей} по возрастанию (Year, Month) (../api/admin/get-month-overrides.md, будущий док); если настройки типа ещё нет → INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND.
  5. Администратор редактирует месяцы (например, «прибивает» MarketPriceM2 = 350000 на 2027-06 и Occupancy = 0.9 на 2027-07, удаляет строку 2026-12) и сохраняет — отправляет полный список месяцев типа (replace-all; ../api/admin/put-month-overrides.md, будущий док).
  6. Сохранение месяцев (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 родителя; грид перерисовывается.
  7. Правки действуют немедленно: следующий investor-series по объекту недвижимости этой группы (UC-PRC-002) разрешает коэффициенты через обновлённый PropertyObjectTypeConfig и применяет новые месяцы поточечно. Для проверки администратор может прогнать investor-preview с этими оверрайдами (UC-PRC-003, шаг 5).

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

A1. Сброс типа к платформенным значениям (удаление настройки)

  1. Администратор сбрасывает тип к платформенным значениям — удаление настройки (../api/admin/put-property-object-type.md, будущий док; если типа нет → 404).
  2. Настройка типа удаляется физически (soft-delete не применяется); каскад по внешнему ключу удаляет все помесячные оверрайды типа; фиксируется доменное PropertyObjectTypeConfigDeletedEvent (GroupId, автор) в audit-лог и публикуется интеграционное EconomicModelChangedKafkaEvent (ChangeScope = PropertyObjectType, ChangeKind = Deleted, GroupId, Version) — пересчёт откатывает витринные метрики группы к платформенному уровню.
  3. Расчёты по объектам недвижимости группы откатываются к платформенному уровню (EconomicModelConfig). Сценарий завершается.

A2. Группа планировок не существует в chessboard

  1. Группа планировок не найдена в chessboardINVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_GROUP_NOT_FOUND (INV-LT1); тип не создаётся. UI штатно предлагает только существующие группы — ошибка защищает от ручных вызовов и рассинхрона справочника. Сценарий завершается.

A3. Месяцы без настройки типа

  1. Сохранение месяцев для groupId без настройки типа → INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND (INV-MO2). Администратор сначала сохраняет тип (шаги 2–3), затем повторяет. Возврат на шаг 5.

A4. Конкурентная правка типа/месяцев

  1. Два администратора правят один тип одновременно: проигравший получает INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT (для месяцев конфликт возникает на touch родителя в рамках replace-all).
  2. Клиент перезагружает тип и грид (шаги 1, 4) и предлагает повторить. Возврат на шаг 2 или 5.

A5. Очистка всех месяцев типа

  1. Администратор удаляет все строки грида и сохраняет: payload — пустой список. Это валидный replace-all: набор месяцев очищается, countAfter = 0 в событии. Расчёт остаётся на уровнях тип/платформа. Возврат на шаг 7.

A6. Невалидные строки месяцев

  1. Дубль (Year, Month) в payload (INV-MO5), пустая строка без единого значения (INV-MO4), Month = 13 или MarketPriceM2 = 0 (INV-MO3) → ValidationError со списком строк/полей; набор в БД не изменён. Возврат на шаг 5.

A7. chessboard недоступен при сохранении типа

  1. 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УсловиеПоведение клиента
ValidationError400INV-LT2/LT4, INV-MO3/MO4/MO5: диапазоны, пустое имя, дубли месяцев, пустые строки (A6)Показать ошибки по строкам/полям
Unauthorized401Нет авторизацииПовторная аутентификация
Forbidden403Нет invest-analytics.property-object-types.manage (сохранение/удаление) / .view (просмотр)Заблокировать действие в UI
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_GROUP_NOT_FOUND404Группы нет в chessboard (INV-LT1, A2)Обновить список групп
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_NOT_FOUND404Месяцы для несозданного типа (INV-MO2, A3); удаление несуществующего типа (A1)Сначала сохранить тип
INVEST_ANALYTICS_PROPERTY_OBJECT_TYPE_CONFLICT409Конкурентная правка типа или replace-all месяцев (A4)Перезагрузить и повторить
INVEST_ANALYTICS_GROUP_SOURCE_UNAVAILABLE503chessboard недоступен при проверке INV-LT1 (A7)Повторить позже

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

СобытиеКогда поднимаетсяSubscribers
PropertyObjectTypeConfigUpsertedEventСоздание/обновление настройки типа (шаг 3)in-proc: audit-лог (GroupId, Version, автор)
PropertyObjectTypeMonthOverridesReplacedEventReplace-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.v1Upsert типа (шаг 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.v1catalog денормализует бейджи (окупаемость, рейтинг, прогнозный рост) карточек и ленты. Окно eventual-consistency — до 15 мин (SLA пересчёта). В этом окне карточки каталога (/lots) могут показывать прежние бейджи доходности, тогда как страница аналитики объекта уже считает по новой модели; расхождение самоустраняется после пересчёта.

Гарантии цепочки:

  • Порядок и монотонность. Ключ партиционирования model-changedChangeScope; подписчик применяет событие, только если 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
AC3Replace-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-подписчика
AC10Upsert/удаление типа и replace-all месяцев публикуют EconomicModelChangedKafkaEvent с верными ChangeScope=PropertyObjectType, GroupId, ChangeKind и растущим Version; internal recompute переиздаёт property-object-metrics.v1, каталог обновляет бейджи в пределах SLA (≤ 15 мин)контрактный тест события + интеграционный тест цепочки пересчёта
AC11No-op-сохранение (без реальной дельты) model-changed не публикует и пересчёт не запускает; повторная доставка события не пересчитывает дважды (монотонный Version)тест идемпотентности/guard

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

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

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