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

UC-OWN-002. Завершение сделки в CRM → купленный объект в «Мои объекты недвижимости» (system)

Назначение

Описывает системный сценарий появления купленного объекта в портфеле инвестора: контекст crm завершает сделку покупки и публикует событие pin.crm.deal-completed.v1, а сервис investor-owner создаёт запись портфеля InvestorPropertyEntity (Source = PinCrmDeal, Status = Active) — купленный объект появляется у инвестора в разделе «Мои объекты недвижимости» (/profile/my-lots). Запускается автоматически при завершении сделки. Связан с контекстом investor-owner.

Metadata

ПолеЗначение
IDUC-OWN-002
Контекстinvestor-owner (см. docs/06-services/investor-owner/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольSystem (системный сценарий сервиса investor-owner; system-actor)
Permissions / scopepermissions не применяются: доступ к топику pin.crm.deal-completed.v1 и вызов internal-API chessboard get-property-objects-by-ids (дозаполнение снапшота) — межсервисные (m2m); мутации — под system-actor (CreatedByUserId = null)
Tenantне применяется: запись портфеля — вне tenant (ADR-0052); TenantId сделки из payload не переносится
Record-levelне применяется на записи; InvestorUserId берётся из payload сделки

Предусловие

  • Событийная шина доступна; сервис подписан на топик pin.crm.deal-completed.v1.
  • Событие несёт снапшот объекта и итоговую цену сделки (требование к контракту crm, будущий путь ../../crm/events/deal-completed.md): DealId, InvestorUserId, PropertyObjectId, ProjectId, BuildingId, DeveloperId, FinalPrice {Amount, CurrencyId}, Currency (CurrencySnapshot валюты — ../../handbook/domain/snapshots.md), CompletedAt, PropertyObjectSnapshot {Title, Number, Project {ProjectId, ProjectName}, Building {BuildingId, BuildingName}, AreaTotal, Floor}, Version.
  • Хранилище контекста investor-owner развёрнуто (в т.ч. уникальность по DealIdux_investor_properties_deal_id — и по активной паре инвестор+объект — ux_investor_properties_investor_property_object_active).
  • InvestorUserId в событии — существующий пользователь-инвестор PIN; investor-owner наличие учётки в identity не проверяет (ссылка без FK, чужой контекст) — это гарантия контракта crm.

Триггер

ПолеЗначение
ИсточникKafka event
Триггерящий элементСобытие завершения сделки DealCompletedKafkaEvent (pin.crm.deal-completed.v1); обработка — ../consumers/crm-deal-completed.md (будущий путь)

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

  1. Менеджер/агент завершает сделку покупки в CRM; crm фиксирует переход Deal → Completed и публикует событие DealCompletedKafkaEvent в топик pin.crm.deal-completed.v1.
  2. Сервис investor-owner принимает событие завершения сделки:
    1. Идемпотентность: повторная доставка того же события (по EventId) подтверждается без эффекта (сквозной trace от сделки сохраняется); обработка под system-actor.
    2. Проверка входа и предусловий:
      • DealId, InvestorUserId, PropertyObjectId заданы;
      • FinalPrice.Amount > 0 (INV-4 PositivePrices), FinalPrice.CurrencyId задан (типизированная ссылка на справочник валют handbook; синхронный резолв не выполняется — валюту валидировал crm на своём write-path, событие несёт готовый CurrencySnapshot);
      • CompletedAt <= now() (ограничение поля PurchasedAt);
      • PropertyObjectSnapshot.Title непустой — при пустом снапшоте см. A3;
      • бизнес-идемпотентность (INV-6 DealIdempotency): если запись портфеля с таким DealId уже есть → идемпотентный no-op, ack (A1);
      • единственность активной записи (INV-1 SingleActivePerLot): если у инвестора уже есть активная запись на этот объект (Status = Active) → A4 (DLQ + разбор Службой заботы), автоматических мутаций нет.
    3. Дополнительная загрузка не требуется — запись портфеля самодостаточна; связь собственника не читается (для Source = PinCrmDeal OwnerMappingId = null, INV-2 SourceConsistency).
    4. Создаём запись портфеля из данных сделки (единственный путь создания для этого источника, INV-2) с маппингом поле-в-поле:
      • Id — новый идентификатор;
      • InvestorUserId ← payload.InvestorUserId (покупатель сделки);
      • DealId ← payload.DealId; OwnerMappingId = null; ExternalOwnershipId = null;
      • PropertyObjectId ← payload.PropertyObjectId; ProjectId ← payload.ProjectId; BuildingId ← payload.BuildingId; DeveloperId ← payload.DeveloperId;
      • PropertyObjectSnapshot ← payload.PropertyObjectSnapshot (SnapshotAt — время события) — иммутабельная фиксация «что купили»; недостающие необязательные поля — A3;
      • PurchasePrice ← payload.FinalPrice (Amount + CurrencyId — итоговая цена сделки, «за сколько купили»); денормализованный CurrencyIsoCode ← payload.Currency.IsoCode (display-код, снятый при записи);
      • PurchasedAt ← payload.CompletedAt (дата завершения сделки);
      • Source = PinCrmDeal; Status = Active (переход [*] → Active);
      • SalePrice/SoldAt/RealizedProfit = null (объект только куплен);
      • фиксируется доменный факт InvestorPropertyAdded.
    5. Согласованность: запись портфеля создаётся, аудит/версионирование (CreatedAt, Version = 1) и факт property-added фиксируются в одном согласованном изменении; уникальный индекс ux_investor_properties_deal_id — последний рубеж гонки двух параллельных обработчиков (проигравший подтверждает существование записи по DealId и идемпотентно завершается).
    6. События: публикуется InvestorPropertyAddedKafkaEvent (топик pin.investor-owner.property-added.v1, ключ и AggregateId = Id; payload: Id, InvestorUserId, PropertyObjectId, ProjectId, Source = PinCrmDeal, PurchasePrice {Amount, CurrencyId}, Currency (CurrencySnapshot из входного события), PurchasedAt, Version).
    7. Результат: событие подтверждается (ack). Появление объекта в портфеле — событийное: до обработки события инвестор объект в «Мои объекты недвижимости» ещё не видит (окно eventual-consistency, целевой SLA появления < 1 мин; лаг обработки мониторится метрикой/алертом, README «Runtime И Эксплуатация»).
  3. notifications (потребитель pin.investor-owner.property-added.v1) отправляет инвестору уведомление «объект появился в портфеле»; user-activity пишет запись в ленту /profile/events.
  4. Инвестор открывает «Мои объекты недвижимости» и видит купленный объект (UC-OWN-003).

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

A1. Повторная доставка / replay события

  1. Запись портфеля с таким DealId уже существует → идемпотентный no-op (INV-6), метрика deal_completed_duplicates_total, ack. Дубль в портфеле не появляется.

A2. Невалидный payload (цена/даты)

  1. FinalPrice.Amount <= 0 (INV-4) либо CompletedAt в будущем → ошибка INVESTOR_OWNER_PRICE_INVALID / ValidationError; событие в DLQ + алерт (ошибка контракта crm, retry бессмыслен).
  2. Исправление — на стороне crm (компенсирующее событие) либо ручное создание записи через admin-API create-investor-property Службой заботы (permission investor-owner.portfolio.manage, с аудитом).

A3. Снапшот в payload неполон

  1. Обязательный PropertyObjectSnapshot.Title есть, необязательные поля (AreaTotal, Floor, Project, Building) отсутствуют → выполняется вызов internal-API chessboard getPropertyObject(PropertyObjectId) (../../chessboard/api/internal/property-objects/get-property-objects-by-ids.md, будущий путь), и снапшот дозаполняется перед созданием записи.
  2. chessboard недоступен → запись создаётся из имеющегося payload (портфель не блокируется); недостающие поля дозаполняются отложенным retry-джобом (README «Зависимости»). Возврат к шагу 2.4.
  3. Отсутствует сам PropertyObjectSnapshot.Title → DLQ (ValidationError) — контракт crm обязан нести снапшот (README «Известные Ограничения»).

A4. У инвестора уже есть Active-запись на этот объект недвижимости

  1. Найдена активная запись на пару (инвестор, объект) (INV-1) — данные рассинхронизированы (например, дубль владения из синка Talent Owners и сделка PIN на один объект недвижимости).
  2. Событие → DLQ с кодом INVESTOR_OWNER_PROPERTY_ALREADY_ACTIVE + тикет Службы заботы; авто-merge запрещён (финансовые данные).
  3. Разрешение: забота архивирует ошибочную запись (archive-investor-property, soft-delete + аудит) и повторяет событие из DLQ → основной поток с шага 2.

A5. Гонка двух обработчиков

  1. Оба не нашли запись по DealId; вторая вставка нарушает уникальность ux_investor_properties_deal_id.
  2. Проигравший обработчик подтверждает существование записи (по DealId) → идемпотентный ack (запись и событие уже созданы первым).

Постусловие

  • В investor_owner.investor_properties существует ровно одна запись с DealId = payload.DealId: Source = PinCrmDeal, Status = Active, PurchasePrice = FinalPrice, PurchasedAt = CompletedAt, OwnerMappingId = null, заполненный PropertyObjectSnapshot (INV-2, INV-4, INV-6).
  • Событие pin.investor-owner.property-added.v1 опубликовано в том же согласованном изменении.
  • Инвестор видит объект в «Мои объекты недвижимости»; notifications/user-activity получили событие.
  • Повторные доставки и replay не создают дублей.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
INVESTOR_OWNER_PRICE_INVALID— (DLQ)FinalPrice.Amount <= 0 (INV-4, A2)DLQ + алерт; исправление контракта/данных crm
INVESTOR_OWNER_PROPERTY_ALREADY_ACTIVE— (DLQ)Активная запись на пару (инвестор, объект недвижимости) уже есть (INV-1, A4)DLQ + тикет заботы; replay после разбора
INVESTOR_OWNER_PROPERTY_SOURCE_MISMATCH— (DLQ)Нарушение согласованности источника при создании записи (INV-2; дефект кода)DLQ + алерт разработке
ValidationError— (DLQ)Пустые обязательные поля payload (DealId/InvestorUserId/PropertyObjectId/PropertyObjectSnapshot.Title), дата в будущемDLQ + алерт; контрактная ошибка crm

HTTP-кодов нет — сценарий полностью событийный; «клиент» — retry/DLQ-политика обработки.

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

СобытиеКогда поднимаетсяSubscribers
InvestorPropertyAddedСоздание записи портфеля из сделкиin-proc (публикация интеграционного события, аудит)

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
InvestorPropertyAddedKafkaEvent.v1pin.investor-owner.property-added.v1Создание записи портфеля из сделкиnotifications («объект появился в портфеле»), user-activity (лента /profile/events)

Consumed: DealCompletedKafkaEvent.v1pin.crm.deal-completed.v1 (контракт — ../../crm/events/deal-completed.md, будущий путь).

Acceptance Criteria

#КритерийПроверка
AC1После deal-completed запись портфеля появляется со всеми полями покупки (PurchasePrice = FinalPrice, PurchasedAt = CompletedAt, Source = PinCrmDeal)интеграционный тест обработки
AC2Повторная доставка того же события не создаёт дубль (INV-6 + idempotency EventId)тест двойной доставки
AC3Гонка двух обработчиков гасится unique ux_investor_properties_deal_id (A5)конкурентный тест
AC4Неполный снапшот дозаполняется из chessboard; при его недоступности запись создаётся из payload (A3)тест с выключенным chessboard
AC5Невалидная цена → DLQ, портфель не мутирует (A2)тест DLQ
AC6Активный дубль по объекту недвижимости → DLQ INVESTOR_OWNER_PROPERTY_ALREADY_ACTIVE, авто-merge отсутствует (A4)тест конфликта INV-1
AC7property-added публикуется в том же согласованном изменении, что и создание записитест согласованной публикации

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

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

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