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

InvestAnalytics Service

Назначение

invest-analytics владеет аналитикой доходности недвижимости PIN: (1) append-only историей изменения цен объектов в ClickHouse (источник — контекст chessboard через события) с public API графиков цены (грануляция day/month/year + growthPercent); (2) экономической моделью доходности (платформенный уровень / тип объекта недвижимости / помесячные оверрайды — PostgreSQL); (3) серверным расчётом серий InvestorMonth[] + AnalyticsSeries + AsideMetrics по объекту недвижимости с параметрами субъекта (юр/физ лицо, УСН-6/УСН-15/ОСНО); (4) admin-инструментами (глобальный дефолт, типы объектов недвижимости, помесячные оверрайды, investor-preview, growth-chart) и контрактом PDF-отчёта «Скачать расчёт» (реализация отложена).

Metadata

ПолеЗначение
Статусdraft
Owning teamBackend PIN (инвест-аналитика)
DomainИнвест-аналитика (каталог и деньги)
Data classificationInternal (модель/конфигурация) + Public (цены/серии — публичны в каталоге); PII нет
Runtime criticalitymedium (страница аналитики объекта недвижимости — ключевая для конверсии, но не блокирует сделки)

Ownership

ArtifactOwner/Rule
PostgreSQL schemainvest_analytics
ClickHouse databasepin_analytics (только property_object_price_history_facts; другие применения CH — через ADR, ADR-0056/0057)
API areaspublic (история цен, серии инвестора), admin (модель/типы/оверрайды/preview — за границей ADR-0053), internal (нет в v1)
Owned entitiesPropertyObjectPriceHistoryFact (CH), EconomicModelConfig, PropertyObjectTypeConfig, PropertyObjectTypeMonthOverride (PG); расчётные модели InvestorMonth/AnalyticsSeries/AsideMetrics (не хранятся)
Produced eventspin.invest-analytics.property-object-metrics.v1 (events/property-object-metrics.md), pin.invest-analytics.model-changed.v1 (events/model-changed.md)
Consumed eventspin.chessboard.property-object-price-changed.v1; pin.invest-analytics.model-changed.v1 (self — internal recompute job)
Search indexesнет
Analytics factsdomain/price-history-fact.md

Ответственность И Границы

  • Что сервис делает: приём и вечное append-only хранение изменений цен объектов недвижимости; API рядов цены по объекту недвижимости (day/month/year, growthPercent = рост от первой к последней точке диапазона); разрешение трёхуровневой экономической модели и серверный расчёт серий доходности по объекту недвижимости и по произвольным параметрам (admin-preview); admin CRUD модели; контракт PDF-отчёта расчёта.
  • Что сервис не делает: не является source of truth текущей цены (это chessboard); не считает графики платежей программ покупки (это crm/booking — ипотечные ставка/срок только читаются как вход CreditPayment); не хранит результаты расчёта; не отдаёт статистику просмотров (это dev-stats); не управляет каталожными оверрайдами цены (это catalog).
  • Source of truth: лента изменений цен (CH), экономическая модель всех трёх уровней (PG).
  • Реплики/projections: реплик чужих данных нет — параметры объекта недвижимости (цена, площадь, GroupId, срок сдачи) запрашиваются у chessboard в момент расчёта; измерения факта (Developer/Project/ Building/LayoutGroup/Layout) приходят снапшотом в payload события цены.

Зависимости

ЗависимостьТипЗачем НужнаFailure mode
Kafka (pin.chessboard.property-object-price-changed.v1)queueПоступление изменений ценЛаг ленты цен: график отстаёт, API работает по накопленному; consumer догоняет по offset
chessboardserviceПараметры объекта недвижимости для расчёта серий (BasePrice, Area, PricePerMeter, GroupId, DeliveryDate)Расчёт серий недоступен → 503/502 на запрос; история цен продолжает работать
crm (purchase-config)serviceСтавка/срок ипотеки для CreditPaymentFallback на дефолты конфигурации сервиса (константы 21%/240 мес)
PostgreSQLdbЭкономическая модель (3 уровня)Расчёт серий и admin API недоступны
ClickHousedbИстория ценГрафик/growthPercent недоступны; расчёт серий работает (историческая часть price-per-meter — synthetic-fallback)
identityserviceАутентификация/permissions (public — токен или реф-cookie readOnly; admin — ADR-0053)Отказ авторизации → 401/403

Проекты

src/services/invest-analytics/
InvestAnalytics.Api/ # public + admin endpoints
InvestAnalytics.Application/ # запросы расчёта, команды admin-модели
InvestAnalytics.Domain/ # EconomicModelConfig, PropertyObjectTypeConfig(+MonthOverride), расчётный движок
InvestAnalytics.Infrastructure/ # PostgreSQL, ClickHouse, клиенты chessboard/crm
InvestAnalytics.Background/ # consumer изменения цены (batch ingestion) + пересчёт метрик и публикация property-object-metrics/model-changed; recompute job по model-changed
InvestAnalytics.Migrator/ # миграции PostgreSQL и ClickHouse
InvestAnalytics.Contracts/ # DTO серий доходности

Основные Сущности

EntityНазначениеSource Of TruthДокумент
PropertyObjectPriceHistoryFactAppend-only факт изменения цены объекта недвижимости (ClickHouse, без TTL — вечно)invest-analyticsdomain/price-history-fact.md
EconomicModelConfigSingleton платформенных коэффициентов доходностиinvest-analyticsdomain/economic-model-config.md
PropertyObjectTypeConfigNullable-оверрайды коэффициентов по типу объекта недвижимости (LayoutGroup)invest-analyticsdomain/property-object-type-config.md
PropertyObjectTypeMonthOverrideПомесячные оверрайды типа объекта недвижимости, PK (GroupId, Year, Month)invest-analyticsdomain/property-object-type-month-override.md
InvestorMonth / AnalyticsSeries / AsideMetricsРасчётные модели серий доходности (не хранятся)invest-analytics (вычисление)domain/investor-series-models.md
EntityKind / TaxRegimeПараметры субъекта и налогового режимаinvest-analyticsdomain/tax-entity-params.md

ER-обзор

API

Карта областей:

Events И Consumers

TypeNameTopicНазначениеLink
ConsumedPropertyObjectPriceChangedKafkaEventpin.chessboard.property-object-price-changed.v1Append факта цены в ClickHouse (batch, идемпотентно по FactId) + триггер пересчёта метрикconsumers/chessboard-price-changed.md; контракт события — docs/06-services/chessboard/events/property-object-price-changed.md
Consumed (self)EconomicModelChangedKafkaEventpin.invest-analytics.model-changed.v1Internal recompute job: пересчёт метрик затронутых объектов недвижимости → переиздание property-object-metrics.v1events/model-changed.md
ProducedPropertyObjectMetricsUpdatedKafkaEventpin.invest-analytics.property-object-metrics.v1Snapshot витринных метрик доходности объекта недвижимости (окупаемость/надёжность/рост/аренда/рейтинг)events/property-object-metrics.md
ProducedEconomicModelChangedKafkaEventpin.invest-analytics.model-changed.v1Инвалидация при правке эконом-модели (scope Global/PropertyObjectType) → пересчёт метрикevents/model-changed.md

Background-джобы: backfill первичной загрузки цен из Talent Chessboard — jobs/price-history-backfill.md (будущий док). Решение о канале синка цен из Talent (отдельного consumer-а нет — всё через chessboard) — consumers/talent-price-sync.md.

Интеграционные Потоки И Согласованность

Сервис не держит реплик чужих данных: измерения объекта недвижимости приходят снапшотом в событии цены, а параметры расчёта (цена/площадь/срок сдачи) запрашиваются у chessboard синхронно в момент расчёта. Ниже — две асинхронные цепочки и синхронные вызовы при расчёте, с окнами согласованности и промежуточным состоянием для пользователя.

Поток цен (append-only + пересчёт метрик)

  1. chessboard при каждом изменении Price (в т.ч. синком из Talent) публикует pin.chessboard.property-object-price-changed.v1 (async, key = PropertyObjectId).
  2. Ingest-consumer добавляет append-only факт в ленту ClickHouse (батч ≤ 1000 строк / 5 с; freshness ленты ≤ 5 мин). PG не пишется; интеграционных событий не публикует.
  3. Отдельный recompute-компонент реагирует на то же изменение цены → пересчитывает витринные метрики объекта и публикует pin.invest-analytics.property-object-metrics.v1 (async, key = PropertyObjectId, full snapshot) → catalog денормализует бейджи карточки (SLO ≤ 15 мин).

Промежуточное состояние для пользователя: график истории цен отстаёт от Talent на freshness ленты (≤ 5 мин); бейджи доходности в каталоге — на окно пересчёта (< 15 мин); а расчёт серий (investor-series) всегда считается «на лету» по данным chessboard в момент запроса и лагом ленты не ограничен (историческая часть price-per-meter при отставании ленты мягко деградирует в синтетику).

Поток правки экономической модели (инвалидация → пересчёт)

  1. Admin-правка (PUT model-config / upsert-delete типа / replace-all оверрайдов) атомарно с аудит-событием публикует pin.invest-analytics.model-changed.v1 (async, key = ChangeScope, несёт scope инвалидации, а не значения).
  2. Internal recompute job (self-consumer) с монотонным guard по (ChangeScope, GroupId, Version) пересчитывает затронутые объекты (Global — все; PropertyObjectType — по GroupId) и переиздаёт property-object-metrics.v1catalog.

Промежуточное состояние: расчёт серий и admin-preview отражают новую модель немедленно (кэша нет, считается на каждый запрос); бейджи каталога подтягиваются в окне пересчёта (< 15 мин). Порядок: устаревшая по Version инвалидация того же scope пропускается (idempotent skip).

Синхронные вызовы при расчёте серии

investor-series (и отложенный PDF-отчёт) синхронно обращаются к: chessboard (параметры аналитики объекта — timeout 2 с, 1 retry; 404 → PROPERTY_OBJECT_NOT_FOUND, недоступность после retry → 503); crm purchase-config (ставка/срок — timeout 2 с, недоступность → fallback-константы 21 %/240 мес, не ошибка); ClickHouse (история цены — недоступность/< 2 фактов → синтетика, IsSyntheticHistory = true, не ошибка). Admin-preview этих трёх зависимостей не вызывает — параметры приходят из тела.

Кросс-сервисные контракты (ожидания и гарантии)

НаправлениеКонтрактКлюч/версияПоведение при отсутствии/устаревании
Ожидает от chessboard (событие)снапшот измерений DeveloperId/ProjectId/BuildingId/LayoutGroupId/LayoutId/AreaTotal в payload ценыkey PropertyObjectId, v1нет снапшота → строка факта отбраковывается (DLQ); реплика измерений принципиально не заводится
Ожидает от chessboard (sync)параметры аналитики BasePrice/AreaTotal/PricePerMeter/GroupId/DeliveryDatechessboard — источник истины; расчёт всегда по актуальному ответу, кэша параметров нет
Ожидает от crm (sync)ставка/срок ипотеки для CreditPaymentfallback-константы 21 %/240 мес (деградация)
Предоставляет catalogproperty-object-metrics.v1 — полный snapshot метрик объектаkey PropertyObjectId, 3 партиции, v1snapshot-семантика: re-read топика безопасен; null-поле очищает сырое значение витрины
Предоставляет себе (self)model-changed.v1 — scope инвалидацииkey ChangeScope, 1 партиция, v1коэффициенты в payload не уходят — подписчик перечитывает модель

Принятые Решения

#РешениеОбоснование
1История цен — ClickHouse ReplacingMergeTree, PARTITION toYYYYMM(EventTime), ORDER (PropertyObjectId, EventTime, FactId), без TTLТребование append-only; вечное хранение — график за жизненный цикл + портфель; объём тривиален (обоснование в domain/price-history-fact.md)
2Без materialized views/rollup-ов для ценЗапросы всегда по одному объекту недвижимости (сотни строк) — simplicity-first ADR-0056; отличие от dev-stats (ADR-0057)
3Расчёт серий — серверный, на каждый запрос, без кэша и без персистаДетерминированный дешёвый цикл ≤ 361 итерации; ADR-0056
4Ставки налогов — константы кода, а не конфигурацияПлатформенные налоговые поля модели легаси (domain/tax-entity-params.md)
5Измерения факта — снапшотом в payload события chessboardНе заводить реплику измерений (в отличие от dev-stats): консюмеру цен достаточно события
6Историческая часть графика price-per-meter — из реальных фактов ClickHouse; синтетическая экстраполяция остаётся fallback-ом (< 2 фактов по объекту недвижимости)Честный график при наличии данных, мягкая деградация при их отсутствии
7PDF-отчёт «Скачать расчёт» — контракт сейчас, реализация отложенаГенерация — отчётным модулем платформы
8Wire-контракт серий — camelCase, ключи gross-profit/ebitda/price-per-meterЕдиный стабильный контракт фронта каталога

Runtime И Эксплуатация

ОбластьТребование
Health checkslive/ready (Api, Background); ready проверяет PG, ClickHouse, Kafka
SLO/SLAAPI p95 ≤ 300 мс (расчёт серий), ≤ 200 мс (история цен); свежесть фактов ≤ 5 мин
Dashboardборд контекста: ingest lag, RPS/latency методов, ошибки консюмера
Alertsрост invest_analytics_price_ingest_lag_seconds > 15 мин; ошибки batch-вставки CH; 5xx public API
Runbooksпереигрывание консюмера с offset (вставка идемпотентна); повторный backfill диапазона PropertyObjectId
Feature flagsinvest-analytics.synthetic-history-fallback (вкл/выкл синтетическую историю)

Безопасность

  • Основные actor types: гость по реф-ссылке (readOnly страница аналитики), инвестор, агент/менеджер (та же public-выдача), администратор платформы (admin API, отдельная identity — ADR-0053).
  • Permission model: invest-analytics.price-history.view, invest-analytics.series.view (public, выдаются ролям каталога), invest-analytics.model-config.view/manage, invest-analytics.property-object-types.view/manage (admin) — каталог permissions в docs/06-services/identity/domain/permissions.md.
  • Sensitive data classes: PII нет; экономическая модель — Internal (не раскрывается public API: наружу уходят только вычисленные серии).
  • Audit: правки модели — actor-stamping + Version + доменные события (аудит-лог admin API).
  • Tenant isolation rule: PG-конфигурация — платформенная, не tenanted (ADR-0052 п.3); CH-факт несёт TenantId = DeveloperId, но публичное чтение не фильтруется по tenant (цены публичны в каталоге); запись — только consumer.

Известные Ограничения И Assumptions

  • Payload pin.chessboard.property-object-price-changed.v1 обязан нести снапшот измерений (DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, AreaTotal) — требование к ТЗ chessboard.
  • Ипотечные параметры (CreditPayment) читаются из purchase-config crm; до его готовности — константы 21%/240 мес.
  • Ставка ОСНО 43% — модельное допущение владельцев платформы, не налоговый расчёт.
  • Поле CumulativeEbitda дублирует Ebitda — сохранено для совместимости контракта.
  • PDF-отчёт: только контракт; генерация — отложена.
  • Оверрайды заданы на уровне типа объекта недвижимости (LayoutGroup), не отдельного объекта; per-lot оверрайды при необходимости — отдельным решением.

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