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

POST /api/v1/internal/property-objects/by-ids — Батч-выдача объектов недвижимости по идентификаторам (internal, для catalog/crm)

Назначение

Внутренний m2m-метод для сервисов PIN: батч-чтение актуального состояния объектов недвижимости по списку id. Потребители:

  • crm — синхронная проверка объекта недвижимости в момент создания заявки на бронирование/сделки (актуальные Status/HoldDealId/Price из source of truth, а не из своей реплики; асинхронные обновления CRM получает событиями pin.chessboard.property-object-status-changed.v1/property-object-price-changed.v1);
  • catalog — reconciliation витрины: точечная сверка/восстановление проекции после DLQ/replay (основной канал — события pin.chessboard.property-object-upserted.v1).
Почему POST, а не GET

Список id не помещается в query string (до 500 элементов), поэтому метод — POST. При этом он безопасен и идемпотентен: тело запроса не мутирует состояние (только чтение).

Без masking

Ответ отдаётся без masking (system-to-system) и включает Hidden- и soft-deleted-объекты недвижимости с явными флагами — потребитель сам решает, что делать с невидимыми публично записями.

Metadata

ПолеЗначение
Сервис/контекстchessboard
API areainternal
Feature grouppropertyObjects
Статусdraft
OwnerPIN Platform Core
Документdocs/06-services/chessboard/api/internal/property-objects/get-property-objects-by-ids.md

Актор и доступ

ПроверкаЗначение
Actor typeSystem — m2m-клиент сервисов PIN (catalog, crm)
Auth policy / Permissionsm2m-токен identity (client credentials), policy ChessboardInternalOnly; список разрешённых client-id — конфигурация; internal area не экспонируется через публичный gateway
Scope (RBAC)не применяется (платформенное system-чтение)
Record-levelне применяется
Tenant isolationBypass tenant- и soft-delete-фильтров — system-actor читает объекты недвижимости всех застройщиков, включая Hidden/удалённые (флаги в ответе)
Auditnone (read; вызовы логируются с client-id)

HTTP-контракт

ПолеЗначение
MethodPOST (read-only batch lookup; тело — список id)
Route/api/v1/internal/property-objects/by-ids
Success status200
Idempotency headerне применяется (операция read-only, повтор безопасен)
Correlationсквозной trace через заголовок traceparent (от сервиса-вызывающего)

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
PropertyObjectIdsbody (JSON)IReadOnlyList<Guid>Данепустой; ≤ 500 элементов (лимит батча); без Guid.Empty; дубли схлопываются без ошибки
ClientIdm2m claimsstringДа (вычисляется)из токена; только разрешённые client-id (catalog, crm)логируется

Модель тела запроса GetPropertyObjectsByIdsRequest:

ПолеТипОбязательностьВалидация
PropertyObjectIdsIReadOnlyList<Guid>Да1..500 элементов, без Guid.Empty

Модель ответа GetPropertyObjectsByIdsResponse

ПолеТипОбязательностьИсточникMaskingОписание
PropertyObjectsIReadOnlyList<InternalPropertyObjectModel>Да (может быть пустым)найденные PropertyObjectEntitynone (system)Снапшоты объектов недвижимости; порядок не гарантирован (потребитель мапит по Id).
NotFoundIdsIReadOnlyList<Guid>Да (может быть пустым)вычисляется: PropertyObjectIds.Distinct() \ PropertyObjects.IdЗапрошенные, но не существующие id (в т.ч. никогда не существовавшие).

Вложенная модель InternalPropertyObjectModel (полная internal-проекция property-object.md)

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаPropertyObject.IdИдентификатор объекта недвижимости.
TenantIdGuidДаPropertyObject.TenantId= DeveloperId (ADR-0052); нужен CRM для стороны застройщика в сделке.
ProjectIdGuidДаPropertyObject.ProjectIdПроект.
BuildingIdGuidДаPropertyObject.BuildingIdЗдание.
LayoutIdGuid?НетPropertyObject.LayoutIdПланировка.
LayoutGroupIdGuid?Нетбатч-загрузка Layout.LayoutGroupIdГруппа планировок (ключ правил rewards, измерений dev-stats).
StatusPropertyObjectStatus (string)ДаPropertyObject.StatusПолный статус, включая Hidden.
StatusSourcePropertyObjectStatusSource (string)ДаPropertyObject.StatusSourceКто выполнил последний переход.
StatusChangedAtDateTimeOffsetДаPropertyObject.StatusChangedAtМомент последнего перехода.
HoldDealIdGuid?НетPropertyObject.HoldDealIdСделка CRM, удерживающая объект недвижимости (guard INV-CHS-003 на стороне CRM).
ReservedUntilDateTimeOffset?НетPropertyObject.ReservedUntilТаймер резерва.
BookedUntilDateTimeOffset?НетPropertyObject.BookedUntilТаймер брони.
SoldAtDateTimeOffset?НетPropertyObject.SoldAtМомент продажи.
NumberstringДаPropertyObject.NumberНомер объекта.
FloorshortДаPropertyObject.FloorЭтаж.
Sectionshort?НетPropertyObject.SectionСекция.
RoomsshortДаPropertyObject.RoomsКомнатность.
TotalAreadecimalДаPropertyObject.TotalAreaОбщая площадь.
LivingAreadecimal?НетPropertyObject.LivingAreaЖилая площадь.
CeilingHeightMetersdecimal?НетPropertyObject.CeilingHeightMeters (без fallback на здание — raw-значение)Высота потолков объекта недвижимости.
PricedecimalДаPropertyObject.PriceТекущая цена (CRM фиксирует цену сделки на своей стороне).
CurrencyIdshortДаPropertyObject.CurrencyIdСсылка на валюту справочника (RUB-only, ADR-0048); CurrencySnapshot для собственных событий с деньгами потребитель строит сам (snapshots.md).
PricePerMeterdecimalДаPropertyObject.PricePerMeterЦена за м².
WindowViewsIReadOnlyList<WindowViewKind> (string)Да (может быть пустым)PropertyObject.WindowViews.ViewsВиды из окон.
Capacityshort?НетPropertyObject.CapacityВместимость.
Descriptionstring?НетPropertyObject.DescriptionОписание.
ExternalTalentPropertyObjectIdGuid?НетPropertyObject.ExternalTalentPropertyObjectIdКлюч реплики Talent.
IsDeletedboolДавычисляется: PropertyObject.DeletedAt != nullSoft-deleted объект недвижимости (потребитель сам исключает из витрин).
UpdatedAtDateTimeOffsetДаPropertyObject.UpdatedAtПоследнее изменение.
VersionlongДаPropertyObject.VersionВерсия агрегата — сверка порядка с событиями pin.chessboard.property-object-* (у событий тот же Version).

Предусловия и постусловия

Предусловия:

  • Вызывающий — доверенный system-актор (catalog/crm) с валидным m2m-токеном; client-id входит в policy ChessboardInternalOnly; endpoint не экспонирован через публичный gateway.
  • PropertyObjectIds непустой, ≤ 500 элементов, без Guid.Empty (дубли допустимы — схлопываются).

Постусловия (read — состояние не меняется):

  • Возвращены снапшоты всех найденных объектов недвижимости (любого статуса, включая Hidden и soft-deleted, с флагами Status/IsDeleted) и список NotFoundIds; объединение множеств {PropertyObjects.Id} ∪ NotFoundIds = входной набор id после дедупликации.
  • Состояние объектов недвижимости не изменено; аудита нет — только лог вызова с clientId.

Кросс-сервисный контракт

  • Предоставляет: полную internal-проекцию InternalPropertyObjectModel без masking, включая Status/StatusSource/HoldDealId/Price/Version. Version — тот же счётчик агрегата, что в событиях pin.chessboard.property-object-*; freshness — read-committed снимок на момент запроса.
  • crm ожидает: актуальные Status/HoldDealId/Price из source of truth в момент создания заявки на бронирование — синхронная точка истины поверх собственной (возможно отстающей) реплики. Guard «release только своей сделкой» (INV-CHS-003) CRM держит на своей стороне по HoldDealId.
  • catalog ожидает: снапшоты для reconciliation витрины после DLQ/replay; применяет правило «большее Version побеждает», чтобы не откатить проекцию более старым снапшотом.
  • Разрыв согласованности: между этим чтением и последующим событием того же объекта возможно окно, в котором событие ещё в пути; конфликт «снимок vs событие» потребитель разрешает по общему Version.

Алгоритм

Батч-чтение: по списку идентификаторов вернуть снапшоты существующих объектов недвижимости и отдельно перечислить ненайденные id. Чтение идёт по всем застройщикам, без tenant- и soft-delete-фильтров и без masking (system-канал).

  1. Контекст и доступ. Вызывает system-актор (catalog/crm) по m2m-токену; разрешён только client-id из списка policy ChessboardInternalOnly (иначе — FORBIDDEN). Tenant- и soft-delete-фильтры сняты — отдаются объекты всех застройщиков, включая Hidden и удалённые (с флагами в ответе).
  2. Проверка входа. PropertyObjectIds непустой, не более 500 элементов, без пустых id; иначе — VALIDATION_ERROR. Дубли схлопываются без ошибки. Отсутствующие id — не ошибка: они попадут в NotFoundIds, метод отдаёт любое состояние объекта by design.
  3. Что читаем. Объекты недвижимости по переданным id (батч по ключу, включая soft-deleted). Для найденных объектов с планировкой — их группы планировок (одна батч-загрузка, для LayoutGroupId, без построчных запросов).
  4. Сборка ответа. Каждый найденный объект проецируется в InternalPropertyObjectModel по таблице модели ответа (поля — прямые значения объекта); LayoutGroupId берётся из группы планировки объекта (null, если планировки нет), IsDeleted — признак soft-delete. NotFoundIds — запрошенные id, для которых объект не найден. Masking не применяется.
  5. Результат. Список снапшотов и список ненайденных id, статус 200 — в том числе когда все id попали в NotFoundIds (это валидный ответ, не 404); нарушение проверки входа → 400.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
UNAUTHORIZED401Нет/невалиден m2m-токен«Unauthorized»no retry (обновить токен)
FORBIDDEN403Client-id не в списке разрешённых policy ChessboardInternalOnly«Forbidden»no retry
VALIDATION_ERROR400PropertyObjectIds пуст / > 500 / содержит Guid.Empty«Некорректный список идентификаторов»no retry
INTERNAL_ERROR500Инфраструктурный сбойgenericretry (backoff; операция read-only)

Совместимость и наблюдаемость

  • Контракт межсервисный: новые optional поля InternalPropertyObjectModel — additive; удаление/переименование/ смена типа, уменьшение лимита батча — breaking (координация с catalog/crm, migration note).
  • Потребители обязаны иметь fallback на unknown enum-значения (PropertyObjectStatus, PropertyObjectStatusSource, WindowViewKind) — правило reference-data.md.
  • Согласованность с событиями: Version в ответе и в pin.chessboard.property-object-*-событиях — один счётчик версии агрегата; потребитель применяет «большее Version побеждает».
  • Logs: clientId, размер батча, NotFoundIds.Count, latency; metrics: chessboard_internal_property_objects_by_ids_requests_total{client}, chessboard_internal_property_objects_by_ids_batch_size (histogram), latency histogram; traces: OTel span chessboard.get-property-objects-by-ids (сквозной trace).
  • Dashboard/runbook — chessboard-overview (README).

Acceptance Criteria

  • AC1 (все id существуют). Дано: валидный client-id и список существующих id. Когда: вызов. Тогда: все объекты в PropertyObjects (включая Hidden/soft-deleted с флагами), NotFoundIds пуст, статус 200.
  • AC2 (частичное совпадение). Дано: часть id не существует. Когда: вызов. Тогда: существующие — в PropertyObjects, отсутствующие — в NotFoundIds, статус 200 (не 404).
  • AC3 (все id отсутствуют). Дано: ни один id не существует. Когда: вызов. Тогда: PropertyObjects пуст, NotFoundIds = весь входной набор, статус 200.
  • AC4 (дедупликация). Дано: список с повторяющимися id. Когда: вызов. Тогда: дубли схлопнуты, каждый объект возвращается один раз, без ошибки.
  • AC5 (нарушение лимита/формата). Дано: > 500 id либо Guid.Empty в списке. Когда: вызов. Тогда: VALIDATION_ERROR (400).
  • AC6 (недоверенный клиент). Дано: client-id вне policy ChessboardInternalOnly. Когда: вызов. Тогда: FORBIDDEN (403); при отсутствующем/невалидном токене — UNAUTHORIZED (401).
  • AC7 (согласованность версий с событиями). Дано: объект получил событие ...upserted/...status-changed с Version = N. Когда: батч-чтение отражает то же изменение. Тогда: возвращаемый Version >= N (общий счётчик), потребитель применяет «большее Version побеждает».

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

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

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

  • Сущности: property-object.md, layout.md, reference-data.md.
  • Асинхронные каналы тех же данных: события pin.chessboard.property-object-upserted.v1, pin.chessboard.property-object-status-changed.v1, pin.chessboard.property-object-price-changed.v1property-object.md «Доменные и интеграционные события».
  • Потребители: docs/06-services/catalog/ (reconciliation витрины), docs/06-services/crm/ (проверка объекта недвижимости при бронировании — будущие документы booking).