POST /api/agent/crm/bookings/{bookingId}/confirm — Подтвердить бронь (бесплатная программа)
Агентская сторона переводит бронь по бесплатной программе из Резерва в Бронь до истечения резерва
(US-INV-06/10). Переход брони Reserved → Booked (INV-BKG-3). Для платной программы этот путь недоступен:
подтверждение идёт через ConfirmPayment менеджера застройщика (area developer), где оплата и перевод в
Booked выполняются одной операцией. Дом полей брони, статусы и инварианты — в booking.md.
Назначение
Дать агенту закрепить объект за сделкой по бесплатной программе бронирования
(booking-program.md): пока идёт отсчёт ReserveExpiresAt, агент подтверждает
бронь, и объект переходит из «Резерв» в «Бронь» с новым таймером BookingExpiresAt. Смену статуса объекта в
шахматке (Резерв → Бронь) выполняет консюмер chessboard по интеграционному событию — CRM чужие таблицы не
пишет (service-boundaries.md).
INV-BKG-3: агентская сторона подтверждает только бесплатную программу (Reserved → Booked). Для платной
программы этот метод недоступен и возвращает CRM_BOOKING_PAYMENT_REQUIRED — перевод в Booked там выполняет
ConfirmPayment менеджера застройщика (area developer) одной операцией с оплатой.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent |
| Feature group | booking |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/confirm-booking.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User — ответственный агент, руководитель агентства или Менеджер ПИН |
| Auth policy / Permissions | crm.bookings.manage (агентская сторона); Менеджер ПИН — platform-scope |
| Scope (RBAC) | организация (агентство): record-level по OriginAgencyCompanyId сделки |
| Record-level | по участникам сделки (deal.md): рядовой агент — активный участник (обычно Executor: Deal.ExecutorAgentUserId = actor.UserId); руководитель — любая сделка своего Deal.OriginAgencyCompanyId; Менеджер ПИН — обход record-level |
| Tenant isolation | Бронь TenantId = DeveloperCompanyId (процесс на объекте застройщика, ADR-0052). Агентская команда выполняется по явному record-level предикату по сделке, а не по авто-tenant-фильтру |
| Audit | update: переход Reserved → Booked, поля BookedAt, BookingExpiresAt, UpdatedByUserId, Version |
Permission crm.bookings.manage проверяется record-level предикатом по сделке, а не авто-tenant-фильтром:
рядовой агент — активный Executor сделки (Deal.ExecutorAgentUserId = actor.UserId), руководитель агентства —
любая сделка своего Deal.OriginAgencyCompanyId, Менеджер ПИН — обход record-level (platform-scope). Сама бронь
лежит в tenant застройщика (TenantId = DeveloperCompanyId, ADR-0052) — доступ агентской стороны к ней даёт
именно предикат по сделке.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/agent/crm/bookings/{bookingId}/confirm |
| Success status | 200 (возвращает карточку брони в статусе Booked) |
| Idempotency header | не применяется — повтор ловится проверкой состояния: вторая confirm уже Booked/терминальной брони → CRM_BOOKING_INVALID_STATE / CRM_BOOKING_ALREADY_TERMINAL |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
BookingId | route | Guid | Да | != Guid.Empty; существующая бронь | — |
ActorUserId | identity claims | Guid | Да | из JWT | — |
ActorAgencyCompanyId | identity claims | Guid | Да | record-level предикат (сверяется с Deal.OriginAgencyCompanyId) | — |
IsAgencyHead | identity claims | bool | Да | флаг «руководитель агентства» (доступ ко всем агентам своего AgencyCompanyId) | — |
IsPinManager | identity claims | bool | Да | platform-scope Менеджера ПИН (обход record-level) | — |
Тело запроса пустое. Версия строки клиентом не передаётся — гонка «worker истекает / агент подтверждает» решается сервером оптимистически.
Модель ответа BookingCardModel
Типизированная проекция брони после перехода (дом полей — booking.md; здесь — только состав карточки). PII не содержит.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
BookingId | Guid | Да | BookingEntity.Id | — | Идентификатор брони. |
DealId | Guid | Да | BookingEntity.DealId | — | Сделка-владелец. |
PropertyObjectId | Guid | Да | BookingEntity.PropertyObjectId | — | Объект шахматки. |
ProgramId | Guid | Да | BookingEntity.ProgramId | — | Программа бронирования. |
ProgramName | string | Да | ProgramTerms.Name | — | Название программы (snapshot). |
IsPaid | bool | Да | ProgramTerms.IsPaid | — | Платность программы (здесь всегда false). |
Status | BookingStatus | Да | BookingEntity.Status | — | Статус строкой (Booked после операции). |
PaymentStatus | BookingPaymentStatus | Да | BookingEntity.PaymentStatus | — | Строкой (NotRequired). |
ReserveExpiresAt | DateTimeOffset | Да | BookingEntity.ReserveExpiresAt | — | Дедлайн резерва (истёкший на момент Booked). |
BookedAt | DateTimeOffset | Да | BookingEntity.BookedAt | — | Момент подтверждения. |
BookingExpiresAt | DateTimeOffset | Да | BookingEntity.BookingExpiresAt | — | Новый дедлайн фазы «Бронь». |
Алгоритм
- Контекст и доступ. Операцию выполняет ответственный агент / руководитель агентства / Менеджер ПИН
(permission
crm.bookings.manage). - Проверка входа и предусловий.
- формат:
BookingId != Guid.Empty— иначе ошибка валидации; - бронь существует — иначе
CRM_BOOKING_NOT_FOUND(404); - допустимость программы: программа бесплатная (
ProgramTerms.IsPaid = false); для платной →CRM_BOOKING_PAYMENT_REQUIRED(409) — платная подтверждаетсяConfirmPaymentзастройщика (INV-BKG-3, событие оплаты — booking-paid.md); - допустимость состояния:
Status = Reservedиnow <= ReserveExpiresAt(проверяются переходом); - право актора на бронь по участникам сделки — иначе
CRM_BOOKING_FORBIDDEN.
- формат:
- Что читаем. Бронь (по явному предикату, у актора нет tenant застройщика) и её сделку (для record-level:
агент/агентство). Статус сделки не проверяется (сделка уже
Bookingс создания брони, INV-BKG-2). - Бизнес-правила и переходы. Право актора: Менеджер ПИН, руководитель агентства при совпавшем
OriginAgencyCompanyIdили ответственный агент (активныйExecutorсделки); иначеCRM_BOOKING_FORBIDDEN(403). ПереходReserved → Booked(guardStatus == Reserved; терминальные статусы иммутабельны, INV-BKG-8 →CRM_BOOKING_ALREADY_TERMINAL; иной активный статус →CRM_BOOKING_INVALID_STATE; для бесплатной программы guardnow <= ReserveExpiresAt, иначеCRM_BOOKING_RESERVE_EXPIRED, INV-BKG-3):BookedAt = now,BookingExpiresAt = BookedAt + ProgramTerms.BookingDurationHours,UpdatedByUserId = actor.PaymentStatusостаётсяNotRequired. В таймлайн сделки пишется запись подтверждения. - Что меняется. Бронь →
Bookedс новым таймером — в одном согласованном изменении. - События. Интеграционное
BookingConfirmedEvent→ топикpin.crm.booking-confirmed.v1(ContractVersion = 1,AggregateId = BookingId,AggregateType = "Booking"), payload{BookingId, DealId, TenantId, PropertyObjectId, ProgramId, BookingExpiresAt, BookedAt}— transactional outbox; контракт — booking-confirmed.md. Консюмеры:chessboard(объект → «Бронь» + таймер доBookingExpiresAt),catalog,notifications. - Результат. Карточка подтверждённой брони (
Status = Booked,BookedAt,BookingExpiresAt) → HTTP200. Если workerbooking-expirationистёк резерв параллельно — конкурентный конфликт; при перечитке бронь ужеReserveExpired→CRM_BOOKING_RESERVE_EXPIRED(409); объект вернулся в «Свободен», агент заводит новую бронь (create-booking-request.md).
Диаграмма
Предусловия и постусловия
Предусловия. Актор — ответственный агент / руководитель агентства / Менеджер ПИН с crm.bookings.manage.
Бронь существует, Status = Reserved, now <= ReserveExpiresAt. Программа бесплатная
(ProgramTerms.IsPaid = false) — платная подтверждается менеджером застройщика через ConfirmPayment.
Постусловие (успех). Бронь Reserved → Booked: BookedAt = now, BookingExpiresAt = BookedAt + BookingDurationHours, PaymentStatus остаётся NotRequired; в таймлайне сделки — запись подтверждения. Сделка
статуса не меняет (уже Booking с создания брони, INV-BKG-2). Опубликован BookingConfirmedEvent (key
BookingId) — потребители chessboard (объект «Резерв → Бронь» + таймер до BookingExpiresAt), catalog,
notifications. Таймаут-драйвер переключается: worker перестаёт следить за ReserveExpiresAt и начинает — за
BookingExpiresAt. Окно eventual-consistency: объект в шахматке переходит в «Бронь» асинхронно.
Постусловие (отказ). Бронь не изменена, событие не опубликовано.
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | BookingConfirmedEvent → pin.crm.booking-confirmed.v1 (outbox); контракт — booking-confirmed.md |
| Внешние вызовы | none (статус объекта меняет консюмер chessboard по событию, не синхронно) |
| Проекции / поиск | бронь выпадает из выборки worker-а по reserve_expires_at (переход из Reserved), попадает в выборку по booking_expires_at (Status = Booked) |
| Уведомления | опосредованно: консюмер booking-confirmed в notifications (участникам сделки «бронь подтверждена») |
Ошибки и failure modes
Два отказа не сводятся к простой валидации формата: CRM_BOOKING_PAYMENT_REQUIRED — бронь по платной программе
(путь — ConfirmPayment застройщика); CRM_BOOKING_RESERVE_EXPIRED — резерв истёк раньше подтверждения (в т.ч.
worker booking-expiration победил в гонке) — объект уже освобождён, нужна новая бронь.
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | BookingId = Guid.Empty | «Некорректный идентификатор брони» | no retry |
CRM_BOOKING_NOT_FOUND | 404 | брони нет или она вне scope актора | «Бронь не найдена» | no retry |
CRM_BOOKING_FORBIDDEN | 403 | актор не участник агентской стороны сделки | «Недостаточно прав для подтверждения брони» | no retry |
CRM_BOOKING_PAYMENT_REQUIRED | 409 | платная программа (ProgramTerms.IsPaid) — путь ConfirmPayment застройщика (INV-BKG-3) | «Платная бронь подтверждается после оплаты застройщиком» | no retry |
CRM_BOOKING_RESERVE_EXPIRED | 409 | now > ReserveExpiresAt (в т.ч. worker истёк резерв) | «Время резерва истекло, оформите бронь заново» | no retry |
CRM_BOOKING_INVALID_STATE | 409 | Status ∉ {Reserved} (уже Booked/AwaitingPayment) | «Бронь нельзя подтвердить в текущем статусе» | no retry |
CRM_BOOKING_ALREADY_TERMINAL | 409 | бронь в терминальном статусе (INV-BKG-8) | «Бронь завершена» | no retry |
Совместимость и наблюдаемость
- Контракт: route +
BookingCardModel; новые optional поля карточки — additive, без breaking change; смена маршрута/семантики статуса — breaking (migration + rollout). Enum-поля (Status,PaymentStatus) сериализуются строкой; unknown-значения клиент трактует как «неизвестный статус». - Logs:
booking-confirm requested/confirmed/reserve-expired/conflict(BookingId,DealId,TenantId,ActorUserId; PII не пишется); metrics:crm_booking_confirm_total{result}, latency p95; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Acceptance Criteria
- Happy path. Given
Reserved-бронь бесплатной программы,now <= ReserveExpiresAt; Whenconfirm; Then200,Booked,BookingExpiresAtпроставлен, опубликованbooking-confirmed. - Платная программа. Given
ProgramTerms.IsPaid = true; Whenconfirm; Then409 CRM_BOOKING_PAYMENT_REQUIRED(путьConfirmPaymentзастройщика). - Резерв истёк. Given
now > ReserveExpiresAt(или worker уже истёк резерв); Whenconfirm; Then409 CRM_BOOKING_RESERVE_EXPIRED, объект уже освобождён. - Повторное подтверждение. Given бронь уже
Booked/AwaitingPayment; Whenconfirm; Then409 CRM_BOOKING_INVALID_STATE. - Терминальная бронь. Given
Statusтерминальный (INV-BKG-8); Whenconfirm; Then409 CRM_BOOKING_ALREADY_TERMINAL. - Гонка worker/агент. Given агент подтверждает в момент истечения резерва; Then побеждает один из процессов;
проигравший агент получает
CRM_BOOKING_RESERVE_EXPIRED. - Доступ. Given актор не участник агентской стороны сделки; When
confirm; Then403 CRM_BOOKING_FORBIDDEN.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): PUT /api/investor/crm/bookings/{bookingId}/requisites — Выбрать реквизиты для брони
- Use Cases (2): UC-BKG-002. Бронирование Резерв → Бронь, UC-BKG-003. Истечение резерва/брони и продление
- Саги (1): Saga «Booking» (Резерв → Бронь → передача в продажу)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей, статусы, INV-BKG-3/5/8, метод
ConfirmBooking): booking.md; условия и таймеры: booking-program.md; родительский процесс: deal.md. - Событие перехода: booking-confirmed.md; контраст платного пути: booking-paid.md; истечение резерва/брони worker-ом: booking-expired.md.
- Старт процесса (Резерв): create-booking-request.md; карта событий — ../../README.md.