diff --git a/bot/operations/hotels.py b/bot/operations/hotels.py index cb0c8bd3..bbac570d 100644 --- a/bot/operations/hotels.py +++ b/bot/operations/hotels.py @@ -75,35 +75,48 @@ async def delete_hotel(update: Update, context): await query.edit_message_text("Отель не найден.") -from pms_integration.manager import PMSIntegrationManager -from asgiref.sync import sync_to_async -from hotels.models import Hotel + + # async def check_pms(update, context): # query = update.callback_query # try: -# # Логирование callback_data +# # Получение hotel_id из callback_data # hotel_id = query.data.split("_")[2] # print(f"Selected hotel id: {hotel_id}") # context.user_data["selected_hotel"] = hotel_id # # Инициализация менеджера PMS # pms_manager = PMSIntegrationManager(hotel_id=hotel_id) -# print(f"Loaded hotel: {pms_manager.hotel}") -# await pms_manager.load_hotel() # Асинхронная загрузка отеля -# pms_manager.load_plugin() # Загрузка плагина +# print(f"Инициализация PMS менеджера для отеля ID: {hotel_id}") -# # Получение данных +# # Загрузка данных отеля +# await pms_manager.load_hotel() +# print(f"Данные отеля загружены: {pms_manager.hotel}") + +# # Загрузка плагина +# pms_manager.load_plugin() +# print(f"Плагин загружен: {pms_manager.plugin}") + +# # Получение данных из PMS # data = pms_manager.fetch_data() -# print(f'PMS_managerПолучено записей: {len(data)}\n\n\n___') -# print(f'Данные {data}\n\n\n') -# # Логирование +# print(f"Данные получены из PMS: {len(data)} записей") +# # print(f"Полные данные: {data}\n\n\n") + +# # Сохранение лога успешной интеграции # await pms_manager.save_log("success", f"Успешная интеграция с PMS {pms_manager.pms_config.name}.") + +# # Ответ пользователю # await query.edit_message_text(f"Интеграция успешна! Получено {len(data)} записей.") +# # Обработка данных и запись в БД +# await pms_manager.plugin._save_to_db(data, hotel_id=int(hotel_id)) +# print(f"Данные успешно сохранены в базу данных.") + # except Exception as e: -# # Логирование ошибок +# # Логирование ошибки +# print(f"Ошибка при выполнении check_pms: {e}") # if 'pms_manager' in locals() and pms_manager.hotel: # await pms_manager.save_log("error", str(e)) # await query.edit_message_text(f"❌ Ошибка: {e}") @@ -112,45 +125,45 @@ async def check_pms(update, context): query = update.callback_query try: - # Получение hotel_id из callback_data + # Получение ID отеля из callback_data hotel_id = query.data.split("_")[2] - print(f"Selected hotel id: {hotel_id}") - context.user_data["selected_hotel"] = hotel_id + + # Получение конфигурации отеля и PMS + hotel = await sync_to_async(Hotel.objects.select_related('pms').get)(id=hotel_id) + pms_config = hotel.pms - # Инициализация менеджера PMS + if not pms_config: + await query.edit_message_text("PMS конфигурация не найдена.") + return + + # Создаем экземпляр PMSIntegrationManager pms_manager = PMSIntegrationManager(hotel_id=hotel_id) - print(f"Инициализация PMS менеджера для отеля ID: {hotel_id}") + await sync_to_async(pms_manager.load_hotel)() + await sync_to_async(pms_manager.load_plugin)() - # Загрузка данных отеля - await pms_manager.load_hotel() - print(f"Данные отеля загружены: {pms_manager.hotel}") + # Проверяем, какой способ интеграции использовать + if hasattr(pms_manager.plugin, 'fetch_data'): + # Плагин поддерживает метод fetch_data + data = await sync_to_async(pms_manager.plugin.fetch_data)() + elif pms_config.api_url and pms_config.token: + # Используем прямой запрос к API + from pms_integration.api_client import APIClient + api_client = APIClient(base_url=pms_config.api_url, access_token=pms_config.token) + data = await sync_to_async(api_client.fetch_reservations)() + else: + # Если подходящий способ не найден + await query.edit_message_text("Подходящий способ интеграции с PMS не найден.") + return - # Загрузка плагина - pms_manager.load_plugin() - print(f"Плагин загружен: {pms_manager.plugin}") - - # Получение данных из PMS - data = pms_manager.fetch_data() - print(f"Данные получены из PMS: {len(data)} записей") - # print(f"Полные данные: {data}\n\n\n") - - # Сохранение лога успешной интеграции - await pms_manager.save_log("success", f"Успешная интеграция с PMS {pms_manager.pms_config.name}.") - - # Ответ пользователю - await query.edit_message_text(f"Интеграция успешна! Получено {len(data)} записей.") - - # Обработка данных и запись в БД - await pms_manager.plugin._save_to_db(data, hotel_id=int(hotel_id)) - print(f"Данные успешно сохранены в базу данных.") + # Сохраняем данные в базу + from bot.utils.database import save_reservations + await sync_to_async(save_reservations)(data) + # Уведомляем об успешной интеграции + await query.edit_message_text(f"Интеграция PMS {pms_config.name} завершена успешно.") except Exception as e: - # Логирование ошибки - print(f"Ошибка при выполнении check_pms: {e}") - if 'pms_manager' in locals() and pms_manager.hotel: - await pms_manager.save_log("error", str(e)) - await query.edit_message_text(f"❌ Ошибка: {e}") - + # Обрабатываем и логируем ошибки + await query.edit_message_text(f"Ошибка: {str(e)}") async def setup_rooms(update: Update, context): """Настроить номера отеля.""" diff --git a/bot/utils/database.py b/bot/utils/database.py index 9ac0ceda..f9365091 100644 --- a/bot/utils/database.py +++ b/bot/utils/database.py @@ -34,4 +34,22 @@ async def get_reservations(hotel_id, start_date=None, end_date=None): query = query.filter(check_in__gte=start_date) if end_date: query = query.filter(check_out__lte=end_date) - return await sync_to_async(list)(query.prefetch_related('guests')) \ No newline at end of file + return await sync_to_async(list)(query.prefetch_related('guests')) + +def save_reservations(data): + """ + Сохранение данных бронирований в базу данных. + :param data: Список бронирований. + """ + for booking in data: + Reservation.objects.update_or_create( + external_id=booking['id'], + defaults={ + 'check_in': booking['begin_date'], + 'check_out': booking['end_date'], + 'amount': booking['amount'], + 'notes': booking.get('notes', ''), + 'guest_name': booking['client']['fio'], + 'guest_phone': booking['client']['phone'], + }, + ) \ No newline at end of file diff --git a/hotels/migrations/0001_initial.py b/hotels/migrations/0001_initial.py new file mode 100644 index 00000000..7df79502 --- /dev/null +++ b/hotels/migrations/0001_initial.py @@ -0,0 +1,116 @@ +# Generated by Django 5.1.4 on 2024-12-09 09:30 + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='APIConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Название API')), + ('url', models.URLField(verbose_name='URL API')), + ('token', models.CharField(blank=True, max_length=255, null=True, verbose_name='Токен')), + ('username', models.CharField(blank=True, max_length=255, null=True, verbose_name='Логин')), + ('password', models.CharField(blank=True, max_length=255, null=True, verbose_name='Пароль')), + ('last_updated', models.DateTimeField(auto_now=True, verbose_name='Дата последнего обновления')), + ], + options={ + 'verbose_name': 'Конфигурация API', + 'verbose_name_plural': 'Конфигурации API', + }, + ), + migrations.CreateModel( + name='FraudLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reservation_id', models.BigIntegerField(unique=True, verbose_name='ID бронирования')), + ('guest_name', models.CharField(blank=True, max_length=255, null=True)), + ('check_in_date', models.DateField()), + ('detected_at', models.DateTimeField(auto_now_add=True)), + ('message', models.TextField()), + ], + options={ + 'verbose_name': 'Журнал мошенничества', + 'verbose_name_plural': 'Журналы мошенничества', + }, + ), + migrations.CreateModel( + name='Guest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Имя гостя')), + ('birthdate', models.DateField(blank=True, null=True, verbose_name='Дата рождения')), + ('phone', models.CharField(blank=True, max_length=50, null=True, verbose_name='Телефон')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')), + ], + options={ + 'verbose_name': 'Гость', + 'verbose_name_plural': 'Гости', + }, + ), + migrations.CreateModel( + name='Reservation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reservation_id', models.BigIntegerField(unique=True, verbose_name='ID бронирования')), + ('room_number', models.CharField(max_length=50, verbose_name='Номер комнаты')), + ('room_type', models.CharField(max_length=255, verbose_name='Тип комнаты')), + ('check_in', models.DateTimeField(verbose_name='Дата заезда')), + ('check_out', models.DateTimeField(verbose_name='Дата выезда')), + ('status', models.CharField(max_length=50, verbose_name='Статус')), + ('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Цена')), + ('discount', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Скидка')), + ], + options={ + 'verbose_name': 'Бронирование', + 'verbose_name_plural': 'Бронирования', + }, + ), + migrations.CreateModel( + name='UserHotel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + options={ + 'verbose_name': 'Пользователь отеля', + 'verbose_name_plural': 'Пользователи отелей', + }, + ), + migrations.CreateModel( + name='APIRequestLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('request_time', models.DateTimeField(auto_now_add=True, verbose_name='Время запроса')), + ('response_status', models.IntegerField(validators=[django.core.validators.MinValueValidator(100), django.core.validators.MaxValueValidator(599)], verbose_name='HTTP статус ответа')), + ('response_data', models.JSONField(blank=True, null=True, verbose_name='Данные ответа')), + ('api', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.apiconfiguration', verbose_name='API')), + ], + options={ + 'verbose_name': 'Журнал запросов API', + 'verbose_name_plural': 'Журналы запросов API', + }, + ), + migrations.CreateModel( + name='Hotel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Название отеля')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создан')), + ('api', models.OneToOneField(blank=True, help_text='API, связанный с этим отелем.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.apiconfiguration', verbose_name='API')), + ], + options={ + 'verbose_name': 'Отель', + 'verbose_name_plural': 'Отели', + }, + ), + ] diff --git a/hotels/migrations/0002_initial.py b/hotels/migrations/0002_initial.py new file mode 100644 index 00000000..5cf7f3a9 --- /dev/null +++ b/hotels/migrations/0002_initial.py @@ -0,0 +1,42 @@ +# Generated by Django 5.1.4 on 2024-12-09 09:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('hotels', '0001_initial'), + ('pms_integration', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='hotel', + name='pms', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pms_integration.pmsconfiguration', verbose_name='PMS система'), + ), + migrations.AddField( + model_name='fraudlog', + name='hotel', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frauds', to='hotels.hotel'), + ), + migrations.AddField( + model_name='reservation', + name='hotel', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель'), + ), + migrations.AddField( + model_name='guest', + name='reservation', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guests', to='hotels.reservation', verbose_name='Бронирование'), + ), + migrations.AddField( + model_name='userhotel', + name='hotel', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hotel_users', to='hotels.hotel', verbose_name='Отель'), + ), + ] diff --git a/hotels/migrations/0003_initial.py b/hotels/migrations/0003_initial.py new file mode 100644 index 00000000..1bf5dc33 --- /dev/null +++ b/hotels/migrations/0003_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.4 on 2024-12-09 09:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('hotels', '0002_initial'), + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='userhotel', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotels', to='users.user', verbose_name='Пользователь'), + ), + migrations.AddIndex( + model_name='apirequestlog', + index=models.Index(fields=['api'], name='hotels_apir_api_id_686bb0_idx'), + ), + migrations.AddIndex( + model_name='apirequestlog', + index=models.Index(fields=['request_time'], name='hotels_apir_request_f65147_idx'), + ), + ] diff --git a/hotels/migrations/__init__.py b/hotels/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hotels/models.py b/hotels/models.py index 9dc4ffc0..022f4aba 100644 --- a/hotels/models.py +++ b/hotels/models.py @@ -118,13 +118,18 @@ class Guest(models.Model): verbose_name = "Гость" verbose_name_plural = "Гости" + class FraudLog(models.Model): hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="frauds") - reservation_id = models.CharField(max_length=255) + reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования") guest_name = models.CharField(max_length=255, null=True, blank=True) check_in_date = models.DateField() detected_at = models.DateTimeField(auto_now_add=True) message = models.TextField() def __str__(self): - return f"FRAUD: {self.guest_name} ({self.check_in_date})" \ No newline at end of file + return f"FRAUD: {self.guest_name} ({self.check_in_date})" + + class Meta: + verbose_name = "Журнал мошенничества" + verbose_name_plural = "Журналы мошенничества" \ No newline at end of file diff --git a/pms_integration/migrations/0001_initial.py b/pms_integration/migrations/0001_initial.py index 8229ed7b..e21fe089 100644 --- a/pms_integration/migrations/0001_initial.py +++ b/pms_integration/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2024-12-09 04:50 +# Generated by Django 5.1.4 on 2024-12-09 09:30 import django.db.models.deletion from django.db import migrations, models @@ -9,7 +9,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('hotels', '__first__'), + ('hotels', '0001_initial'), ] operations = [ diff --git a/pms_integration/plugins/realtycalendar_pms.py b/pms_integration/plugins/realtycalendar_pms.py new file mode 100644 index 00000000..f7316142 --- /dev/null +++ b/pms_integration/plugins/realtycalendar_pms.py @@ -0,0 +1,78 @@ +import hashlib +import requests +import json +from datetime import datetime +from hotels.models import Reservation + +from pms_integration.plugins.base_plugin import BasePMSPlugin + + +class RealtyCalendarPlugin(BasePMSPlugin): + """ + Плагин для взаимодействия с RealtyCalendar. + """ + def __init__(self, config): + super().__init__(config) + self.public_key = config.token # Используем `token` как публичный ключ + self.private_key = config.password # Используем `password` как приватный ключ + self.base_url = config.url + + def generate_sign(self, params): + """ + Генерация подписи запроса. + :param params: Параметры запроса. + :return: Подпись. + """ + sorted_keys = sorted(params.keys()) + data_string = ''.join(f"{key}={params[key]}" for key in sorted_keys) + sign_string = f"{data_string}{self.private_key}" + return hashlib.md5(sign_string.encode('utf-8')).hexdigest() + + def fetch_data(self, start_date=None, end_date=None): + """ + Получение данных из RealtyCalendar. + :param start_date: Начальная дата (формат YYYY-MM-DD). + :param end_date: Конечная дата (формат YYYY-MM-DD). + :return: Список данных бронирования. + """ + if not start_date: + start_date = datetime.now().strftime('%Y-%m-%d') + if not end_date: + end_date = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d') + + params = { + 'begin_date': start_date, + 'end_date': end_date, + } + params['sign'] = self.generate_sign(params) + + url = f"{self.base_url}/bookings/{self.public_key}/" + headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + } + + response = requests.post(url, json=params, headers=headers) + response.raise_for_status() + + data = response.json() + return data.get('bookings', []) + + @staticmethod + def save_data(bookings): + """ + Сохранение данных бронирования в базу данных. + :param bookings: Список бронирований. + """ + for booking in bookings: + Reservation.objects.update_or_create( + external_id=booking['id'], + defaults={ + 'check_in': booking['begin_date'], + 'check_out': booking['end_date'], + 'amount': booking['amount'], + 'notes': booking.get('notes', ''), + 'guest_name': booking['client']['fio'], + 'guest_phone': booking['client']['phone'], + }, + ) diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index 9157ca0e..acfcf34a 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2024-12-09 04:50 +# Generated by Django 5.1.4 on 2024-12-09 09:30 import django.contrib.auth.models import django.contrib.auth.validators