UC-BKG-004. Оплата платного бронирования (чек + подтверждение)
Назначение
Закрытие платного бронирования при отложенном эквайринге: инвестор оплачивает бронь вне платформы и
прикрепляет чек, а менеджер застройщика подтверждает оплату вручную, переводя бронь
AwaitingPayment → Booked. Дом полей — booking.md, deal-document.md;
таймлайн — deal-timeline-event.md.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-BKG-004 |
| Контекст | crm (см. docs/06-services/crm/) |
| Тип | основной |
| Статус | draft |
Критерий «платёжный шлюз (карта/СБП)» отложен решением 9 ТЗ — эквайринг не реализуется; оплата вне платформы,
чек BookingPaymentProof + ручное подтверждение менеджером.
Актор
| Роль | Permissions / scope | Tenant | Record-level |
|---|---|---|---|
| Инвестор (оплата вне платформы, чек) | crm.deal-documents.manage / crm.bookings.manage (US-INV-21) | null (инвестор — кросс-tenant участник, ADR-0052) | deal.PrimaryInvestorUserId = actor (форсится сервером; чужую бронь прикрепить нельзя) |
| Менеджер застройщика (подтверждение оплаты) | crm.bookings.decide | TenantId = 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(иначе workerbooking-expiration→ReserveExpired, 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
- Вход. Оплатив вне платформы, инвестор вызывает
POST /api/investor/crm/bookings/{bookingId}/payment-proof{FileId, FileName, ContentType, SizeBytes}. Охват — record-level поPrimaryInvestorUserId(developer-tenant брони чужой инвестору). - Проверки и инварианты. Бронь существует и
deal.PrimaryInvestorUserId = actor(иначеCRM_BOOKING_FORBIDDEN);Status = AwaitingPayment,ProgramTerms.IsPaid,PaymentStatus = Pending; файл валиден (INV-DOC-3:ContentType ∈ {application/pdf, image/jpeg, image/png},SizeBytes <= 50 МБ). - Создание документа. Загрузка документа (INV-DOC-1: загрузчик типа
BookingPaymentProof— инвестор):Type = BookingPaymentProof,Status = Uploaded,BookingIdзаполнен,Title = «Чек об оплате бронирования». Бронь не мутируется:PaymentStatusостаётсяPending(переход→ Paid— толькоConfirmPayment, INV-BKG-4). СобытиеDocumentUploaded→ таймлайн-событиеDocumentUploaded«чек прикреплён» тем же изменением (INV-DOC-6). - Согласованность. Документ + таймлайн + outbox фиксируются атомарно в одном согласованном изменении.
- Публикация.
DocumentStatusChangedEvent→pin.crm.document-status-changed.v1(notificationsменеджеру/инвестору о статусе документа, US-INV-14.3).BookingPaidEventздесь не эмитится. Результат —Status = Uploaded, HTTP201.
Фаза 2. Подтверждение оплаты — confirm-booking-payment.md
- Вход. Менеджер застройщика вызывает
POST /api/developer/crm/bookings/{bookingId}/confirm-payment{ProofDocumentId}(tenanted —TenantId = booking.TenantId = DeveloperCompanyId). - 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. - Guard INV-BKG-3 + ConfirmBooking. Тем же изменением подтверждение брони (платная ветка — разрешена при
PaymentStatus = Paid):Status: AwaitingPayment → Booked,BookedAt = now,BookingExpiresAt = BookedAt + BookingDurationHours. СобытияBookingPaidиBookingConfirmed→ таймлайн-событияBookingPaid/BookingConfirmed. - Согласованность. Обе смены статуса брони + таймлайн + две outbox-записи фиксируются атомарно в одном
согласованном изменении. Гонка «worker истекает / менеджер подтверждает» разрешается оптимистичной
блокировкой: проигравший ретраится и видит
ReserveExpired. - Публикация.
BookingPaidEvent→pin.crm.booking-paid.v1(messengerкарточка «договор оплачен», US-INV-16;rewards— учёт факта оплаты);BookingConfirmedEvent→pin.crm.booking-confirmed.v1(chessboardобъект недвижимости → «Бронь» + таймер доBookingExpiresAt;catalog;notifications). Результат —Status = Booked,PaymentStatus = Paid, HTTP200.
Диаграмма
Альтернативные Потоки
AwaitingPayment- Тип/размер файла вне пределов (INV-DOC-3) →
VALIDATION_FAILED; бронь не вAwaitingPayment(договор ещё не загружен / бронь завершена) →CRM_BOOKING_INVALID_STATE; бесплатная программа или уже оплачено →CRM_BOOKING_PAYMENT_NOT_REQUIRED. - Инвестор исправляет файл и повторяет
attach-booking-payment-proof. Повторная загрузка — новая запись документа (INV-DOC-4, append-only); менеджер выбирает актуальный чек приConfirmPayment.
Возвращается к Фазе 1 (шаг 1) или завершается ошибкой.
confirm-booking-paymentбез прикреплённогоBookingPaymentProof(Uploaded+) для этой брони →CRM_BOOKING_PROOF_REQUIRED(INV-BKG-4). Бронь не меняется.
Возвращается к Фазе 2 (шаг 6) после того как инвестор прикрепит чек.
now > ReserveExpiresAtв статусеAwaitingPayment: фоновый polling-workerbooking-expiration(INV-BKG-5) переводит броньAwaitingPayment → ReserveExpired.- Попытка
confirm-booking-paymentнад терминальной бронью →CRM_BOOKING_ALREADY_TERMINAL(INV-BKG-8);BookingExpiredEvent→pin.crm.booking-expired.v1уже освободил объект недвижимости. Детали — UC-BKG-003; новая попытка — новый агрегат брони.
- Повторное
confirm-booking-payment(двойной клик / ретрай сети): первый перевёл бронь вBooked, второй видитStatus != AwaitingPayment→CRM_BOOKING_INVALID_STATE; повторной оплаты/второгоbooking-paidнет. - Повторный
attach-booking-payment-proofпосле подтверждения: бронь ужеBooked, неAwaitingPayment→CRM_BOOKING_INVALID_STATE; ранее прикреплённый чек остаётся (append-only, INV-DOC-4), новых записей нет. - Повтор в пределах
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_FAILED | 400 | Пустой BookingId/FileId/ProofDocumentId; недопустимый тип/размер чека (INV-DOC-3) | Показать ошибки формы |
UNAUTHORIZED | 401 | Нет/просрочен JWT | Повторная авторизация |
CRM_BOOKING_NOT_FOUND | 404 | Бронь не найдена | Обновить карточку сделки |
CRM_BOOKING_FORBIDDEN | 403 | Чужая бронь: deal.PrimaryInvestorUserId != actor (Фаза 1) / booking.TenantId != actor.TenantId (Фаза 2) | Нет доступа |
CRM_BOOKING_INVALID_STATE | 409 | Status != AwaitingPayment (INV-BKG-4) | Перечитать статус брони |
CRM_BOOKING_PAYMENT_NOT_REQUIRED | 409 | Бесплатная программа / уже оплачено (Фаза 1) | Оплата не требуется |
CRM_BOOKING_PROOF_REQUIRED | 409 | Нет чека BookingPaymentProof в статусе Uploaded+ (INV-BKG-4, Фаза 2) | Дождаться прикрепления чека |
CRM_DEAL_ALREADY_TERMINAL | 409 | Сделка терминальна (INV-DOC-5, Фаза 1) | Сделка завершена/отменена |
CRM_BOOKING_ALREADY_TERMINAL | 409 | Бронь терминальна (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 |
|---|---|---|---|
DocumentStatusChangedEvent | pin.crm.document-status-changed.v1 | Фаза 1: чек прикреплён (→ Uploaded) | notifications (статус документа, US-INV-14.3) |
BookingPaidEvent | pin.crm.booking-paid.v1 | Фаза 2: ConfirmPayment (PaymentStatus → Paid) | messenger (карточка «договор оплачен», US-INV-16), rewards |
BookingConfirmedEvent | pin.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
| # | Критерий | Проверка |
|---|---|---|
| AC1 | attach-booking-payment-proof создаёт DealDocument типа BookingPaymentProof (Uploaded), не мутируя бронь (PaymentStatus остаётся Pending) | Integration-тест: assert документа + неизменности брони + document-status-changed в outbox |
| AC2 | Прикрепление чужой брони (deal.PrimaryInvestorUserId != actor) → CRM_BOOKING_FORBIDDEN; бронь не в AwaitingPayment → CRM_BOOKING_INVALID_STATE | Тест record-level и guard статуса |
| AC3 | Невалидный тип/размер чека (INV-DOC-3) → VALIDATION_FAILED; повторная загрузка — новая запись (INV-DOC-4) | Тест валидации файла + append-only |
| AC4 | confirm-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-payment → CRM_BOOKING_ALREADY_TERMINAL | Тест A3 (гонка с booking-expiration) |
| AC7 | В таймлайн добавлены DocumentUploaded (Фаза 1), BookingPaid + BookingConfirmed (Фаза 2) | Тест ленты таймлайна сделки |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (3): UC-BKG-002. Бронирование Резерв → Бронь, UC-CRM-005. Оформление сделки: договоры → эскроу → Росреестр → завершение, US-реестр блока «Сделка» (US-INV / US-AGT / US-DEV / US-MGR)
- scenarios (1): UC-OVR-006. Сделка: от брони до регистрации (инвестор-инициатор)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: attach-booking-payment-proof.md (Фаза 1), confirm-booking-payment.md (Фаза 2); загрузка договора брони — upload-booking-contract.md.
- Domain entities: booking.md (INV-BKG-3/4/8,
AwaitingPayment/Booked,PaymentStatus,ProgramTerms), deal-document.md (BookingPaymentProof, INV-DOC-1/3/4/6), deal.md (PrimaryInvestorUserId,Money), deal-timeline-event.md (BookingPaid/BookingConfirmed/DocumentUploaded). - Kafka events: document-status-changed.md, booking-paid.md, booking-confirmed.md.
- Соседние use cases: UC-BKG-002 (Резерв → Бронь, вход в платный путь A1), UC-BKG-003 (истечение).
- US-реестр / реестр UC: user-stories-deal.md (US-INV-06/21); README контекста.
- ADR: 0052 tenancy, 0056 simplicity-first; решение 9 ТЗ (эквайринг отложен).