POST /api/developer/crm/bookings/{bookingId}/confirm-payment — Подтвердить оплату брони
Менеджер застройщика вручную подтверждает оплату платного бронирования по прикреплённому клиентом чеку:
эквайринг отложен, поэтому статус оплаты ставится не платёжным провайдером, а человеком. Операция закрывает
платный путь брони AwaitingPayment → Booked: сначала фиксирует факт оплаты (PaymentStatus → Paid), затем
подтверждает бронь (→ Booked) в одном изменении. Дом полей агрегата —
booking.md.
Назначение
Принимает от менеджера застройщика {BookingId, ProofDocumentId} и проводит бронь через два доменных перехода:
подтверждение оплаты (INV-BKG-4) → подтверждение брони (INV-BKG-3, платная ветка). Основание — документ-чек
BookingPaymentProof (deal-document.md) в статусе Uploaded и выше. На
выходе — карточка брони. Сделка не мутируется (переход Booking → Sale — отдельная операция StartSale).
INV-BKG-4: платная ветка подтверждения брони выполняется одной атомарной операцией — подтверждение оплаты
(PaymentStatus: Pending → Paid) и подтверждение брони (Status: AwaitingPayment → Booked, INV-BKG-3) пишутся в
одном изменении. Для бесплатной программы переход Reserved → Booked без оплаты делает агент отдельной
операцией — confirm-booking.md.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | developer (JWT менеджера застройщика) |
| Feature group | booking |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/confirm-booking-payment.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика); Operator (Менеджер ПИН — platform-scope, ручное подтверждение) |
| Auth policy / Permissions | crm.bookings.decide (решения застройщика по брони: договор/оплата/продление) |
| Scope (RBAC) | организация (застройщик): record-level по Booking.TenantId |
| Record-level | booking.TenantId = actor.TenantId (менеджер своего застройщика) либо Менеджер ПИН (platform-scope) |
| Tenant isolation | TenantId в запросе = booking.TenantId = DeveloperCompanyId; кросс-tenant запрещён (кроме platform-scope) |
| Audit | update (PaymentConfirmedByUserId/UpdatedByUserId = actor.UserId); реквизиты/бинарь чека в логи не пишутся |
Два допустимых актора: менеджер застройщика (crm.bookings.decide, record-level по booking.TenantId = actor.TenantId — только своя организация) либо Менеджер ПИН (platform-scope, обход record-level, ручное
подтверждение). Кросс-tenant запрос менеджера застройщика — CRM_BOOKING_FORBIDDEN.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/bookings/{bookingId}/confirm-payment |
| Success status | 200 (бронь подтверждена, возвращается карточка брони) |
| Idempotency header | не применяется (естественная защита: повтор на Booked → CRM_BOOKING_INVALID_STATE) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
BookingId | route | Guid | Да | != Guid.Empty; существующая бронь TenantId = актор, Status = AwaitingPayment (шаг 2) | — |
ProofDocumentId | body | Guid | Да | != Guid.Empty; существующий DealDocumentEntity Type = BookingPaymentProof, BookingId = BookingId, Status ∈ {Uploaded, UnderReview, Accepted} (шаг 2–3) | — |
TenantId | identity claims | Guid | Да | Required; = booking.TenantId (DeveloperCompanyId) | Tenant isolation |
ActorUserId | identity claims | Guid | Да | JWT менеджера застройщика; permission crm.bookings.decide | — |
Модель ответа BookingCardModel
Проекция блока брони в карточке сделки застройщика (дельта дома booking.md); PII не содержит, маскирование не требуется.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
BookingId | Guid | Да | BookingEntity.Id | — | Идентификатор брони. |
DealId | Guid | Да | DealId | — | Сделка-владелец (deal.md). |
PropertyObjectId | Guid | Да | PropertyObjectId | — | Объект (логическая ссылка на chessboard). |
Status | string (enum BookingStatus) | Да | Status | — | После операции — Booked. |
InitiatedBy | string (enum BookingInitiator) | Да | InitiatedBy | — | Точка входа брони. |
ProgramName | string | Да | ProgramTerms.Name (snapshot) | — | Программа бронирования. |
PaymentStatus | string (enum BookingPaymentStatus) | Да | PaymentStatus | — | После операции — Paid. |
PaymentAmount | Money (денежная величина — deal.md) | Да | PaymentAmount | — | Подтверждённая сумма (платная программа). |
PaidAt | DateTimeOffset | Да | PaidAt | — | Момент подтверждения оплаты. |
PaymentConfirmedByUserId | Guid | Да | PaymentConfirmedByUserId | — | Менеджер, подтвердивший оплату. |
ContractDocumentId | Guid? | Нет | ContractDocumentId | — | Договор брони (если загружен застройщиком). |
BookedAt | DateTimeOffset | Да | BookedAt | — | Момент подтверждения брони. |
BookingExpiresAt | DateTimeOffset | Да | BookingExpiresAt | — | Дедлайн фазы «Бронь». |
UpdatedAt | DateTimeOffset | Да | UpdatedAt | — | Момент обновления. |
Алгоритм
-
Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.bookings.decide) либо менеджер ПИН (platform-scope, ручное подтверждение); tenant —TenantId = DeveloperCompanyId. Естественная защита от повтора: наBooked→CRM_BOOKING_INVALID_STATE(шаг 2), отдельного ключа идемпотентности нет. -
Проверка входа и предусловий.
BookingId/ProofDocumentId!= Guid.Empty— иначеVALIDATION_FAILED;- бронь существует в tenant застройщика — иначе
CRM_BOOKING_NOT_FOUND; - доступ (record-level):
booking.TenantId == TenantId(менеджер своего застройщика) либо менеджер ПИН — иначеCRM_BOOKING_FORBIDDEN; - терминальность (INV-BKG-8):
booking.Status ∉ {Completed, ReserveExpired, BookingExpired, Cancelled}— иначеCRM_BOOKING_ALREADY_TERMINAL; - состояние допускает оплату (INV-BKG-4):
booking.Status == AwaitingPayment— иначеCRM_BOOKING_INVALID_STATE; - наличие чека (INV-BKG-4): существует документ
Type = BookingPaymentProof,BookingId = BookingId,Status ∈ {Uploaded, UnderReview, Accepted}— иначеCRM_BOOKING_PROOF_REQUIRED.
-
Что читаем. Бронь (для проверки инвариантов и мутации; гонка «worker истекает / менеджер подтверждает» разрешается версией строки — проигравший ретраится с перечиткой). Документ-чек — read-only (этой операцией не мутируется).
-
Бизнес-правила и переходы. На брони последовательно:
- подтверждение оплаты (INV-BKG-4):
PaymentStatus: Pending → Paid;PaidAt = now;PaymentConfirmedByUserId = ActorUserId; фиксируется фактBookingPaid; - подтверждение брони (INV-BKG-3, платная ветка — разрешена при
PaymentStatus == Paid):Status: AwaitingPayment → Booked;BookedAt = now;BookingExpiresAt = BookedAt + ProgramTerms.BookingDurationHours; фиксируется фактBookingConfirmed.
Оба факта пишут таймлайн-события сделки (deal-timeline-event.md).
- подтверждение оплаты (INV-BKG-4):
-
Что меняется. Обе смены статуса — в одной записи брони (audit/
Version += 1); документ-чек не обновляется. Сделка не мутируется (переходBooking → Sale— отдельная операцияStartSale). Конкурентный доступ — по версии строки: если worker перевёл бронь вReserveExpired→ команда фейлится, клиент перечитывает состояние (CRM_BOOKING_ALREADY_TERMINAL). -
События. Публикуются два интеграционных события:
BookingPaidEvent→ топикpin.crm.booking-paid.v1(ContractVersion = 1); payload{BookingId, DealId, TenantId, PropertyObjectId, PaymentAmount {Amount, CurrencyId}, Currency (CurrencySnapshot), ConfirmedByUserId, PaidAt}— потребителиmessenger(карточка «договор оплачен», US-INV-16),rewards(учёт факта оплаты); дом — booking-paid.md;BookingConfirmedEvent→ топикpin.crm.booking-confirmed.v1(ContractVersion = 1); payload{BookingId, DealId, TenantId, PropertyObjectId, ProgramId, BookingExpiresAt, BookedAt}— потребителиchessboard(объект недвижимости → «Бронь» + таймер),catalog,notifications; дом — booking-confirmed.md.
-
Согласованность. Мутация брони, таймлайн-события и оба интеграционных события фиксируются в одном согласованном изменении (атомарно).
-
Результат. Карточка
BookingCardModel(Status = Booked,PaymentStatus = Paid,PaidAt,BookedAt,BookingExpiresAt, …) → HTTP200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | BookingPaidEvent → pin.crm.booking-paid.v1 (messenger/rewards); BookingConfirmedEvent → pin.crm.booking-confirmed.v1 (chessboard/catalog/notifications) — обе в outbox |
| External calls | none (чек — метаданные DealDocumentEntity в БД; бинарь в storage не читается) |
| Cache / projections / search | статус объекта недвижимости в chessboard/catalog — консюмерами booking-confirmed (CRM чужие таблицы не пишет) |
| Notifications | опосредованно: notifications — консюмер booking-confirmed; messenger — карточка «договор оплачен» по booking-paid |
Ошибки и failure modes
Два отказа не сводятся к простой валидации: CRM_BOOKING_PROOF_REQUIRED — к брони не прикреплён чек в статусе
Uploaded+ (нужно сначала attach-booking-payment-proof.md);
CRM_BOOKING_ALREADY_TERMINAL — гонка с worker booking-expiration, успевшим перевести бронь в
ReserveExpired/BookingExpired раньше подтверждения (INV-BKG-8) — менеджер перечитывает состояние.
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | BookingId/ProofDocumentId пустые | «Проверьте корректность запроса» | no retry |
CRM_BOOKING_NOT_FOUND | 404 | бронь не найдена | «Бронирование не найдено» | no retry |
CRM_BOOKING_FORBIDDEN | 403 | booking.TenantId != TenantId актора (чужой застройщик) | «Нет доступа к бронированию» | no retry |
CRM_BOOKING_INVALID_STATE | 409 | Status != AwaitingPayment (INV-BKG-4) | «Оплату можно подтвердить только для брони, ожидающей оплаты» | no retry |
CRM_BOOKING_PROOF_REQUIRED | 409 | нет чека BookingPaymentProof в статусе Uploaded+ для этой брони (INV-BKG-4) | «Прикрепите чек об оплате перед подтверждением» | no retry |
CRM_BOOKING_ALREADY_TERMINAL | 409 | бронь в терминальном статусе (INV-BKG-8; напр. ReserveExpired из-за гонки с worker) | «Бронирование уже завершено или истекло» | no retry (перечитать состояние) |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор — менеджер застройщика брони (или Менеджер ПИН) с permission
crm.bookings.decide; - бронь существует в
TenantId = DeveloperCompanyIdактора, нетерминальна (INV-BKG-8) и в статусеAwaitingPayment(INV-BKG-4); - к броне прикреплён документ-чек
BookingPaymentProofв статусеUploaded/UnderReview/Accepted(INV-BKG-4).
Постусловия (гарантированы после успеха):
- на брони выполнены два перехода в одном изменении:
PaymentStatus: Pending → Paid(PaidAt,PaymentConfirmedByUserId) иStatus: AwaitingPayment → Booked(BookedAt,BookingExpiresAt = BookedAt + ProgramTerms.BookingDurationHours); - записаны таймлайн-события
BookingPaidиBookingConfirmed; в outbox —booking-paid.v1+booking-confirmed.v1; - документ-чек не мутируется; сделка не мутируется (переход
Booking → Sale— отдельная операцияStartSale).
Интеграционная последовательность (после коммита)
Оба события обрабатываются асинхронно после коммита; актор в ответе 200 видит Booked/Paid сразу, а
эффекты соседей доходят с задержкой (окно eventual-consistency — секунды):
BookingPaidEvent → pin.crm.booking-paid.v1(PaymentAmount,ConfirmedByUserId,PaidAt):messenger— карточка «договор оплачен» (US-INV-16);rewards— учёт факта оплаты.BookingConfirmedEvent → pin.crm.booking-confirmed.v1(ProgramId,BookingExpiresAt,BookedAt):chessboard— объект недвижимости → «Бронь» + таймер;catalog— обновление витрины;notifications— уведомление сторон.
Что предоставляет: оба события — additive-only; статус объекта в шахматке/каталоге меняют консюмеры (CRM чужие таблицы не пишет). В промежуточном окне объект ещё не помечен «Бронь» — ожидаемое eventual-consistency состояние.
Acceptance Criteria
- Happy path. Дано: бронь
AwaitingPayment, приложен чекUploaded+. Когда: менеджер подтверждает оплату. Тогда:200,PaymentStatus = Paid,Status = Booked, проставленыPaidAt/BookedAt/BookingExpiresAt, опубликованыbooking-paid.v1+booking-confirmed.v1. - Нет чека. Дано: у брони нет
BookingPaymentProofвUploaded+. Когда: подтверждение. Тогда:409 CRM_BOOKING_PROOF_REQUIRED, изменений нет. - Неверное состояние. Дано: бронь уже
Booked(неAwaitingPayment). Когда: подтверждение. Тогда:409 CRM_BOOKING_INVALID_STATE. - Терминальная бронь (гонка с worker). Дано: worker перевёл бронь в
ReserveExpired. Когда: подтверждение. Тогда:409 CRM_BOOKING_ALREADY_TERMINAL; менеджер перечитывает состояние. - Чужой застройщик. Дано: бронь другого tenant. Когда: подтверждение. Тогда:
403 CRM_BOOKING_FORBIDDEN.
Совместимость и наблюдаемость
- Все поля
BookingCardModelи payload обоих событий — additive optional без breaking change; удаление/ переименование/смена типа — breaking (migration + rollout; версия события — новый.v2+ dual-publish). - Logs:
booking-payment confirmed(BookingId,DealId,TenantId,ProofDocumentId; суммы — без раскрытия сверх необходимого); metrics:crm_booking_payments_confirmed_total, latency p95; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): POST /api/investor/crm/bookings/{bookingId}/payment-proof — Прикрепить чек оплаты брони
- Use Cases (1): UC-BKG-004. Оплата платного бронирования (чек + подтверждение)
- Саги (1): Saga «Booking» (Резерв → Бронь → передача в продажу)
- scenarios (1): UC-OVR-006. Сделка: от брони до регистрации (инвестор-инициатор)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: booking.md (агрегат, INV-BKG-3/4/8, enum-ы,
ProgramTerms,Moneyчерез deal.md), deal-document.md (чекBookingPaymentProof), deal-timeline-event.md (таймлайнBookingPaid/BookingConfirmed). - События: booking-paid.md, booking-confirmed.md; карта области — README.
- ADR: 0052 tenancy, 0056 simplicity-first; эквайринг отложен (оплата подтверждается вручную).