Doc Catalog - PlutoGr/TravelAgency GitHub Wiki
Catalog
src/Services/Catalog/. Каталог туров и направлений: публичная выдача опубликованных туров и конструктор, в котором менеджер собирает и публикует свой тур. Источник правды о турах для Booking. База travel_catalog.
Простой пример
Гость открывает /tours/3f2a…. Фронтенд запрашивает GET /api/v1/catalog/tours/3f2a…:
- тур опубликован: 200 с описанием, программой по дням, предложениями (даты и цены), фото по id;
- тур черновик или снят с публикации: 404, как будто его нет.
Модель
Tour (агрегат):
| Поле | Что это |
|---|---|
Title, ShortDescription (до 300 символов), Description |
тексты |
TourType |
Beach, Mountain, City, Cultural, Adventure, Cruise, Safari |
Country, DirectionId, DepartureCity, DurationDays |
основное |
MealPlan |
RO, BB, HB, FB, AI, UAI |
AccommodationText |
проживание текстом (отель как сущность планируется на этапе 2) |
Status |
Draft, Published, Unpublished; PendingReview зарезервирован под премодерацию и не используется |
Source |
Manager или Automation (сейчас всегда Manager) |
OwnerId |
менеджер-владелец |
PublishedAt, CreatedAt, UpdatedAt |
даты |
Version |
версия для ETag и If-Match, растёт при любом изменении тура |
ImageUrl |
устаревшая колонка, в ответы не попадает; удаление в задаче #81 |
Дочерние таблицы:
| Сущность | Что хранит |
|---|---|
TourOffer |
предложение тура: DepartureDate (местный день вылета, столбец date), ValidFrom, ValidTo, PricePerPerson, Currency, AvailableSeats |
TourDay |
программа по дням: DayNumber, Title, Description |
TourInclusion |
пункт Included или NotIncluded, текст, порядок |
TourImage |
MediaFileId, порядок, IsCover (обложка одна, уникальный индекс), Alt, WidthPx |
TourComponent |
задел под составные туры этапа 2; в ответах components: [] |
Direction: Name, Country, Description, IsActive.
Свойство Tour.IsActive вычисляется как Status == Published и оставлено для совместимости ответов.
Публичное API
Только Published. Таблица с параметрами: Публичный API.
GET /catalog/tours: фильтры по стране (подстрока), направлению, типу, цене и датам; сортировка по дате создания, названию, цене, длительности; страницы. Цена «от», фильтр цены и ближайшая дата (nearestDate, строкаyyyy-MM-dd) считаются по предложениям сDepartureDateне раньше сегодня. Сегодня берётTimeProviderв поясеCatalog:DefaultTimeZone(по умолчаниюEurope/Moscow): вылет в этот день ещё показывается. До 5 превью на тур.
Фильтр дат вылета:
- дата задаётся календарным днём (
DateOnly): в адресеyyyy-MM-dd, на чипах «с ДД.ММ.ГГГГ» и «по ДД.ММ.ГГГГ»; - обе границы включительны:
DepartureDate >= dateFromиDepartureDate <= dateTo. Сравнивается местный день города вылета, без перевода в UTC; - допустимы даты с 2000-01-01 по 2100-12-31, вне диапазона 400 с
code=date_rangeи текстом «Выберите дату в пределах 2000–2100 года»; - если дата «с» позже даты «по», тот же код и текст «Дата «по» должна быть не раньше даты «с».»;
- у полей формы стоят
minиmax, дата из адреса вне диапазона или перепутанный порядок показывают подсказку, и запрос не отправляется; - ошибка базы (
DbExceptionили тип из пространстваNpgsql) всегда даёт 500 сcode=database_errorи фиксированным текстом; - ответ валидации сервисов на общем middleware содержит
errors, вcodeпопадает только свой код из белого списка (date_range), иначеvalidation. GET /catalog/tours/{id}: страница тура (PublicTourDto).GET /catalog/tours/cards?ids=: до 50 карточек для избранного; снятый тур приходит сavailable: false.GET /catalog/directions.
Пояс города вылета задан списком DepartureCityTimeZones (src/Services/Catalog/TravelAgency.Catalog.Application/Time/DepartureCityTimeZones.cs), это не таблица базы. Сопоставление без регистра, без пробелов по краям, «ё» равно «е». Пример: «Москва» это Europe/Moscow, «Новосибирск» это Asia/Novosibirsk. Города нет в списке: момент с ненулевым временем переводится в Catalog:DefaultTimeZone. Миграция 20261019125458_AddTourOfferDepartureDate для такого города подставляет константу Europe/Moscow и пишет отдельный NOTICE. PostgresNoticeLoggingInterceptor переносит этот NOTICE в лог приложения.
Управление турами
/catalog/manage/tours, роли Manager и Admin (TourManageController). Менеджер видит и меняет только свои туры (иначе 403), Admin все. Сценарий целиком: Конструктор и публикация тура.
| Действие | Запрос |
|---|---|
| список | GET /catalog/manage/tours |
один тур с чек-листом и ETag |
GET /catalog/manage/tours/{id} |
| черновик | POST /catalog/manage/tours |
| шаги мастера | PUT {id}/basics, description, program, conditions, prices, images |
| публикация | POST {id}/publish, POST {id}/unpublish |
| удаление черновика | DELETE {id} |
Ошибки
CatalogExceptionMapper переводит исключения в ProblemDetails:
| Код | Когда |
|---|---|
| 400 | валидация |
| 403 | чужой тур |
| 404 | тура нет |
| 409 | устаревший If-Match; удаление не черновика; снятие с публикации черновика |
| 422 | type: https://travelagency/errors/tour-not-publishable, список кодов в missing; чужое, неизвестное или не фото тура, один код images.notFound |
| 428 | нет If-Match |
| 503 | Media недоступен |
gRPC
- Сервер
CatalogService(порт 8081):GetTourSnapshot,GetTourSnapshotForExistingBooking. Вызывает Booking. - Клиент Media:
GetMediaFiles,MarkMediaFilesPublic.
Подробно: Межсервисные контракты.
Демо-данные
CatalogDataSeeder создаёт 5 демо-туров через доменный Publish() (владелец: демо-аккаунт менеджера, см. README) и один черновик второго менеджера. Туры ищутся по названию, существующие сохраняют id и предложения. Направления: Мальдивы, Пхукет, Санторини, Бали, Дубай.
Настройки
ConnectionStrings__CatalogDb, JwtSettings__*, GrpcSettings__InternalServiceToken, GrpcClients__MediaServiceUrl, Seeding__DemoCatalog, Catalog__DefaultTimeZone (по умолчанию Europe/Moscow).