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

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 areadeveloper (JWT менеджера застройщика)
Feature groupbooking
Статусdraft
Документdocs/06-services/crm/api/developer/confirm-booking-payment.md

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

ПроверкаЗначение
Actor typeUser (Менеджер застройщика); Operator (Менеджер ПИН — platform-scope, ручное подтверждение)
Auth policy / Permissionscrm.bookings.decide (решения застройщика по брони: договор/оплата/продление)
Scope (RBAC)организация (застройщик): record-level по Booking.TenantId
Record-levelbooking.TenantId = actor.TenantId (менеджер своего застройщика) либо Менеджер ПИН (platform-scope)
Tenant isolationTenantId в запросе = booking.TenantId = DeveloperCompanyId; кросс-tenant запрещён (кроме platform-scope)
Auditupdate (PaymentConfirmedByUserId/UpdatedByUserId = actor.UserId); реквизиты/бинарь чека в логи не пишутся
Доступ

Два допустимых актора: менеджер застройщика (crm.bookings.decide, record-level по booking.TenantId = actor.TenantId — только своя организация) либо Менеджер ПИН (platform-scope, обход record-level, ручное подтверждение). Кросс-tenant запрос менеджера застройщика — CRM_BOOKING_FORBIDDEN.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/bookings/{bookingId}/confirm-payment
Success status200 (бронь подтверждена, возвращается карточка брони)
Idempotency headerне применяется (естественная защита: повтор на BookedCRM_BOOKING_INVALID_STATE)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
BookingIdrouteGuidДа!= Guid.Empty; существующая бронь TenantId = актор, Status = AwaitingPayment (шаг 2)
ProofDocumentIdbodyGuidДа!= Guid.Empty; существующий DealDocumentEntity Type = BookingPaymentProof, BookingId = BookingId, Status ∈ {Uploaded, UnderReview, Accepted} (шаг 2–3)
TenantIdidentity claimsGuidДаRequired; = booking.TenantId (DeveloperCompanyId)Tenant isolation
ActorUserIdidentity claimsGuidДаJWT менеджера застройщика; permission crm.bookings.decide

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

Проекция блока брони в карточке сделки застройщика (дельта дома booking.md); PII не содержит, маскирование не требуется.

ПолеТипОбязательностьИсточникMaskingОписание
BookingIdGuidДаBookingEntity.IdИдентификатор брони.
DealIdGuidДаDealIdСделка-владелец (deal.md).
PropertyObjectIdGuidДаPropertyObjectIdОбъект (логическая ссылка на chessboard).
Statusstring (enum BookingStatus)ДаStatusПосле операции — Booked.
InitiatedBystring (enum BookingInitiator)ДаInitiatedByТочка входа брони.
ProgramNamestringДаProgramTerms.Name (snapshot)Программа бронирования.
PaymentStatusstring (enum BookingPaymentStatus)ДаPaymentStatusПосле операции — Paid.
PaymentAmountMoney (денежная величина — deal.md)ДаPaymentAmountПодтверждённая сумма (платная программа).
PaidAtDateTimeOffsetДаPaidAtМомент подтверждения оплаты.
PaymentConfirmedByUserIdGuidДаPaymentConfirmedByUserIdМенеджер, подтвердивший оплату.
ContractDocumentIdGuid?НетContractDocumentIdДоговор брони (если загружен застройщиком).
BookedAtDateTimeOffsetДаBookedAtМомент подтверждения брони.
BookingExpiresAtDateTimeOffsetДаBookingExpiresAtДедлайн фазы «Бронь».
UpdatedAtDateTimeOffsetДаUpdatedAtМомент обновления.

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.bookings.decide) либо менеджер ПИН (platform-scope, ручное подтверждение); tenant — TenantId = DeveloperCompanyId. Естественная защита от повтора: на BookedCRM_BOOKING_INVALID_STATE (шаг 2), отдельного ключа идемпотентности нет.

  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.
  3. Что читаем. Бронь (для проверки инвариантов и мутации; гонка «worker истекает / менеджер подтверждает» разрешается версией строки — проигравший ретраится с перечиткой). Документ-чек — read-only (этой операцией не мутируется).

  4. Бизнес-правила и переходы. На брони последовательно:

    • подтверждение оплаты (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).

  5. Что меняется. Обе смены статуса — в одной записи брони (audit/Version += 1); документ-чек не обновляется. Сделка не мутируется (переход Booking → Sale — отдельная операция StartSale). Конкурентный доступ — по версии строки: если worker перевёл бронь в ReserveExpired → команда фейлится, клиент перечитывает состояние (CRM_BOOKING_ALREADY_TERMINAL).

  6. События. Публикуются два интеграционных события:

    • 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.
  7. Согласованность. Мутация брони, таймлайн-события и оба интеграционных события фиксируются в одном согласованном изменении (атомарно).

  8. Результат. Карточка BookingCardModel (Status = Booked, PaymentStatus = Paid, PaidAt, BookedAt, BookingExpiresAt, …) → HTTP 200.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsBookingPaidEventpin.crm.booking-paid.v1 (messenger/rewards); BookingConfirmedEventpin.crm.booking-confirmed.v1 (chessboard/catalog/notifications) — обе в outbox
External callsnone (чек — метаданные 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_FAILED400BookingId/ProofDocumentId пустые«Проверьте корректность запроса»no retry
CRM_BOOKING_NOT_FOUND404бронь не найдена«Бронирование не найдено»no retry
CRM_BOOKING_FORBIDDEN403booking.TenantId != TenantId актора (чужой застройщик)«Нет доступа к бронированию»no retry
CRM_BOOKING_INVALID_STATE409Status != AwaitingPayment (INV-BKG-4)«Оплату можно подтвердить только для брони, ожидающей оплаты»no retry
CRM_BOOKING_PROOF_REQUIRED409нет чека BookingPaymentProof в статусе Uploaded+ для этой брони (INV-BKG-4)«Прикрепите чек об оплате перед подтверждением»no retry
CRM_BOOKING_ALREADY_TERMINAL409бронь в терминальном статусе (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 — секунды):

  1. BookingPaidEvent → pin.crm.booking-paid.v1 (PaymentAmount, ConfirmedByUserId, PaidAt): messenger — карточка «договор оплачен» (US-INV-16); rewards — учёт факта оплаты.
  2. 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; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь).

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

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

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