POST /api/investor/crm/bookings/{bookingId}/payment-proof — Прикрепить чек оплаты брони
Инвестор оплатил платное бронирование вне платформы и прикрепляет чек (US-INV-21). Онлайн-эквайринг
отложен (решение 9 ТЗ): онлайн-оплаты нет, вместо неё — загрузка файла-чека, который затем вручную
подтверждает менеджер застройщика (confirm-booking-payment.md).
Операция создаёт документ сделки типа BookingPaymentProof в статусе Uploaded, привязанный к брони; сама
бронь не мутируется (PaymentStatus остаётся Pending — ждёт подтверждения). Дом полей —
deal-document.md и booking.md.
Назначение
Принять от инвестора {BookingId, FileId, FileName, ContentType, SizeBytes} и завести документ
DealDocumentEntity (Type = BookingPaymentProof, Status = Uploaded, с заполненным BookingId) — прямая
загрузка [*] → Uploaded (INV-DOC-1: загрузчик типа BookingPaymentProof — инвестор). Владелец — контекст
crm, feature group booking, сценарий US-INV-21.
INV-DOC-1: загрузчик документа BookingPaymentProof — только инвестор (PrimaryInvestorUserId берётся из
токена, а не из тела запроса). Операция не мутирует бронь: PaymentStatus остаётся Pending до ручного
подтверждения менеджером застройщика
(confirm-booking-payment.md) — здесь публикуется только
DocumentStatusChangedEvent, BookingPaidEvent не публикуется.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | investor |
| Feature group | booking |
| Статус | draft |
| Документ | docs/06-services/crm/api/investor/attach-booking-payment-proof.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Инвестор) |
| Auth policy / Permissions | authenticated (JWT инвестора) + crm.bookings.manage; self-service по своей брони |
| Scope (RBAC) | не применяется: инвестор — вне developer-tenant (кросс-tenant участник, ADR-0052) |
| Record-level | deal.PrimaryInvestorUserId = actor.UserId (бронь → сделка); форсится сервером, чужую бронь прикрепить нельзя |
| Tenant isolation | не developer-tenanted: document.TenantId = booking.TenantId = DeveloperCompanyId наследуется от сделки; изоляция запроса — record-level по PrimaryInvestorUserId из claims |
| Audit | create (DealDocumentEntity); UploadedByUserId/CreatedByUserId = actor.UserId; бинарь чека, FileId, FileName в логи не пишутся |
Record-level предикат deal.PrimaryInvestorUserId = actor.UserId форсится сервером из JWT — инвестор не может
подставить чужой PrimaryInvestorUserId через тело запроса. Инвестор — кросс-tenant участник вне
developer-tenant (ADR-0052): RBAC scope не применяется, доступ к чужой брони закрыт именно record-level
проверкой, а не tenant-фильтром.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/investor/crm/bookings/{bookingId}/payment-proof |
| Success status | 201 (документ-чек создан) |
| Idempotency header | не применяется (повтор → новая запись документа, INV-DOC-4; менеджер выбирает актуальный чек при подтверждении) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
BookingId | route | Guid | Да | != Guid.Empty; существующая бронь; deal.PrimaryInvestorUserId = actor; Status = AwaitingPayment; ProgramTerms.IsPaid; PaymentStatus = Pending | — |
FileId | body | Guid | Да | != Guid.Empty; логическая ссылка на storage | Internal: в логи не пишется |
FileName | body | string | Да | 1..512, не пустое после trim | может нести детали — целиком не логируется |
ContentType | body | string | Да | ∈ {application/pdf, image/jpeg, image/png} (US-INV-12: JPG/PDF; конфигурация) | — |
SizeBytes | body | long | Да | > 0 и <= 52_428_800 (50 МБ, конфигурация) | защита от unbounded |
PrimaryInvestorUserId | identity claims | Guid | Да | Required; record-level предикат, форсится = actor.UserId | scope isolation |
Модель ответа DealDocumentModel
Проекция созданной записи (дельта дома deal-document.md); PII/бинарь не
содержит — только метаданные и FileId (скачивание — по пресайн-ссылке storage с проверкой участия).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | DealDocumentEntity.Id | — | documentId чека (на него ссылается подтверждение оплаты). |
DealId | Guid | Да | DealId | — | Сделка-владелец (deal.md). |
BookingId | Guid | Да | BookingId | — | Бронь, к которой прикреплён чек. |
Type | string (enum DealDocumentType) | Да | Type | — | Всегда BookingPaymentProof. |
Status | string (enum DealDocumentStatus) | Да | Status | — | После операции — Uploaded. |
Title | string | Да | Title | — | «Чек об оплате бронирования». |
FileId | Guid | Да | FileId | — | Файл в storage/CDN. |
FileName | string | Да | FileName | — | Оригинальное имя файла. |
ContentType | string | Да | ContentType | — | MIME-тип чека. |
SizeBytes | long | Да | SizeBytes | — | Размер файла. |
UploadedAt | DateTimeOffset | Да | UploadedAt | — | Момент загрузки (US-INV-13). |
CreatedAt | DateTimeOffset | Да | CreatedAt | — | Создание записи. |
Алгоритм
- Контекст и доступ. Операцию выполняет инвестор;
PrimaryInvestorUserIdберётся из его токена (из тела не принимается). Повтор загрузки допустим — каждый раз создаётся новая запись документа (INV-DOC-4). - Проверка входа и предусловий.
- формат:
BookingId/FileIdне пустые;FileName1..512;ContentType ∈ {application/pdf, image/jpeg, image/png};SizeBytes> 0 && <= 52_428_800— иначеVALIDATION_FAILED; - существование брони: если брони нет →
CRM_BOOKING_NOT_FOUND; - принадлежность/record-level (INV-DOC-1, загрузчик — инвестор):
deal.PrimaryInvestorUserId == PrimaryInvestorUserId— иначеCRM_BOOKING_FORBIDDEN; - терминальность сделки (INV-DOC-5):
deal.Status ∉ {Completed, Cancelled}— иначеCRM_DEAL_ALREADY_TERMINAL; - допустимость состояния брони:
booking.Status == AwaitingPayment(договор брони уже загружен застройщиком) — иначеCRM_BOOKING_INVALID_STATE; - применимость оплаты:
booking.ProgramTerms.IsPaid == true && booking.PaymentStatus == Pending— иначеCRM_BOOKING_PAYMENT_NOT_REQUIRED(бесплатная программа / уже оплачено).
- формат:
- Что читаем. Бронь и её сделку (родитель для record-level и guard терминальности) — только для проверок
выше; ни бронь, ни сделка этой операцией не изменяются. Существующий чек не требуется — это проактивная
загрузка
[*] → Uploaded. - Бизнес-правила и переходы. Создаётся документ (INV-DOC-1/INV-DOC-3):
TenantId = booking.TenantId(=DeveloperCompanyId);DealId = booking.DealId;BookingId = booking.Id;Type = BookingPaymentProof;Status = Uploaded;Title = "Чек об оплате бронирования";FileId/FileName/ContentType/SizeBytes— из входа (INV-DOC-3: обязательны со статусаUploaded, тип/размер в допустимых пределах);UploadedByUserId = CreatedByUserId = actor.UserId;UploadedAt = CreatedAt = now. Добавляется строка таймлайна сделки «чек об оплате прикреплён» (deal-timeline-event.md); парная таймлайн-строкаBookingPaidпоявится позднее при подтверждении менеджером (confirm-booking-payment.md), не здесь. Бронь не изменяется:PaymentStatusостаётсяPending(переход→ Paid— только при подтверждении, INV-BKG-4). - Что меняется. В одном согласованном изменении (INV-DOC-6): добавляется запись документа + строка таймлайна; бронь/сделка не обновляются.
- Какие факты фиксируются. Публикуется
DocumentStatusChangedEvent→ топикpin.crm.document-status-changed.v1(ContractVersion = 1,AggregateType = "DealDocument") с payload{DocumentId, DealId, TenantId, DocumentType = BookingPaymentProof, FromStatus, ToStatus = Uploaded, ChangedByUserId = actor.UserId, ChangedAt = UploadedAt}— потребительnotifications(уведомление о статусе документа, US-INV-14.3); контракт — document-status-changed.md (FromStatus = Requestedпри наличии запроса-предшественника, иначе прямой upload[*] → Uploaded).BookingPaidEventздесь не публикуется — только после подтверждения менеджером (booking-paid.md). - Результат. Проекция созданного документа
DealDocumentModel(Type = BookingPaymentProof,Status = Uploaded,FileId,UploadedAt, …) → HTTP201.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | DocumentStatusChangedEvent → pin.crm.document-status-changed.v1 (потребитель notifications). BookingPaidEvent — не здесь (только при подтверждении менеджером) |
| External calls | none (бинарь чека уже в storage/CDN — загрузка клиентом отдельным пресайн-flow; в CRM пишутся только метаданные и FileId) |
| Cache / projections / search | none |
| Notifications | опосредованно: notifications — потребитель document-status-changed (статус документа инвестору/менеджеру, US-INV-14.3) |
Ошибки и failure modes
Два отказа не сводятся к простой валидации формата: CRM_BOOKING_INVALID_STATE — договор брони ещё не загружен
застройщиком (Status != AwaitingPayment) либо бронь уже завершена; CRM_BOOKING_PAYMENT_NOT_REQUIRED —
программа бесплатная (IsPaid = false) либо оплата уже подтверждена (PaymentStatus != Pending), прикреплять
чек не нужно.
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | пустой BookingId/FileId, недопустимый ContentType, размер вне пределов, пустое FileName (INV-DOC-3) | «Проверьте файл чека (тип JPG/PDF, размер до 50 МБ)» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CRM_BOOKING_NOT_FOUND | 404 | бронь не найдена | «Бронирование не найдено» | no retry |
CRM_BOOKING_FORBIDDEN | 403 | deal.PrimaryInvestorUserId != actor (чужая бронь) | «Нет доступа к бронированию» | no retry |
CRM_BOOKING_INVALID_STATE | 409 | Status != AwaitingPayment (договор брони ещё не загружен / бронь завершена) | «Чек можно прикрепить только после загрузки договора бронирования» | no retry (перечитать состояние) |
CRM_BOOKING_PAYMENT_NOT_REQUIRED | 409 | бесплатная программа (IsPaid = false) или PaymentStatus != Pending (уже оплачено) | «Оплата по этой брони не требуется» | no retry |
CRM_DEAL_ALREADY_TERMINAL | 409 | сделка в терминальном статусе (INV-DOC-5) | «Сделка завершена или отменена» | no retry |
Acceptance Criteria
- Given своя бронь
AwaitingPayment,ProgramTerms.IsPaid = true,PaymentStatus = Pending, When инвестор загружает валидный чек (PDF/JPEG/PNG), Then создан документBookingPaymentProof(Uploaded) с заполненнымBookingId, добавлена строка таймлайна, опубликованоDocumentStatusChangedEvent→pin.crm.document-status-changed.v1(ToStatus = Uploaded); бронь не мутируется (PaymentStatusостаётсяPending); ответ201. - Given чек уже прикреплён, When повторная загрузка, Then создаётся новая запись документа (INV-DOC-4); менеджер выбирает актуальный чек при подтверждении.
- Given
deal.PrimaryInvestorUserId != actor, When attach, ThenCRM_BOOKING_FORBIDDEN(403) по INV-DOC-1; брони нет →CRM_BOOKING_NOT_FOUND(404). - Given
Status != AwaitingPayment(договор брони ещё не загружен либо бронь завершена), When attach, ThenCRM_BOOKING_INVALID_STATE(409). - Given бесплатная программа (
IsPaid = false) либоPaymentStatus != Pending, When attach, ThenCRM_BOOKING_PAYMENT_NOT_REQUIRED(409). - Given
ContentType ∉ {application/pdf, image/jpeg, image/png}, размер вне(0, 50 МБ]или пустыеBookingId/FileId, When attach, ThenVALIDATION_FAILED(400) по INV-DOC-3. - Given чек прикреплён, When проверяется эффект на бронь, Then
BookingPaidEventне публикуется и переходPaymentStatus → Paidне происходит — только после ручного подтверждения менеджером (INV-BKG-4, confirm-booking-payment.md); парная строка таймлайнаBookingPaidпоявляется тогда же, не здесь. - Given сделка
Completed/Cancelled, When attach, ThenCRM_DEAL_ALREADY_TERMINAL(409) по INV-DOC-5.
Совместимость и наблюдаемость
- Новые optional поля
DealDocumentModelи payload события — additive без breaking change; удаление/ переименование/смена типа поля или значения enum (DealDocumentType/DealDocumentStatus) — breaking (migration + rollout; версия события — новый.v2+ dual-publish). Список допустимыхContentType— конфигурация (расширение — additive). - Logs:
booking-payment-proof attached(BookingId,DealId,DocumentId,PrimaryInvestorUserId;FileId/FileName/бинарь — не пишутся); metrics:crm_booking_payment_proofs_total{result}, latency p95 (SLO команд < 500 мс); traces — OTel, correlation черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-BKG-004. Оплата платного бронирования (чек + подтверждение)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: deal-document.md (дом полей,
BookingPaymentProof, INV-DOC-1/3/5/6, статусы), booking.md (AwaitingPayment,PaymentStatus, INV-BKG-4), deal.md (PrimaryInvestorUserId, терминальные статусы), deal-timeline-event.md (таймлайн загрузки чека). - Парная операция подтверждения: confirm-booking-payment.md.
- События: document-status-changed.md, booking-paid.md (только при подтверждении); карта области — README.
- ADR: 0052 tenancy; решение 9 ТЗ (эквайринг отложен).