Doc Booking - PlutoGr/TravelAgency GitHub Wiki
Booking
src/Services/Booking/. Заявки на туры, предложения менеджера и избранное. База travel_booking.
Простой пример
Клиент на странице тура нажимает «Оставить заявку»:
POST /api/v1/bookings
{ "tourId": "3f2a…", "comment": "Двое взрослых, июль" }
→ 201 { "id": "b7c1…", "status": 0, … }
Booking сначала спрашивает у Catalog по gRPC, опубликован ли тур. Если нет, ответ 422 tour-unavailable, заявка не создаётся. Дальше заявку ведёт менеджер: берёт в работу, отправляет предложение, клиент его подтверждает. Весь сценарий: Заявки и чат.
Модель
| Сущность | Поля |
|---|---|
Booking |
Id, ClientId, TourId, Comment, Status, CreatedAt, UpdatedAt, AssignedManagerId, AssignedAt |
BookingStatusHistory |
BookingId, Status, ChangedByUserId, ChangedAt |
BookingAssignmentHistory |
BookingId, PreviousManagerId, NewManagerId, ChangedByUserId, ChangedAt, Comment (до 500 символов): одна строка на каждую смену ответственного |
Proposal |
BookingId, ManagerId, TourSnapshot, Notes, IsConfirmed, CreatedAt |
TourSnapshot |
TourId, Title, Description, Price, Currency, DurationDays, SnapshotTakenAt: копия тура на момент предложения |
Favorite |
UserId, TourId, AddedAt; пара уникальна |
OutboxMessage |
EventType, Payload, Status (Pending, Processed, Failed), CreatedAt, ProcessedAt |
Статус заявки в JSON передаётся числом:
| Число | Статус | Смысл |
|---|---|---|
| 0 | New |
создана клиентом |
| 1 | InProgress |
менеджер взял в работу |
| 2 | ProposalSent |
клиенту отправлено предложение |
| 3 | Confirmed |
клиент подтвердил предложение |
| 4 | Closed |
заявка завершена |
| 5 | Cancelled |
отменена |
Перевод чисел в названия на фронтенде: src/api/bookings.ts.
Переходы статусов
| Из | Можно в |
|---|---|
New |
InProgress, Cancelled |
InProgress |
ProposalSent (только если есть хотя бы одно предложение), Cancelled |
ProposalSent |
Confirmed, InProgress, Cancelled |
Confirmed |
Closed, Cancelled |
Closed, Cancelled |
никуда |
Недопустимый переход: BookingDomainException, ответ 422.
Кто что может
| Действие | Клиент | Manager, Admin |
|---|---|---|
| создать заявку | да | да |
свои заявки GET /bookings/my |
да | да |
все заявки GET /bookings |
нет (403 на Gateway) | да |
| заявка по id | только своя, иначе 403 | любая; плюс имя и email клиента из Identity |
смена статуса PATCH …/status |
только своя и только в Cancelled |
любой допустимый переход |
предложение POST …/proposal |
нет | да, кроме Closed и Cancelled |
подтверждение POST …/confirm |
своя заявка | да |
взять заявку POST …/take |
нет (403) | да, кроме Closed и Cancelled |
назначить менеджера PUT …/assigned-manager |
нет (403) | да, кроме Closed и Cancelled |
список GET /bookings?assigned=me|queue|all |
нет (403) | да |
PATCH с Confirmed отвечает 400: подтверждение идёт только через confirm с proposalId. Неверный proposalId: 422. Поле clientPhone в ответе всегда null: телефон хранится в Identity, но gRPC GetUserSummary его не передаёт.
Ответственный менеджер
Простой пример. Заявка без ответственного лежит в общей очереди. Менеджер её берёт:
POST /api/v1/bookings/b7c1…/take
{ "expectedManagerId": null }
→ 200 { "id": "b7c1…", "assignedManagerId": "<id менеджера>", "assignedAt": "2026-10-19T14:00:00Z", … }
Второй менеджер, который отправил тот же запрос, получает 409 «Заявку уже взял другой менеджер».
AssignedManagerId = null: заявка в общей очереди.GET /bookings?assigned=queueотдаёт такие заявки,assigned=meзаявки текущего менеджера,assigned=all(по умолчанию) все. Другое значение: 400.expectedManagerId: какой ответственный был на экране у того, кто нажал (nullдля очереди). Если в базе уже другой, ответ 409, назначение не меняется.- Одновременное «Взять» защищено системным столбцом PostgreSQL
xmin(concurrency token): второйSaveChangesполучаетDbUpdateConcurrencyException, ответ тот же 409. - Повтор тем же менеджером (
expectedManagerIdравен его id) возвращает 200 и не добавляет строку истории. PUT /bookings/{id}/assigned-managerназначает другого менеджера. Booking спрашивает роль цели у Identity (GetUserSummary): если Identity пользователя не нашёл (NotFound) или роль не Manager, ответ 400 «Указанный пользователь не менеджер.». Любой другой сбой вызова Identity даёт 503, назначение не меняется.- Закрытая (
Closed) или отменённая (Cancelled) заявка: 422. - Каждая смена пишет строку
BookingAssignmentHistoryи сообщение outboxBookingManagerAssigned.
Закреплённый менеджер клиента. При создании заявки Booking берёт ответственного последней заявки этого клиента, у которой менеджер назначен (учитываются и отменённые, и завершённые). Значение копируется в новую заявку, только если Identity подтверждает роль Manager; тогда же пишется первая строка истории. Если Identity пользователя не нашёл или роль другая, заявка попадает в общую очередь. Любой другой сбой вызова Identity даёт 503, заявка не сохраняется. Снятие уже назначенного менеджера при смене роли (#170) не делается.
Что видно в ответе. BookingDto содержит assignedManagerId, assignedAt и assignedManagerName (имя из Identity; при сбое Identity поле пустое, ответ не падает). assignmentHistory заполнен только для Manager и Admin, у клиента поле null.
Чат. Писать в чат заявки могут клиент-владелец, ответственный менеджер и Admin. Остальные менеджеры только читают. Booking отдаёт это Chat через gRPC: Межсервисные контракты.
Миграция 20261019135257_AddBookingAssignedManager добавляет в Bookings пустые столбцы AssignedManagerId и AssignedAt, таблицу BookingAssignmentHistories и индексы. Существующие заявки не заполняются: после миграции все они в общей очереди, и менеджер пишет в их чат только после take или назначения. Задача #220, PR #245.
Связи с другими сервисами (gRPC)
| Вызов | Когда |
|---|---|
Catalog GetTourSnapshot |
создание заявки и добавление в избранное; тур должен быть опубликован |
Catalog GetTourSnapshotForExistingBooking |
создание предложения; подходит и снятый с публикации тур |
Identity GetUserSummary |
GET /bookings/{id} для менеджера и админа (имя и email клиента); имя ответственного менеджера; роль пользователя в ValidateBookingAccess; проверка роли Manager при создании заявки и в assigned-manager |
сервер ValidateBookingAccess |
Chat проверяет, может ли пользователь читать (has_access) и писать (can_write) |
сервер GetBookingAssignment |
клиент, ответственный и прежние менеджеры заявки |
Цена в снимке: самое дешёвое предложение тура, действующее сегодня; если такого нет, самое дешёвое из всех; если предложений нет, 0 USD. Подробно: Межсервисные контракты.
Избранное
GET /favorites, POST /favorites/{tourId} (409, если уже есть), DELETE /favorites/{tourId}. Карточки избранного фронтенд берёт из Catalog: GET /catalog/tours/cards?ids=.
Outbox
При создании заявки, смене статуса и отправке предложения в той же транзакции пишутся сообщения BookingCreated, BookingStatusChanged, ProposalSent. При каждой смене ответственного, включая закрепление при создании, пишется BookingManagerAssigned (BookingId, PreviousManagerId, NewManagerId, ChangedBy). Обработчика, который бы их отправлял, в коде нет: сообщения остаются в статусе Pending. Задача на этапе 1: MVP-1.
Настройки
ConnectionStrings__BookingDb, JwtSettings__*, GrpcSettings__InternalServiceToken, GrpcClients__CatalogServiceUrl, GrpcClients__IdentityServiceUrl.