Три сценария бронирования — сквозной путь

Каждый сценарий проходит один и тот же путь: найти санаторий → выбрать номер → выбрать программу → забронировать. Показано, что фильтруется на каждом шаге, из каких полей АСБ это берётся (file:line) и где сценарии расходятся. Всё сверено с кодом и живыми пробами тест-контура.

A · Поиск санатория B · Выбор номера C · Выбор программы D · Бронь (setletter)
Общие шаги A и C ① Двое взрослых ② Взрослые + ребёнок ③ Один Справочник: слои и поля

Легенда параметров — что значит и ОТКУДА определение

Бронируется место (путёвка), не номер. Источник каждого: ДОКА — документация АСБ (read_me.html / letter_help.json, ниже прямая цитата); КОД — наш код; ПРОБА — установлено экспериментом (в доке описания нет).

ПараметрУровеньЧто значит (цитата доки / вывод пробы)Источник
catcodкатегория«Код категории»ДОКА
catbednumкатегория«кол-во основных мест» → наш capacityДОКА normalize.py:160
catdopnumкатегория«кол-во доп мест» (занимать только детям) → наш dop_capacityДОКА normalize.py:162
oneplaceflкатегориядока: «Возможность размещения 1 гостя во весь номер, 1-возможно / 0-нет». Проба уточнила операционно: 0 → одно отдельное место продать нельзя (частичная загрузка → -35); 1 → один гость бронируется (по цене места)ДОКА + ПРОБА
oneplacedatesкатегория«пары дат ограничений по подселению в номер»ДОКА
dopplaceflгость«Код доп места: 0-осн, 1-доп»ДОКА
agecodгость«Возраст: 1-Вз, 2-Реб»ДОКА
sexcod / countintcodгость«Пол: 1-М, 2-Ж» / «код страны ISO, РФ 643»ДОКА
concodпрограмма«код договора» — квоты раздельны по договоруДОКА
queryflзаказ«0-заказ (проходит проверку квоты), 1-запрос (не проходит)»ДОКА
catplaceflномерВ доке только "catplacefl": 0 без описания. Установлено пробой: 0 = бронь по местам; 1 = выкуп всего номера под одного (~1.5× цены места; setletter реально забронировал весь номер, refs 3396675/76)ПРОБА diag_oneplace_enforce.py
Что такое «код категории» (catcod). Категория — это тип номера, а не конкретная комната: класс одинаковых номеров с общей ценой, вместимостью, фото и флагами. Например catcod=6518 = «Двухместный Мансардный без балкона корпус 4» — таких физических номеров в пансионате может быть много, но catcod у них один. Отдельных комнат (с номерами дверей) АСБ не отдаёт — всё считается на категорию: квота (quoteval) = сколько единиц этого типа свободно; цена — по паре (категория × программа); вместимость (catbednum/catdopnum) и oneplacefl — свойства категории. В нашей БД запись Room = категория (room_id = "objid:catcod", напр. 1456:6518), не физическая комната. Бронируешь ты место в категории.

Что делает наш код КОД: всегда шлёт catplacefl=0, а oneplacefl в БД не хранит (синк его не читает — слой 1). Отсюда баг сценария ③.

Шаги A и C — одинаковы во всех трёх сценариях

Шаг A · Поиск санатория occupancy-blind

Форма поиска: даты + состав гостей → в URL (rooms=2- / 2-7 / 1-). Каталог фильтруется в services/search.py.

Фильтры санаториевУчитывают состав гостей?
тип · гео (страна/регион/город) · цена · звёзды · профиль лечения · удобства/питание · болезнь · сортировкаНет. Число гостей в фильтр санаториев не входит — оно едет в URL для след. шагов. Список санаториев для одного, для пары и для семьи одинаков search.py:110-226
Все поля-основы фильтров — из ответов АСБ через синк (type←objtyps, гео←geoobject, stars←objstars) или производные (price_from=min цен, удобства=детект по услугам). Подробно — в слое 2.

Шаг C · Список программ в выбранном номере

Внутри номера показываются программы (RoomProgramPicker.tsx:39-58).

Как отбираются программы номера
Доступные = те, у кого есть кэш-цена для этого номера (room.program_prices). Сортировка: профсоюзные — в конец, внутри — по цене «за ночь / место». Цена берётся из синканного кэша prices; наличие по программе (availability{progid}) — по договору concod (слой 3). Учёта числа гостей / oneplacefl при отборе программ нет.

Сценарий ① — Двое взрослых (себе и супруге)

Состав: 2 взрослых, один номер. rooms=2-
Взрослый 1
осн. место · dopplacefl=0
Взрослый 2
осн. место · dopplacefl=0

Шаг B · Подбор номера

Что проверяетсяРезультат для 2 взрослыхfile:line
Наличие (3 состояния)«Нет мест» / «подтвердим при бронировании» / «Выбрать» — по квоте номераRoomsList.tsx:70-71
Вместимость: взрослых ≤ capacityнужен номер с cap ≥ 2. Двухместные подходят; одноместный (cap=1) → «до 1 взрослых», кнопка «Не подходит по местам»RoomsList.tsx:76-81

Фит-чек здесь работает правильно: верхняя граница вместимости — и есть реальное ограничение для двух взрослых.

Шаг D · Бронь

setletter, 2 основных места (catplacefl=0) — полная загрузка. oneplacefl не важен.

Итог: ✅ бронируется на любом подходящем номере, где есть квота. Нет мест → ref=-21 «Quota is empty». Самый надёжный сценарий.

Сценарий ② — Себе, супруге и ребёнку

Состав: 2 взрослых + 1 ребёнок. rooms=2-7 (возраст ребёнка)
Взрослый 1
осн. · dopplacefl=0
Взрослый 2
осн. · dopplacefl=0
Ребёнок
доп. · dopplacefl=1 · agecod=2

Шаг B · Подбор номера

Что проверяетсяРезультат для 2 взр. + ребёнкаfile:line
Наличиекак обычно, по квоте номераRoomsList.tsx:70-71
Вместимость: взрослых ≤ cap, всего ≤ cap+dopнужен номер с cap ≥ 2 И доп. местом (dop ≥ 1). Двухместный без доп. (dop=0) → «до 2 гостей», не подходит; с доп. — подходитRoomsList.tsx:76-81
Возраст ребёнка (на брони)validate_booking: доп. место только ребёнку; возраст на заезд ≤ child_age объекта и = заявленномуbooking_jsdata.py:93-142

Фит-чек тоже правильный: отсекает номера без доп. места. Ограничение — верхняя граница вместимости.

Шаг D · Бронь

setletter, 2 осн. места + 1 доп. (ребёнок). АСБ проверяет осн. квоту (2) и доступность доп. места.

Итог: ✅ бронируется, если у номера есть доп. место и осн. квота на двоих. Нет доп. места → отсекается ещё фит-чеком до АСБ.

Сценарий ③ — Один (себе)

Состав: 1 взрослый. rooms=1-
Взрослый 1
1 осн. из 2 · dopplacefl=0
— пусто —
2-е место не занято

Шаг B · Подбор номера

Что проверяетсяРезультат для 1 взрослогоfile:line
Наличиепо квоте номера (не по продаваемости одного места)RoomsList.tsx:70-71
Вместимость (только верхняя граница)1 ≤ cap — проходит в любой номер с cap ≥ 1. Двухместный показывается как «подходит», кнопка «Выбрать»RoomsList.tsx:76-81
Проверка oneplaceflОТСУТСТВУЕТ. Двухместный с oneplacefl=0 покажется одиночке доступным. Фронт флага не знает — синк его не сохраняет (слой 1)— (нигде)

Здесь фит-чек недостаточен: верхняя граница пройдена, но реальное ограничение — можно ли продать одно место (oneplacefl) — не проверяется.

Шаг D · Бронь

  1. setletter, 1 осн. место, catplacefl=0частичная загрузка
  2. oneplacefl=1 → бронь проходит (возможно подселение). oneplacefl=0ref=-35 «one main seat is not available»
Итог: ⚠️ один гость бронируется только на номерах oneplacefl=1 (есть у 54 из 169 объектов). На остальных 115 — всегда -35, на любом выбранном номере, даже когда места есть: система нигде на пути не учитывает одноместность, а выкуп (catplacefl=1) мы не шлём. Это и есть кейс «Кристалл Уют».
Где сценарии расходятся: шаги A (санатории) и C (программы) одинаковы. В шаге B фит-чек по вместимости корректен для ① и ② (реальное ограничение — верхняя граница), но недостаточен для ③ (реальное ограничение — oneplacefl, которого система не знает). В шаге D ① и ② срываются только при отсутствии квоты (-21), а ③ — ещё и на oneplacefl=0 (-35).

Справочник: слои пайплайна и происхождение полей

Детали под сквозным путём: как каталог АСБ попадает в наши таблицы, механика фильтра/показа/расчёта и что после брони.

Слой 1 Синхронизация — откуда берётся каждое поле

Крон дёргает read-методы АСБ и раскладывает ответы в Postgres. Оркестратор — services/catalog_sync.py, маппинг — asb/normalize.py.

objects → properties (+ objtyps → type)

Поле АСБ→ колонкаfile:line
objidProperty.id (PK); основа room_id/id программы/ключей цен-квотcatalog_sync.py:179
objnam / objnotname / descriptionnormalize.py:123,135
objtypid + objtyps.objtypnamtypenormalize.py:123
objstars / objphonestars / phonenormalize.py:134,136
hourin / hourouthour_in / hour_outnormalize.py:137-138
child_start_age / child_ageодноимённые (0→NULL)normalize.py:141-142
geoobject.Locality/AdministrativeArea/Country Namecity / region / countrynormalize.py:124-131
geoobject.Latitude/Longitudelat / lonnormalize.py:143-144
img_link только наличиеесть ли главное фото; URL не хранитсяcatalog_sync.py:239

categs → rooms

Поле АСБ→ колонкаfile:line
catcod / catnam / catnotcatcod / name / descriptionnormalize.py:157-159
catbednumcapacity — основные койкиnormalize.py:160
catdopnumdop_capacity — доп. местаnormalize.py:162
catpropids / catgalery / imgcountfeatures+bed / photos / photos_countnormalize.py:150-166

programms → programs · prices → prices · quotes → quotes

Поле АСБ→ колонкаfile:line
programms.progid/pricelistid/prognamprogid/pricelist_id/name(+is_union)normalize.py:179-183
programms.concodconcod — договор (квоты раздельны)normalize.py:181
programms.minlength/maxlength/datein/dateoutmin_nights/max_nights/date_in/date_outnormalize.py:185-188
programms.servcods коды→именаincluded — имена услуг; коды не хранятсяnormalize.py:172-176
prices.catcod/pricelistid/datein/priceprices: интервалы→по-дневно, 77777 отброшенаnormalize.py:84-115
quotes.resdate/catcod/concod/quotevalquotes: сумма по (catcod,дата,договор); может прийти falsecatalog_sync.py:336-349
Производные (НЕ поля АСБ): price_from (min цен), is_union (из имени), bed (из features), удобства/питание/профили (детект по текстам), rating/reviews (наши отзывы).

Что АСБ отдаёт, но мы НЕ сохраняем важно

Поле / методПочему важноfile:line
categs.oneplaceflфлаг одноместного размещения — не хранится, поэтому система не знает, где можно одному (сценарий ③)normalize.py:148-167 (нет)
categs.oneplacedatesдаты, когда одноместное разрешено — теряютсяnormalize.py (нет)
programms.quotestartdays / jsonrestrictionокно открытия квоты / даты-ограничения — не хранятся (проверяет только АСБ)(нет)
check_price_by_objectметод есть, синком не вызывается — нулевой вкладclient.py:113

Слой 2 Фильтрация каталога

ФильтрЛогикаПоле-основа (источник)file:line
Типточное совпадениеtype ← objtypssearch.py:114
ТекстLIKE имя/город/регион← objects/geoobjectsearch.py:117
ГеоOR по 3 уровнямcountry/region/city ← geoobjectsearch.py:125
Цена≥/≤, NULL не прячемprice_from ← производноеsearch.py:136
ЗвёздыIN; «1» включает 0stars ← objstarssearch.py:144
ПрофильORдетект по текстамsearch.py:153
Удобства / пул / питаниеAND / OR / ORдетект по услугам АСБsearch.py:168-196
БолезньOR через программылокальный маппингsearch.py:200

Счётчики фасетов считаются отдельно (GET /filters) глобально по живым объектам — не сужаются под текущий выбор. Ни один фильтр не зависит от числа гостей.

Слой 3 Показ выбора (страница объекта)

В АСБ нет отдельной «цены номера» и «цены программы» — одна цена за ночь/место, зависящая от номера И программы → матрица номер×программа.

ЧтоПоля / логикаfile:line
Номер (RoomItem)capacity(=catbednum), dop_capacity(=catdopnum), features, price_from, program_prices{progid}, available, quota_knownproperties.py:482-495
Матрица ценmin(price) по (room,pricelist) из кэша pricesproperties.py:451-462
Наличие (страница)по room_id: все ночи qty>0; нет данных → True+quota_known=Falseproperties.py:378-406
Наличие (POST /price)по concod: availability{progid:bool}; нет данных → Trueproperties.py:604-624
Цена на странице — из кэша (min синканных цен). Точная (/price) и «от» (/rooms-price) — живой calcsum.

Слой 4 Расчёт цены — calcsum

Живой вызов, queryfl=1 («только расчёт, квоту не проверяет»). По каждому гостю — клиент (первые capacity осн., дальше доп.). Из ответа берём одно поле — price pricing.py:53-58. Запрос: catplacefl=0 (всегда), dopplacefl, agecod, pricelistid=progid.

Слой 5 После брони — поля ответа АСБ

Поле setletterЧто делаемfile:line
ref (строкой)intletcod; ≤0 → бронь отклонена → Order.letcodbooking_jsdata.py:208-215
mesотказ → текст ошибки; успех → № заказа → Order.letregnrbooking_jsdata.py:214-222
priceOrder.pricebooking_jsdata.py:216-219
paysysurlOrder.paysys_url (основа ссылки оплаты)booking_jsdata.py:224
paysysid не хранимпарсится, но в Order не сохраняетсяbooking_jsdata.py:225

Оплата: режим real → всегда paysysurl; stubpaysysurl или наша /pay/{token}. Статус GET /bookings/{token} читает наш Order (не АСБ letters). Документы: url_let_docsref-ссылка (http) → Order.docs_url. Фронт: «Забронировано» → опрос 3с → «Оплачено» + «Скачать документы».

Коды Ответы setletter

refСмыслКогда
>0заказ создан (letcod, №, price, paysysurl)места есть
-2дата заезда в прошломdatein < сегодня
-21Quota is empty — нет квоты на весь номерполная загрузка / выкуп, а мест нет
-35one main seat — нельзя продать отдельное месточастично при oneplacefl=0 или нет места-квоты
-36время заезда ≠ времени программыdatein-время не совпало