Не вносить изменения в логику ценообразования (в частности SQL) без подтверждения — правило зафиксировано в CLAUDE.md проекта.
apps/backend-service/src/housing/services/sql/pricing.sql — итоговый расчёт
(total_price/day_price/base_total_price) для диапазона дат,
встраивается как саб-запрос в HousingService.addSelections.apps/backend-service/src/migrations/1778399744332-update-pricing-functions.ts —
единственное место, где определены SQL-функции calc_discount/calc_day_price/
calc_children_detailed (плюс несколько неиспользуемых calc_* той же миграции —
оставлены намеренно, не мусор).
Эти SQL-функции создаются только миграцией — synchronize строит лишь таблицы/колонки из
entity, сырой SQL миграций не выполняет. На любой БД с нуля (включая e2e-стенд) без ручного прогона
миграции любой расчёт цены падает 500-й ошибкой calc_children_detailed(...) does not exist.
На prod/staging миграции гоняются вручную отдельным шагом. Для e2e — ensurePricingFunctions()
в apps/backend-service/test/db.ts реплеит up() этой миграции в beforeAll.
[start_date, end_date), дата выезда не входит в число ночей.
Стыковка броней в один день разрешена в обе стороны (выезд одной = заезд другой).calendar_day.price/adult_surcharge_price) выигрывает у
базовой цены, только если явно задан (не NULL) на конкретную дату.Housing.adult_surcharge_price. Это асимметрия на
SQL-уровне относительно доплаты за взрослых (которая пересчитывается по каждому дню) — не трогать
без отдельного решения.count_days (ночей ≥ count_days), не по
максимальному проценту.Discount.type (percent/number) на расчёт не
влияет (осознанное решение).DiscountInput @Min(1) @Max(99);
фронт: apps/shared/src/ui/housing/form/discounts/item.vue.
Housing.unavailable_reasons: [UnavailableReason!] — вычисляемое (не колоночное) поле,
одно inline SQL-выражение в addSelections, работает одинаково и в housingWithPrice/
ownerHousingWithPrice (один объект), и в housings/ownerHousings
(список, без N+1). Подзапрос считается только если поле реально запрошено. addFilters для
housings получил guard: если unavailable_reasons в выборке — жёсткие
WHERE-фильтры (animalsFilter/minChildAgeFilter/minNightsFilter/
rangeDateBookingFilter+availabilityFilter/guestCountFilter) не
исключают объект из выдачи — объект получает причины вместо того, чтобы пропасть.
| Код | Смысл |
|---|---|
PETS_NOT_ALLOWED | объект не принимает животных |
CHILD_AGE_NOT_ALLOWED | возраст ребёнка не подходит |
MIN_NIGHTS_NOT_MET | бронь короче минимума ночей |
DATES_BOOKED | даты заняты |
NOT_ENOUGH_BEDS | не хватает спальных мест |
Enum-каталог: @shared/catalogs/housing/unavailable-reason.ts. Бэкенд отдаёт только код
нарушения, текст/i18n — на фронте (message.unavailable_reason_<CODE> в
apps/shared/src/i18n/ru.js). Поля booked/enough_beds/
nights_count удалены целиком — заменены на unavailable_reasons и клиентский
расчёт (dayjs(end).diff(start,'day')) соответственно.
Postgres не разрешает ссылаться на алиас SELECT-списка внутри составного ORDER BY-выражения;
TypeORM's addOrderBy валидирует любую raw-строку вида identifier.identifier
как алиас. Рабочее решение — свой собственный addSelect(sql, 'sort_alias')
(sort_dates_booked/sort_enough_beds) и ссылка на него bare-именем, без
составного выражения.
prepareForBooking при отказе возвращает конкретную причину
(BadRequestException) вместо generic Error → 500. Проверка
min_night_count переиспользует minNightsFilter (учитывает per-day override).findOneWithPrice при отсутствии/недоступности жилья кидает NotFoundException
вместо необработанного TypeError.BookingChildAgePrice (мёртвый код) удалён целиком.
Любое сообщение об ошибке, которое может дойти до пользователя ($t(error) с
err.message целиком), обязано иметь перевод в apps/shared/src/i18n/ru.js —
иначе показывается сырой английский ключ.