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

UC-BKG-004. Оплата платного бронирования (чек + подтверждение)

Назначение

Закрытие платного бронирования при отложенном эквайринге: инвестор оплачивает бронь вне платформы и прикрепляет чек, а менеджер застройщика подтверждает оплату вручную, переводя бронь AwaitingPayment → Booked. Дом полей — booking.md, deal-document.md; таймлайн — deal-timeline-event.md.

Metadata

ПолеЗначение
IDUC-BKG-004
Контекстcrm (см. docs/06-services/crm/)
Типосновной
Статусdraft
Расхождение US-INV-21

Критерий «платёжный шлюз (карта/СБП)» отложен решением 9 ТЗ — эквайринг не реализуется; оплата вне платформы, чек BookingPaymentProof + ручное подтверждение менеджером.

Актор

РольPermissions / scopeTenantRecord-level
Инвестор (оплата вне платформы, чек)crm.deal-documents.manage / crm.bookings.manage (US-INV-21)null (инвестор — кросс-tenant участник, ADR-0052)deal.PrimaryInvestorUserId = actor (форсится сервером; чужую бронь прикрепить нельзя)
Менеджер застройщика (подтверждение оплаты)crm.bookings.decideTenantId = DeveloperCompanyIdучастник-застройщик сделки (SellManager), booking.TenantId = actor.TenantId
Менеджер ПИНplatform-scope (обход record-level)кросс-tenantне применяется

Предусловие

  • Существует бронь BookingEntity по платной программе (ProgramTerms.IsPaid = true) в статусе AwaitingPayment, PaymentStatus = Pending — путь UC-BKG-002 A1.
  • Договор бронирования (DealDocumentEntity типа BookingContract) загружен застройщиком (Status = Uploaded); инвестор скачал/подписал.
  • Резерв не истёк: now <= ReserveExpiresAt (иначе worker booking-expirationReserveExpired, INV-BKG-5).
  • Инвестор — участник сделки (PrimaryInvestorUserId = actor); менеджер — SellManager своего TenantId.

Триггер

ПолеЗначение
ИсточникUI-действие → вызов API (две фазы: инвестор, затем менеджер застройщика)
Триггерящий элементИнвестор: «Оплатить» / прикрепить чек (/profile/deals/[id]) → attach-booking-payment-proof; Менеджер: «Подтвердить оплату» (/crm/deals/[id]) → confirm-booking-payment

Основной Поток

Happy path: инвестор прикрепляет чек (Фаза 1) → менеджер застройщика подтверждает оплату, бронь становится Booked (Фаза 2).

Фаза 1. Прикрепление чека — attach-booking-payment-proof.md

  1. Вход. Оплатив вне платформы, инвестор вызывает POST /api/investor/crm/bookings/{bookingId}/payment-proof {FileId, FileName, ContentType, SizeBytes}. Охват — record-level по PrimaryInvestorUserId (developer-tenant брони чужой инвестору).
  2. Проверки и инварианты. Бронь существует и deal.PrimaryInvestorUserId = actor (иначе CRM_BOOKING_FORBIDDEN); Status = AwaitingPayment, ProgramTerms.IsPaid, PaymentStatus = Pending; файл валиден (INV-DOC-3: ContentType ∈ {application/pdf, image/jpeg, image/png}, SizeBytes <= 50 МБ).
  3. Создание документа. Загрузка документа (INV-DOC-1: загрузчик типа BookingPaymentProof — инвестор): Type = BookingPaymentProof, Status = Uploaded, BookingId заполнен, Title = «Чек об оплате бронирования». Бронь не мутируется: PaymentStatus остаётся Pending (переход → Paid — только ConfirmPayment, INV-BKG-4). Событие DocumentUploaded → таймлайн-событие DocumentUploaded «чек прикреплён» тем же изменением (INV-DOC-6).
  4. Согласованность. Документ + таймлайн + outbox фиксируются атомарно в одном согласованном изменении.
  5. Публикация. DocumentStatusChangedEventpin.crm.document-status-changed.v1 (notifications менеджеру/инвестору о статусе документа, US-INV-14.3). BookingPaidEvent здесь не эмитится. Результат — Status = Uploaded, HTTP 201.

Фаза 2. Подтверждение оплаты — confirm-booking-payment.md

  1. Вход. Менеджер застройщика вызывает POST /api/developer/crm/bookings/{bookingId}/confirm-payment {ProofDocumentId} (tenanted — TenantId = booking.TenantId = DeveloperCompanyId).
  2. Guard INV-BKG-4. Только менеджер TenantId, только из AwaitingPayment, при наличии чека BookingPaymentProof (BookingId совпадает, Status ∈ {Uploaded, UnderReview, Accepted}) — иначе CRM_BOOKING_INVALID_STATE / CRM_BOOKING_PROOF_REQUIRED. Переход PaymentStatus: Pending → Paid, PaidAt = now, PaymentConfirmedByUserId = actor.
  3. Guard INV-BKG-3 + ConfirmBooking. Тем же изменением подтверждение брони (платная ветка — разрешена при PaymentStatus = Paid): Status: AwaitingPayment → Booked, BookedAt = now, BookingExpiresAt = BookedAt + BookingDurationHours. События BookingPaid и BookingConfirmed → таймлайн-события BookingPaid/BookingConfirmed.
  4. Согласованность. Обе смены статуса брони + таймлайн + две outbox-записи фиксируются атомарно в одном согласованном изменении. Гонка «worker истекает / менеджер подтверждает» разрешается оптимистичной блокировкой: проигравший ретраится и видит ReserveExpired.
  5. Публикация. BookingPaidEventpin.crm.booking-paid.v1 (messenger карточка «договор оплачен», US-INV-16; rewards — учёт факта оплаты); BookingConfirmedEventpin.crm.booking-confirmed.v1 (chessboard объект недвижимости → «Бронь» + таймер до BookingExpiresAt; catalog; notifications). Результат — Status = Booked, PaymentStatus = Paid, HTTP 200.

Диаграмма

Альтернативные Потоки

warning
A1. Чек невалиден или бронь не в AwaitingPayment
  1. Тип/размер файла вне пределов (INV-DOC-3) → VALIDATION_FAILED; бронь не в AwaitingPayment (договор ещё не загружен / бронь завершена) → CRM_BOOKING_INVALID_STATE; бесплатная программа или уже оплачено → CRM_BOOKING_PAYMENT_NOT_REQUIRED.
  2. Инвестор исправляет файл и повторяет attach-booking-payment-proof. Повторная загрузка — новая запись документа (INV-DOC-4, append-only); менеджер выбирает актуальный чек при ConfirmPayment.

Возвращается к Фазе 1 (шаг 1) или завершается ошибкой.

A2. Менеджер подтверждает без чека / чек отсутствует
  1. confirm-booking-payment без прикреплённого BookingPaymentProof (Uploaded+) для этой брони → CRM_BOOKING_PROOF_REQUIRED (INV-BKG-4). Бронь не меняется.

Возвращается к Фазе 2 (шаг 6) после того как инвестор прикрепит чек.

A3. Резерв истёк до подтверждения (worker, UC-BKG-003)
  1. now > ReserveExpiresAt в статусе AwaitingPayment: фоновый polling-worker booking-expiration (INV-BKG-5) переводит бронь AwaitingPayment → ReserveExpired.
  2. Попытка confirm-booking-payment над терминальной бронью → CRM_BOOKING_ALREADY_TERMINAL (INV-BKG-8); BookingExpiredEventpin.crm.booking-expired.v1 уже освободил объект недвижимости. Детали — UC-BKG-003; новая попытка — новый агрегат брони.
A4. Повторные вызовы (идемпотентность фаз)
  1. Повторное confirm-booking-payment (двойной клик / ретрай сети): первый перевёл бронь в Booked, второй видит Status != AwaitingPaymentCRM_BOOKING_INVALID_STATE; повторной оплаты/второго booking-paid нет.
  2. Повторный attach-booking-payment-proof после подтверждения: бронь уже Booked, не AwaitingPaymentCRM_BOOKING_INVALID_STATE; ранее прикреплённый чек остаётся (append-only, INV-DOC-4), новых записей нет.
  3. Повтор в пределах AwaitingPayment (чек ещё не подтверждён) — новая запись документа (A1), бронь не мутируется; менеджер выбирает актуальный ProofDocumentId при подтверждении.

Разрешается ровно один переход → Paid/→ Booked; downstream-события эмитятся один раз.

Постусловие

  • Создан DealDocumentEntity типа BookingPaymentProof (Status = Uploaded), привязанный к брони; бронь при этом не изменена (PaymentStatus = Pending).
  • После подтверждения менеджером: PaymentStatus = Paid (PaidAt, PaymentConfirmedByUserId), бронь Status = Booked (BookedAt, BookingExpiresAt заполнены).
  • В таймлайн добавлены DocumentUploaded, BookingPaid, BookingConfirmed.
  • Опубликованы document-status-changed (Фаза 1), booking-paid и booking-confirmed (Фаза 2); статус объекта недвижимости в chessboard («Бронь» + таймер) синхронизирован консюмером.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
VALIDATION_FAILED400Пустой BookingId/FileId/ProofDocumentId; недопустимый тип/размер чека (INV-DOC-3)Показать ошибки формы
UNAUTHORIZED401Нет/просрочен JWTПовторная авторизация
CRM_BOOKING_NOT_FOUND404Бронь не найденаОбновить карточку сделки
CRM_BOOKING_FORBIDDEN403Чужая бронь: deal.PrimaryInvestorUserId != actor (Фаза 1) / booking.TenantId != actor.TenantId (Фаза 2)Нет доступа
CRM_BOOKING_INVALID_STATE409Status != AwaitingPayment (INV-BKG-4)Перечитать статус брони
CRM_BOOKING_PAYMENT_NOT_REQUIRED409Бесплатная программа / уже оплачено (Фаза 1)Оплата не требуется
CRM_BOOKING_PROOF_REQUIRED409Нет чека BookingPaymentProof в статусе Uploaded+ (INV-BKG-4, Фаза 2)Дождаться прикрепления чека
CRM_DEAL_ALREADY_TERMINAL409Сделка терминальна (INV-DOC-5, Фаза 1)Сделка завершена/отменена
CRM_BOOKING_ALREADY_TERMINAL409Бронь терминальна (INV-BKG-8; напр. ReserveExpired из-за гонки, A3)Оформить бронь заново

Доменные События

СобытиеКогда поднимаетсяSubscribers
DocumentUploadedФаза 1: [*] → Uploaded (чек прикреплён)внутренний диспетчер → таймлайн DocumentUploaded + сборка DocumentStatusChangedEvent
BookingPaidФаза 2: ConfirmPayment (PaymentStatus → Paid)внутренний диспетчер → таймлайн BookingPaid + сборка BookingPaidEvent
BookingConfirmedФаза 2: ConfirmBooking (→ Booked)внутренний диспетчер → таймлайн BookingConfirmed + сборка BookingConfirmedEvent

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
DocumentStatusChangedEventpin.crm.document-status-changed.v1Фаза 1: чек прикреплён (→ Uploaded)notifications (статус документа, US-INV-14.3)
BookingPaidEventpin.crm.booking-paid.v1Фаза 2: ConfirmPayment (PaymentStatus → Paid)messenger (карточка «договор оплачен», US-INV-16), rewards
BookingConfirmedEventpin.crm.booking-confirmed.v1Фаза 2: ConfirmBooking (→ Booked)chessboard (объект недвижимости → «Бронь» + таймер), catalog, notifications

Контракты/payload — document-status-changed.md, booking-paid.md, booking-confirmed.md; booking-paid и booking-confirmed — самостоятельные контракты, кладутся в один outbox-коммит той же транзакции. Публикация — через outbox, атомарно с переходами.

Acceptance Criteria

#КритерийПроверка
AC1attach-booking-payment-proof создаёт DealDocument типа BookingPaymentProof (Uploaded), не мутируя бронь (PaymentStatus остаётся Pending)Integration-тест: assert документа + неизменности брони + document-status-changed в outbox
AC2Прикрепление чужой брони (deal.PrimaryInvestorUserId != actor) → CRM_BOOKING_FORBIDDEN; бронь не в AwaitingPaymentCRM_BOOKING_INVALID_STATEТест record-level и guard статуса
AC3Невалидный тип/размер чека (INV-DOC-3) → VALIDATION_FAILED; повторная загрузка — новая запись (INV-DOC-4)Тест валидации файла + append-only
AC4confirm-booking-payment при наличии чека Uploaded+: PaymentStatus → Paid, затем Status → Booked, BookingExpiresAt заполнен, обе outbox-записи (booking-paid, booking-confirmed) в одной транзакцииТест двойного перехода + assert outbox
AC5Подтверждение без чека → CRM_BOOKING_PROOF_REQUIRED (INV-BKG-4); чужой tenant → CRM_BOOKING_FORBIDDENТест A2 + tenant-guard
AC6Гонка с worker: now > ReserveExpiresAt → бронь ReserveExpired, confirm-paymentCRM_BOOKING_ALREADY_TERMINALТест A3 (гонка с booking-expiration)
AC7В таймлайн добавлены DocumentUploaded (Фаза 1), BookingPaid + BookingConfirmed (Фаза 2)Тест ленты таймлайна сделки

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

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

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