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).
Список id не помещается в query string (до 500 элементов), поэтому метод — POST. При этом он
безопасен и идемпотентен: тело запроса не мутирует состояние (только чтение).
Ответ отдаётся без masking (system-to-system) и включает Hidden- и soft-deleted-объекты
недвижимости с явными флагами — потребитель сам решает, что делать с невидимыми публично записями.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| API area | internal |
| Feature group | propertyObjects |
| Статус | draft |
| Owner | PIN Platform Core |
| Документ | docs/06-services/chessboard/api/internal/property-objects/get-property-objects-by-ids.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | System — m2m-клиент сервисов PIN (catalog, crm) |
| Auth policy / Permissions | m2m-токен identity (client credentials), policy ChessboardInternalOnly; список разрешённых client-id — конфигурация; internal area не экспонируется через публичный gateway |
| Scope (RBAC) | не применяется (платформенное system-чтение) |
| Record-level | не применяется |
| Tenant isolation | Bypass tenant- и soft-delete-фильтров — system-actor читает объекты недвижимости всех застройщиков, включая Hidden/удалённые (флаги в ответе) |
| Audit | none (read; вызовы логируются с client-id) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST (read-only batch lookup; тело — список id) |
| Route | /api/v1/internal/property-objects/by-ids |
| Success status | 200 |
| Idempotency header | не применяется (операция read-only, повтор безопасен) |
| Correlation | сквозной trace через заголовок traceparent (от сервиса-вызывающего) |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
PropertyObjectIds | body (JSON) | IReadOnlyList<Guid> | Да | непустой; ≤ 500 элементов (лимит батча); без Guid.Empty; дубли схлопываются без ошибки | — |
ClientId | m2m claims | string | Да (вычисляется) | из токена; только разрешённые client-id (catalog, crm) | логируется |
Модель тела запроса GetPropertyObjectsByIdsRequest:
| Поле | Тип | Обязательность | Валидация |
|---|---|---|---|
PropertyObjectIds | IReadOnlyList<Guid> | Да | 1..500 элементов, без Guid.Empty |
Модель ответа GetPropertyObjectsByIdsResponse
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
PropertyObjects | IReadOnlyList<InternalPropertyObjectModel> | Да (может быть пустым) | найденные PropertyObjectEntity | none (system) | Снапшоты объектов недвижимости; порядок не гарантирован (потребитель мапит по Id). |
NotFoundIds | IReadOnlyList<Guid> | Да (может быть пустым) | вычисляется: PropertyObjectIds.Distinct() \ PropertyObjects.Id | — | Запрошенные, но не существующие id (в т.ч. никогда не существовавшие). |
Вложенная модель InternalPropertyObjectModel (полная internal-проекция property-object.md)
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PropertyObject.Id | — | Идентификатор объекта недвижимости. |
TenantId | Guid | Да | PropertyObject.TenantId | — | = DeveloperId (ADR-0052); нужен CRM для стороны застройщика в сделке. |
ProjectId | Guid | Да | PropertyObject.ProjectId | — | Проект. |
BuildingId | Guid | Да | PropertyObject.BuildingId | — | Здание. |
LayoutId | Guid? | Нет | PropertyObject.LayoutId | — | Планировка. |
LayoutGroupId | Guid? | Нет | батч-загрузка Layout.LayoutGroupId | — | Группа планировок (ключ правил rewards, измерений dev-stats). |
Status | PropertyObjectStatus (string) | Да | PropertyObject.Status | — | Полный статус, включая Hidden. |
StatusSource | PropertyObjectStatusSource (string) | Да | PropertyObject.StatusSource | — | Кто выполнил последний переход. |
StatusChangedAt | DateTimeOffset | Да | PropertyObject.StatusChangedAt | — | Момент последнего перехода. |
HoldDealId | Guid? | Нет | PropertyObject.HoldDealId | — | Сделка CRM, удерживающая объект недвижимости (guard INV-CHS-003 на стороне CRM). |
ReservedUntil | DateTimeOffset? | Нет | PropertyObject.ReservedUntil | — | Таймер резерва. |
BookedUntil | DateTimeOffset? | Нет | PropertyObject.BookedUntil | — | Таймер брони. |
SoldAt | DateTimeOffset? | Нет | PropertyObject.SoldAt | — | Момент продажи. |
Number | string | Да | PropertyObject.Number | — | Номер объекта. |
Floor | short | Да | PropertyObject.Floor | — | Этаж. |
Section | short? | Нет | PropertyObject.Section | — | Секция. |
Rooms | short | Да | PropertyObject.Rooms | — | Комнатность. |
TotalArea | decimal | Да | PropertyObject.TotalArea | — | Общая площадь. |
LivingArea | decimal? | Нет | PropertyObject.LivingArea | — | Жилая площадь. |
CeilingHeightMeters | decimal? | Нет | PropertyObject.CeilingHeightMeters (без fallback на здание — raw-значение) | — | Высота потолков объекта недвижимости. |
Price | decimal | Да | PropertyObject.Price | — | Текущая цена (CRM фиксирует цену сделки на своей стороне). |
CurrencyId | short | Да | PropertyObject.CurrencyId | — | Ссылка на валюту справочника (RUB-only, ADR-0048); CurrencySnapshot для собственных событий с деньгами потребитель строит сам (snapshots.md). |
PricePerMeter | decimal | Да | PropertyObject.PricePerMeter | — | Цена за м². |
WindowViews | IReadOnlyList<WindowViewKind> (string) | Да (может быть пустым) | PropertyObject.WindowViews.Views | — | Виды из окон. |
Capacity | short? | Нет | PropertyObject.Capacity | — | Вместимость. |
Description | string? | Нет | PropertyObject.Description | — | Описание. |
ExternalTalentPropertyObjectId | Guid? | Нет | PropertyObject.ExternalTalentPropertyObjectId | — | Ключ реплики Talent. |
IsDeleted | bool | Да | вычисляется: PropertyObject.DeletedAt != null | — | Soft-deleted объект недвижимости (потребитель сам исключает из витрин). |
UpdatedAt | DateTimeOffset | Да | PropertyObject.UpdatedAt | — | Последнее изменение. |
Version | long | Да | PropertyObject.Version | — | Версия агрегата — сверка порядка с событиями pin.chessboard.property-object-* (у событий тот же Version). |
Предусловия и постусловия
Предусловия:
- Вызывающий — доверенный system-актор (
catalog/crm) с валидным m2m-токеном; client-id входит в policyChessboardInternalOnly; 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-канал).
- Контекст и доступ. Вызывает system-актор (
catalog/crm) по m2m-токену; разрешён только client-id из списка policyChessboardInternalOnly(иначе —FORBIDDEN). Tenant- и soft-delete-фильтры сняты — отдаются объекты всех застройщиков, включаяHiddenи удалённые (с флагами в ответе). - Проверка входа.
PropertyObjectIdsнепустой, не более 500 элементов, без пустых id; иначе —VALIDATION_ERROR. Дубли схлопываются без ошибки. Отсутствующие id — не ошибка: они попадут вNotFoundIds, метод отдаёт любое состояние объекта by design. - Что читаем. Объекты недвижимости по переданным id (батч по ключу, включая soft-deleted). Для найденных
объектов с планировкой — их группы планировок (одна батч-загрузка, для
LayoutGroupId, без построчных запросов). - Сборка ответа. Каждый найденный объект проецируется в
InternalPropertyObjectModelпо таблице модели ответа (поля — прямые значения объекта);LayoutGroupIdберётся из группы планировки объекта (null, если планировки нет),IsDeleted— признак soft-delete.NotFoundIds— запрошенные id, для которых объект не найден. Masking не применяется. - Результат. Список снапшотов и список ненайденных id, статус
200— в том числе когда все id попали вNotFoundIds(это валидный ответ, не404); нарушение проверки входа →400.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
UNAUTHORIZED | 401 | Нет/невалиден m2m-токен | «Unauthorized» | no retry (обновить токен) |
FORBIDDEN | 403 | Client-id не в списке разрешённых policy ChessboardInternalOnly | «Forbidden» | no retry |
VALIDATION_ERROR | 400 | PropertyObjectIds пуст / > 500 / содержит Guid.Empty | «Некорректный список идентификаторов» | no retry |
INTERNAL_ERROR | 500 | Инфраструктурный сбой | generic | retry (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 spanchessboard.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побеждает».
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: InvestorPropertyEntity
- Use Cases (1): UC-OWN-002. Завершение сделки в CRM → купленный объект в «Мои объекты недвижимости» (system)
- Консюмеры (1): Consumer: синхронизация из Talent Owners (pin-investor-owner-talent-owners-sync)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: 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.v1— property-object.md «Доменные и интеграционные события». - Потребители:
docs/06-services/catalog/(reconciliation витрины),docs/06-services/crm/(проверка объекта недвижимости при бронировании — будущие документы booking).