import requests import hashlib import json from .base_plugin import BasePMSPlugin from datetime import datetime, timedelta from asgiref.sync import sync_to_async from touchh.utils.log import CustomLogger from hotels.models import Hotel, Reservation class RealtyCalendarPlugin(BasePMSPlugin): """Плагин для импорта данных из системы RealtyCalendar.""" def __init__(self, config): super().__init__(config) self.public_key = config.public_key self.private_key = config.private_key self.api_url = config.url.rstrip("/") self.logger = CustomLogger(name="RealtyCalendarPlugin", log_level="DEBUG").get_logger() if not self.public_key or not self.private_key: raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar") def get_default_parser_settings(self): """ Возвращает настройки по умолчанию для обработки данных. """ return { "date_format": "%Y-%m-%dT%H:%M:%S", "timezone": "UTC" } def _get_sorted_keys(self, obj): """ Возвращает отсортированный по имени список ключей. """ sorted_keys = sorted(obj.keys()) self.logger.debug(f"Отсортированные ключи: {sorted_keys}") return sorted_keys def _generate_data_string(self, obj): """ Формирует строку параметров для подписи. """ sorted_keys = self._get_sorted_keys(obj) string = "".join(f"{key}={obj[key]}" for key in sorted_keys) self.logger.debug(f"Сформированная строка данных: {string}") return string + self.private_key def _generate_md5(self, string): """ Генерирует MD5-хеш от строки. """ md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest() self.logger.debug(f"Сформированный MD5-хеш: {md5_hash}") return md5_hash def _generate_sign(self, data): """ Генерирует подпись для данных запроса. """ data_string = self._generate_data_string(data) self.logger.debug(f"Строка для подписи: {data_string}") sign = self._generate_md5(data_string) self.logger.debug(f"Подпись: {sign}") return sign async def _fetch_data(self): """ Выполняет запрос к API RealtyCalendar для получения данных о бронированиях. """ self.logger.debug("Начало выполнения функции _fetch_data") base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/" headers = { "Accept": "application/json", "Content-Type": "application/json", } # Определяем даты выборки now = datetime.now() data = { "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"), "end_date": now.strftime("%Y-%m-%d"), } self.logger.debug(f"Даты выборки: {data}") # Генерация подписи data["sign"] = self._generate_sign(data) # Отправляем запрос self.logger.debug(f"URL запроса: {base_url}") self.logger.debug(f"Заголовки: {headers}") self.logger.debug(f"Данные запроса: {data}") response = requests.post(url=base_url, headers=headers, json=data) self.logger.debug(f"Запрос: {response}") self.logger.debug(f"Статус ответа: {response.status_code}") # self.logger.debug(f"Ответ: {response.text}") if response.status_code == 200: try: response_data = response.json() self.logger.debug(f"Тип данных ответа: {type(response_data)}") if not isinstance(response_data, dict) or "bookings" not in response_data: raise ValueError(f"Неожиданная структура ответа: {response_data}") bookings = response_data.get("bookings", []) # self.logger.debug(f"Полученные данные бронирований: {bookings}") except json.JSONDecodeError as e: self.logger.error(f"Ошибка декодирования JSON: {e}") raise ValueError("Ошибка декодирования JSON ответа.") # Фильтрация данных filtered_data = [ { "id": item.get("id"), "begin_date": item.get("begin_date"), "end_date": item.get("end_date"), "amount": item.get("amount"), "status": item.get("status"), "is_delete": item.get("is_delete"), "apartment_id": item.get("apartment_id"), "prepayment": item.get("prepayment"), "deposit": item.get("deposit"), "source": item.get("source"), "notes": item.get("notes"), } for item in bookings if isinstance(item, dict) and item.get("status") in ["booked", "request"] ] self.logger.debug(f"Отфильтрованные данные: {type(filtered_data)}") for item in filtered_data: self.logger.debug(f"Данные бронирования: {item}") await self._save_to_db(item) from django.utils import timezone async def _save_to_db(self, data): from django.utils import timezone """ Сохраняет данные в БД (например, информацию о номере). """ try: # Проверяем общее количество записей для обработки if not isinstance(data, list): self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}") return total_records = len(data) self.logger.info(f"Общее количество записей для обработки: {total_records}") for index, item in enumerate(data, start=1): try: self.logger.info(f"Обработка записи {index}/{total_records}") # Проверка типа данных if not isinstance(item, dict): self.logger.error(f"Пропущена запись {index}/{total_records}: ожидался dict, но получен {type(item).__name__}") continue # Получаем отель по настройкам PMS hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) self.logger.debug(f"Отель найден: {hotel.name}") # Проверяем, существует ли уже резервация с таким внешним ID reservation_id = item.get('id') if not reservation_id: self.logger.error(f"Пропущена запись {index}/{total_records}: отсутствует 'id' в данных.") continue # Преобразуем даты в "aware" объекты try: check_in = timezone.make_aware(datetime.strptime(item.get('begin_date'), "%Y-%m-%d")) check_out = timezone.make_aware(datetime.strptime(item.get('end_date'), "%Y-%m-%d")) except Exception as e: self.logger.error(f"Ошибка преобразования дат для записи {index}/{total_records}: {e}") continue existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id) # Теперь вызываем .first() после асинхронного вызова existing_reservation = await sync_to_async(existing_reservation.first)() if existing_reservation: self.logger.debug(f"Резервация {reservation_id} уже существует. Обновляем...") await sync_to_async(Reservation.objects.update_or_create)( reservation_id=reservation_id, defaults={ 'room_number': item.get('apartment_id'), 'room_type': 'Описание отсутствует', 'check_in': check_in, 'check_out': check_out, 'status': item.get('status'), 'hotel': hotel } ) self.logger.debug(f"Резервация обновлена.") else: self.logger.debug(f"Резервация не найдена, создаем новую...") reservation = await sync_to_async(Reservation.objects.create)( reservation_id=reservation_id, room_number=item.get('apartment_id'), room_type='Описание отсутствует', check_in=check_in, check_out=check_out, status=item.get('status'), hotel=hotel ) self.logger.debug(f"Новая резервация создана с ID: {reservation.reservation_id}") except Exception as e: self.logger.error(f"Ошибка при обработке записи {index}/{total_records}: {e}") except Exception as e: self.logger.error(f"Ошибка обработки данных в _save_to_db: {e}") def validate_plugin(self): """ Проверка на соответствие требованиям. Можно проверить наличие методов или полей. """ # Проверяем наличие обязательных методов required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"] for m in required_methods: if not hasattr(self, m): raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.") self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.") return True