У вилл на Бали хаотичное ценообразование. Одна вилла стоит $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) тоже не сразу получился. Сначала пытался хранить все в одной таблице с приоритетами. Вышло нечитаемо. Разнёс по моделям — стало понятнее и проще дебажить.