У вилл на Бали хаотичное ценообразование. Одна вилла стоит $200 в будни и $350 в выходные. На Новый год — $500. Для конкретного гостя — скидка 15%. А ещё есть минимальный срок проживания: 3 ночи в высокий сезон, 2 в низкий. Всё это нужно синхронизировать с Booking.com, Airbnb и Agoda через Channex.
155 коммитов за пять рабочих дней (27-31 января и 1 февраля). Самый объёмный спринт за всю историю проекта.
Date overrides: своя цена на каждый день
Первая задача — дать менеджеру возможность задать цену на конкретную дату. Модель DateOverride: вилла, дата, цена, причина. Простая таблица, но с ней начинаются сложности.
Менеджер ставит $400 на 14 февраля. Эта цена должна уйти в Channex, оттуда — в Booking.com и Airbnb. Немедленно. Не через 5 минут, не по крону — сразу.
Решил через Django signals. При сохранении DateOverride срабатывает post_save signal, который ставит Celery task на синхронизацию с Channex API. Task берёт все overrides для виллы на ближайшие 365 дней и отправляет batch-запросом.
@receiver(post_save, sender=DateOverride)
def sync_override_to_channex(sender, instance, **kwargs):
from apps.channex.tasks import sync_villa_availability
sync_villa_availability.delay(
villa_id=instance.villa_id,
date_from=instance.date.isoformat(),
date_to=instance.date.isoformat(),
)
Лог изменений: ChangeLog фиксирует кто, когда и что поменял. Старое значение, новое значение, тип изменения. Для аудита и для отладки — когда цена на OTA не совпадает, можно посмотреть историю.
UI: месячная сетка
Календарь — месячная сетка с ценами в каждой ячейке. Цвет ячейки зависит от источника цены: базовая — серая, override — синяя, заблокированная дата — красная. Toolbar сверху: переключение месяцев, фильтр по виллам, кнопка bulk-edit.
Клик по дате открывает модальное окно с полями: цена, минимальный срок, check-in разрешён (да/нет), причина изменения. Можно выделить диапазон дат и применить изменения ко всем сразу.
Каждая ячейка показывает: цену, иконку ограничения (если есть min stay), статус синхронизации (зелёная галочка или жёлтый спиннер). Hover — детали: источник цены, дата последней синхронизации, ограничения.
Первая версия рендерилась медленно — 400ms на перерисовку месяца с 30 виллами. Проблема была в том, что каждая ячейка делала отдельный запрос за статусом синхронизации. Переделал на batch-загрузку: один запрос на весь месяц для всех вилл.
Сложная часть: архитектура комплексов
На Бали есть отдельные виллы и есть комплексы — группы вилл под одним управлением. Комплекс «Bali Sunset Villas» может включать 5 одинаковых вилл с 2 спальнями и 3 виллы с 3 спальнями. На OTA они продаются как один листинг с типом «2 Bedroom Villa» — а конкретная вилла назначается при заезде.
Для этого появилась модель VillaType — группировка вилл по типу внутри комплекса. Тип определяет: количество спален, площадь, вместимость, набор удобств. Конкретные виллы привязываются к типу.
Channex работает с property (комплекс) и room type (тип виллы). Маппинг: один Complex = один Channex property, один VillaType = один Channex room type. При бронировании нужно автоматически назначить конкретную виллу из доступных.
AvailabilityCalculator — класс, который считает доступность по типу виллы. Берёт все виллы этого типа, вычитает забронированные и заблокированные на дату, возвращает количество доступных единиц. Это число уходит в Channex как availability.
class AvailabilityCalculator:
def get_availability(self, villa_type_id: int, date: date) -> int:
total = Villa.objects.filter(villa_type_id=villa_type_id).count()
booked = Booking.objects.filter(
villa__villa_type_id=villa_type_id,
check_in__lte=date,
check_out__gt=date,
status__in=['confirmed', 'checked_in'],
).values('villa_id').distinct().count()
blocked = DateOverride.objects.filter(
villa__villa_type_id=villa_type_id,
date=date,
is_blocked=True,
).values('villa_id').distinct().count()
return max(0, total - booked - blocked)
Назначение виллы при бронировании — отдельный алгоритм. Выбирает виллу с наименьшим количеством бронирований за период (равномерная загрузка) и без конфликтов по датам.
VillaRatePlan: источник правды для цен
Самая запутанная часть. На OTA может быть несколько тарифов для одного типа виллы: стандартный, невозвратный, раннее бронирование. У каждого — своя цена и свои ограничения.
Модель VillaRatePlan: тип виллы, название тарифа, базовая цена, валюта, ограничения (min stay, max stay, closed to arrival, closed to departure). Плюс ссылка на Channex rate plan ID.
Rate plan templates — шаблоны для быстрого создания. «Стандартный» — без ограничений. «Невозвратный» — скидка 10%, min stay 3 ночи. «Early Bird» — скидка 15%, бронирование за 30+ дней. Шаблон применяется к типу виллы, создавая конкретный VillaRatePlan.
Иерархия цен: шаблон тарифа -> базовая цена rate plan -> сезонные корректировки -> date override. Каждый следующий уровень перезаписывает предыдущий. Date override всегда побеждает. Этот каскад реализован в методе get_effective_price(villa_type, rate_plan, date).
Per-rate-plan restrictions: для каждого тарифа свои ограничения по датам. Стандартный тариф открыт круглый год, невозвратный закрыт в низкий сезон. Модальное окно в UI показывает сетку: строки — тарифы, столбцы — даты, ячейки — open/closed.
Sync Details UI
Синхронизация с Channex ломается. Часто. Неправильный формат даты, превышен rate limit, таймаут на стороне Channex. Нужна прозрачная диагностика.
Страница Sync Details: summary-блоки сверху (успешных/неуспешных/в процессе), фильтры по дате и статусу. Ниже — collapsible-секции по каждому комплексу. Внутри — таблица с human-readable логами: «14 фев 2026, 10:32 — Цена на 2BR Villa обновлена: $200->$350 — OK» или «14 фев 2026, 10:33 — Rate limit exceeded, retry через 60 сек».
Booking redesign
Параллельно переделал страницу бронирований. Убрал простую таблицу, добавил filter tabs: All, Upcoming, Current, Past, Cancelled. Каждый tab показывает count. Строки стали expandable — клик раскрывает детали: гость, виллы, платежи, change log.
Сертификация Channex
Последние два дня ушли на подготовку к сертификации. Channex требует: скриншоты всех экранов, документация по архитектуре, test cases для каждого API-метода, описание обработки ошибок.
Подготовил пакет: 23 скриншота, 8 страниц документации, 15 тест-кейсов. Написал сертификационное письмо с описанием системы и контактами.
Результат
155 коммитов за 5 дней. Calendar Hub с date overrides, авто-синхронизацией через Django signals, комплексной архитектурой вилл (VillaType, VillaRatePlan), rate plan templates. Полностью переработанный Booking UI. Пакет документов для сертификации Channex.
Больше всего времени ушло на архитектуру комплексов — три раза переделывал маппинг между внутренними моделями и Channex API. Первый вариант был «одна вилла = один room type». Второй — «один комплекс = один property, все виллы — один room type». Финальный — группировка по VillaType — оказался правильным, но добрался до него только через ошибки.
Каскад цен (шаблон -> rate plan -> сезон -> override) тоже не сразу получился. Сначала пытался хранить все в одной таблице с приоритетами. Вышло нечитаемо. Разнёс по моделям — стало понятнее и проще дебажить.