Compare commits
14 Commits
2c1157b116
...
PMSManager
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ad382f01a | |||
| 67e487c378 | |||
| 0cc3e469cb | |||
| d771df32d5 | |||
| 51569fb5c6 | |||
| 6721ae3e8c | |||
| 24f1a40561 | |||
|
|
d8bb7493e3 | ||
|
|
aca071f450 | ||
|
|
6b98cda299 | ||
| d826232dca | |||
| 05509f79fb | |||
| 157f47d86d | |||
| e5e7a7f054 |
19
.drone.yml
19
.drone.yml
@@ -12,20 +12,11 @@ steps:
|
|||||||
- git reset --hard $DRONE_COMMIT
|
- git reset --hard $DRONE_COMMIT
|
||||||
|
|
||||||
# Шаг 2: Обновление и запуск с помощью update.sh
|
# Шаг 2: Обновление и запуск с помощью update.sh
|
||||||
- name: deploy_app
|
- name: docker-build
|
||||||
image: docker:24
|
image: plugins/docker
|
||||||
environment:
|
settings:
|
||||||
MYSQL_PASSWORD: touchh
|
repo: trevor198507/touchh-py
|
||||||
volumes:
|
dry_run: true
|
||||||
- name: docker_sock
|
|
||||||
path: /var/run/docker.sock
|
|
||||||
commands:
|
|
||||||
- apk add --no-cache bash
|
|
||||||
- chmod +x ./bin/update
|
|
||||||
- docker-compose up -d
|
|
||||||
- until docker inspect -f '{{.State.Running}}' src-web-1 | grep true; do echo "Waiting for container to be running..."; sleep 5; done
|
|
||||||
- git branch --set-upstream-to=origin/PMSManager_refactor PMSManager_refactor || true
|
|
||||||
- ./bin/update
|
|
||||||
|
|
||||||
# Шаг 3: Миграция базы данных
|
# Шаг 3: Миграция базы данных
|
||||||
- name: run_migrations
|
- name: run_migrations
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ class UserActivityLogAdmin(admin.ModelAdmin):
|
|||||||
get_hotel_name.short_description = "Отель"
|
get_hotel_name.short_description = "Отель"
|
||||||
get_room_number.short_description = "Комната"
|
get_room_number.short_description = "Комната"
|
||||||
|
|
||||||
|
|
||||||
# from .views import import_selected_hotels
|
# from .views import import_selected_hotels
|
||||||
# # Регистрируем admin класс для ImportedHotel
|
# # Регистрируем admin класс для ImportedHotel
|
||||||
# @admin.register(ImportedHotel)
|
# @admin.register(ImportedHotel)
|
||||||
@@ -247,4 +248,3 @@ class RoomDiscrepancyAdmin(admin.ModelAdmin):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = RoomDiscrepancy
|
model = RoomDiscrepancy
|
||||||
fields = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']
|
fields = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']
|
||||||
|
|
||||||
@@ -1,27 +1,149 @@
|
|||||||
|
# import json
|
||||||
|
# from datetime import timedelta
|
||||||
|
# from django.utils import timezone
|
||||||
|
# from django.db.models import Q
|
||||||
|
# from hotels.models import Reservation, Hotel
|
||||||
|
# from .models import UserActivityLog, RoomDiscrepancy
|
||||||
|
# from touchh.utils.log import CustomLogger
|
||||||
|
|
||||||
|
# # Настройка логирования
|
||||||
|
# logger = CustomLogger(__name__).get_logger()
|
||||||
|
|
||||||
|
# class ReservationChecker:
|
||||||
|
# """
|
||||||
|
# Класс для проверки несоответствий между бронированиями и логами заселения.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# def __init__(self):
|
||||||
|
# self.checkin_diff_hours = 3
|
||||||
|
|
||||||
|
# def log_info(self, message):
|
||||||
|
# logger.info(message)
|
||||||
|
|
||||||
|
# def log_warning(self, message):
|
||||||
|
# logger.warning(message)
|
||||||
|
|
||||||
|
# def log_error(self, message):
|
||||||
|
# logger.error(message)
|
||||||
|
|
||||||
|
# def run_check(self):
|
||||||
|
# """Запуск проверки фродовых событий."""
|
||||||
|
# self.log_info("Запуск проверки фродовых данных.")
|
||||||
|
# try:
|
||||||
|
# check_in_diff = timedelta(hours=self.checkin_diff_hours)
|
||||||
|
|
||||||
|
# # Кэшируем отели в словарь для быстрого доступа
|
||||||
|
# hotels_map = {hotel.hotel_id: hotel for hotel in Hotel.objects.all()}
|
||||||
|
|
||||||
|
# # Загружаем бронирования и активности пользователей
|
||||||
|
# user_logs = UserActivityLog.objects.filter(fraud_checked=False)
|
||||||
|
# reservations = Reservation.objects.filter(fraud_checked=False).select_related('hotel')
|
||||||
|
|
||||||
|
# # Преобразуем бронирования в словарь для быстрого поиска
|
||||||
|
# reservations_map = {
|
||||||
|
# (res.hotel.hotel_id, res.room_number): res for res in reservations
|
||||||
|
# }
|
||||||
|
|
||||||
|
# violations = []
|
||||||
|
# missing_reservations = set(reservations) # Сет для поиска пропавших бронирований
|
||||||
|
|
||||||
|
# for user_log in user_logs:
|
||||||
|
# try:
|
||||||
|
# params = json.loads(user_log.url_parameters.replace("'", '"')) if user_log.url_parameters else {}
|
||||||
|
# hotel_id = params.get('utm_content')
|
||||||
|
# room = params.get('utm_term')
|
||||||
|
|
||||||
|
# if not hotel_id or not room:
|
||||||
|
# continue # Пропускаем записи без нужных параметров
|
||||||
|
|
||||||
|
# key = (hotel_id, room)
|
||||||
|
# reserv = reservations_map.get(key)
|
||||||
|
|
||||||
|
# discrepancy_type = None
|
||||||
|
|
||||||
|
# if reserv:
|
||||||
|
# if reserv in missing_reservations:
|
||||||
|
# missing_reservations.remove(reserv)
|
||||||
|
|
||||||
|
# if user_log.date_time < reserv.check_in:
|
||||||
|
# discrepancy_type = 'early'
|
||||||
|
# elif user_log.date_time > reserv.check_in + check_in_diff:
|
||||||
|
# discrepancy_type = 'late'
|
||||||
|
# else:
|
||||||
|
# discrepancy_type = 'no_booking'
|
||||||
|
|
||||||
|
# if discrepancy_type:
|
||||||
|
# violations.append(RoomDiscrepancy(
|
||||||
|
# hotel=hotels_map.get(hotel_id),
|
||||||
|
# room_number=room,
|
||||||
|
# discrepancy_type=discrepancy_type,
|
||||||
|
# booking_id=reserv.reservation_id if reserv else None,
|
||||||
|
# check_in_date_expected=reserv.check_in if reserv else None,
|
||||||
|
# check_in_date_actual=user_log.date_time,
|
||||||
|
# ))
|
||||||
|
|
||||||
|
# user_log.fraud_checked = True # Отмечаем логи как проверенные
|
||||||
|
|
||||||
|
# except json.JSONDecodeError:
|
||||||
|
# self.log_error(f"Ошибка декодирования JSON в URL-параметрах: {user_log.url_parameters}")
|
||||||
|
# except Exception as e:
|
||||||
|
# self.log_error(f"Ошибка при обработке логов: {e}")
|
||||||
|
|
||||||
|
# # Добавляем пропущенные бронирования
|
||||||
|
# for miss_reserv in missing_reservations:
|
||||||
|
# violations.append(RoomDiscrepancy(
|
||||||
|
# hotel=miss_reserv.hotel,
|
||||||
|
# room_number=miss_reserv.room_number,
|
||||||
|
# discrepancy_type='missed',
|
||||||
|
# booking_id=miss_reserv.reservation_id,
|
||||||
|
# check_in_date_expected=miss_reserv.check_in,
|
||||||
|
# ))
|
||||||
|
|
||||||
|
# # Массово сохраняем нарушения
|
||||||
|
# if violations:
|
||||||
|
# RoomDiscrepancy.objects.bulk_create(violations)
|
||||||
|
# self.log_info(f"Записано {len(violations)} новых несоответствий.")
|
||||||
|
|
||||||
|
# # Обновляем флаги fraud_checked
|
||||||
|
# UserActivityLog.objects.filter(id__in=[log.id for log in user_logs]).update(fraud_checked=True)
|
||||||
|
# Reservation.objects.filter(id__in=[res.id for res in reservations]).update(fraud_checked=True)
|
||||||
|
|
||||||
|
# except Exception as e:
|
||||||
|
# self.log_error(f"Ошибка при выполнении проверки: {e}")
|
||||||
|
|
||||||
|
# self.log_info("Проверка завершена.")
|
||||||
|
|
||||||
|
# # Функция для запуска из планировщика
|
||||||
|
# def run_reservation_check():
|
||||||
|
# """Запуск проверки через планировщик."""
|
||||||
|
# logger.info("Планировщик вызывает run_reservation_check.")
|
||||||
|
# try:
|
||||||
|
# checker = ReservationChecker()
|
||||||
|
# checker.run_check()
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"Ошибка при запуске проверки: {e}")
|
||||||
|
# logger.info("run_reservation_check завершена.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from urllib.parse import parse_qs
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from hotels.models import Reservation, Hotel
|
from hotels.models import Reservation, Hotel
|
||||||
from .models import UserActivityLog, ViolationLog, RoomDiscrepancy
|
from .models import UserActivityLog, RoomDiscrepancy
|
||||||
from touchh.utils.log import CustomLogger
|
from touchh.utils.log import CustomLogger
|
||||||
|
|
||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
logger = CustomLogger(__name__).get_logger()
|
logger = CustomLogger(__name__).get_logger()
|
||||||
|
|
||||||
|
|
||||||
class ReservationChecker:
|
class ReservationChecker:
|
||||||
"""
|
"""
|
||||||
Класс для проверки несоответствий между бронированиями и логами заселения.
|
Класс для проверки несоответствий между бронированиями и логами заселения.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""
|
self.checkin_diff_hours = 3 # Разрешенное отклонение от времени заселения
|
||||||
Инициализация времени проверки и списка нарушений.
|
|
||||||
"""
|
|
||||||
self.start_time = timezone.now() - timedelta(days=30)
|
|
||||||
self.end_time = timezone.now()
|
|
||||||
self.violations = []
|
|
||||||
self.checkin_diff_hours = 3
|
|
||||||
|
|
||||||
def log_info(self, message):
|
def log_info(self, message):
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
@@ -32,112 +154,106 @@ class ReservationChecker:
|
|||||||
def log_error(self, message):
|
def log_error(self, message):
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
|
|
||||||
def fetch_user_logs(self):
|
|
||||||
try:
|
|
||||||
self.log_info("Начинается извлечение логов активности пользователей.")
|
|
||||||
user_logs = UserActivityLog.objects.filter(created__range=(self.start_time, self.end_time))
|
|
||||||
self.log_info(f"Найдено {user_logs.count()} логов активности для анализа.")
|
|
||||||
return user_logs
|
|
||||||
except Exception as e:
|
|
||||||
self.log_error(f"Ошибка при извлечении логов активности: {e}")
|
|
||||||
return UserActivityLog.objects.none()
|
|
||||||
|
|
||||||
def fetch_reservations(self):
|
|
||||||
try:
|
|
||||||
self.log_info("Начинается извлечение бронирований.")
|
|
||||||
reservations = Reservation.objects.filter(
|
|
||||||
Q(check_in__lte=self.end_time) & Q(check_out__gte=self.start_time)
|
|
||||||
)
|
|
||||||
self.log_info(f"Найдено {reservations.count()} бронирований для анализа.")
|
|
||||||
return reservations
|
|
||||||
except Exception as e:
|
|
||||||
self.log_error(f"Ошибка при извлечении бронирований: {e}")
|
|
||||||
return Reservation.objects.none()
|
|
||||||
|
|
||||||
def find_violations(self):
|
|
||||||
self.log_info("Начинается анализ несоответствий.")
|
|
||||||
user_logs = self.fetch_user_logs()
|
|
||||||
reservations = self.fetch_reservations()
|
|
||||||
|
|
||||||
log_lookup = {}
|
|
||||||
for log in user_logs:
|
|
||||||
params = parse_qs(log.url_parameters or "")
|
|
||||||
hotel_id = params.get("utm_content", [None])[0]
|
|
||||||
room_number = params.get("utm_term", [None])[0]
|
|
||||||
if hotel_id and room_number:
|
|
||||||
key = (hotel_id, room_number)
|
|
||||||
log_lookup.setdefault(key, []).append(log)
|
|
||||||
|
|
||||||
for reservation in reservations:
|
|
||||||
key = (reservation.hotel.hotel_id, reservation.room_number)
|
|
||||||
logs = log_lookup.get(key, [])
|
|
||||||
|
|
||||||
if reservation.status == "заселен" and not logs:
|
|
||||||
self.record_violation(
|
|
||||||
hotel=reservation.hotel,
|
|
||||||
room_number=reservation.room_number,
|
|
||||||
violation_type="no_qr_scan",
|
|
||||||
details=f"Бронирование для номера {reservation.room_number} в отеле '{reservation.hotel.name}' "
|
|
||||||
f"не имеет записи сканирования QR."
|
|
||||||
)
|
|
||||||
|
|
||||||
for log in logs:
|
|
||||||
if log.created < reservation.check_in:
|
|
||||||
self.record_violation(
|
|
||||||
hotel=reservation.hotel,
|
|
||||||
room_number=reservation.room_number,
|
|
||||||
violation_type="early_check_in",
|
|
||||||
details=f"Раннее заселение: сканирование QR {log.created} раньше времени заезда "
|
|
||||||
f"{reservation.check_in} для номера {reservation.room_number}."
|
|
||||||
)
|
|
||||||
|
|
||||||
for (hotel_id, room_number), logs in log_lookup.items():
|
|
||||||
matching_reservations = reservations.filter(
|
|
||||||
hotel__hotel_id=hotel_id,
|
|
||||||
room_number=room_number
|
|
||||||
)
|
|
||||||
if not matching_reservations.exists():
|
|
||||||
for log in logs:
|
|
||||||
self.record_violation(
|
|
||||||
hotel=Hotel.objects.filter(hotel_id=hotel_id).first(),
|
|
||||||
room_number=room_number,
|
|
||||||
violation_type="no_reservation",
|
|
||||||
details=f"Сканирование QR {log.created} для номера {room_number} в отеле с ID '{hotel_id}' "
|
|
||||||
f"не соответствует ни одному бронированию."
|
|
||||||
)
|
|
||||||
|
|
||||||
def record_violation(self, hotel, room_number, violation_type, details):
|
|
||||||
if hotel:
|
|
||||||
self.violations.append(ViolationLog(
|
|
||||||
hotel=hotel,
|
|
||||||
room_number=room_number,
|
|
||||||
violation_type=violation_type,
|
|
||||||
violation_details=details
|
|
||||||
))
|
|
||||||
self.log_warning(f"Зафиксировано нарушение: {details}")
|
|
||||||
|
|
||||||
def save_violations(self):
|
|
||||||
if self.violations:
|
|
||||||
ViolationLog.objects.bulk_create(self.violations)
|
|
||||||
self.log_info(f"Создано {len(self.violations)} записей в ViolationLog.")
|
|
||||||
else:
|
|
||||||
self.log_info("Нарушений не обнаружено.")
|
|
||||||
|
|
||||||
def run_check(self):
|
def run_check(self):
|
||||||
self.log_info(f"Запуск проверки с {self.start_time} по {self.end_time}.")
|
"""Запуск проверки фродовых событий."""
|
||||||
|
self.log_info("🔍 Запуск проверки фродовых данных.")
|
||||||
try:
|
try:
|
||||||
self.find_violations()
|
check_in_diff = timedelta(hours=self.checkin_diff_hours)
|
||||||
self.save_violations()
|
|
||||||
|
# Кэшируем отели в словарь для быстрого доступа
|
||||||
|
hotels_map = {hotel.hotel_id: hotel for hotel in Hotel.objects.all()}
|
||||||
|
|
||||||
|
# Загружаем бронирования и активности пользователей
|
||||||
|
user_logs = UserActivityLog.objects.filter(fraud_checked=False)
|
||||||
|
reservations = Reservation.objects.filter(fraud_checked=False).select_related('hotel')
|
||||||
|
|
||||||
|
# Преобразуем бронирования в словарь для быстрого поиска
|
||||||
|
reservations_map = {
|
||||||
|
(res.hotel.hotel_id, res.room_number): res for res in reservations
|
||||||
|
}
|
||||||
|
|
||||||
|
violations = []
|
||||||
|
checked_reservations = set() # Сет для бронирований, которые были проверены
|
||||||
|
|
||||||
|
self.log_info(f"✅ Загружено {len(user_logs)} логов активности и {len(reservations)} бронирований.")
|
||||||
|
|
||||||
|
for user_log in user_logs:
|
||||||
|
try:
|
||||||
|
params = json.loads(user_log.url_parameters.replace("'", '"')) if user_log.url_parameters else {}
|
||||||
|
hotel_id = params.get('utm_content')
|
||||||
|
room = params.get('utm_term')
|
||||||
|
|
||||||
|
if not hotel_id or not room:
|
||||||
|
self.log_warning(f"🚫 Пропущен лог без hotel_id или room_number: {user_log.url_parameters}")
|
||||||
|
continue # Пропускаем записи без нужных параметров
|
||||||
|
|
||||||
|
key = (hotel_id, room)
|
||||||
|
reserv = reservations_map.get(key)
|
||||||
|
|
||||||
|
discrepancy_type = "match" # По умолчанию считаем, что всё соответствует
|
||||||
|
|
||||||
|
if reserv:
|
||||||
|
checked_reservations.add(reserv)
|
||||||
|
|
||||||
|
if user_log.date_time < reserv.check_in:
|
||||||
|
discrepancy_type = 'early'
|
||||||
|
self.log_warning(f"⚠️ Обнаружено раннее заселение: {user_log.date_time} < {reserv.check_in}")
|
||||||
|
elif user_log.date_time > reserv.check_in + check_in_diff:
|
||||||
|
discrepancy_type = 'late'
|
||||||
|
self.log_warning(f"⚠️ Обнаружено позднее заселение: {user_log.date_time} > {reserv.check_in + check_in_diff}")
|
||||||
|
else:
|
||||||
|
discrepancy_type = 'no_booking'
|
||||||
|
self.log_warning(f"🚨 Заселение без бронирования: {user_log.date_time} (Отель {hotel_id}, Комната {room})")
|
||||||
|
|
||||||
|
violations.append(RoomDiscrepancy(
|
||||||
|
hotel=hotels_map.get(hotel_id),
|
||||||
|
room_number=room,
|
||||||
|
discrepancy_type=discrepancy_type,
|
||||||
|
booking_id=reserv.reservation_id if reserv else None,
|
||||||
|
check_in_date_expected=reserv.check_in if reserv else None,
|
||||||
|
check_in_date_actual=user_log.date_time,
|
||||||
|
))
|
||||||
|
|
||||||
|
user_log.fraud_checked = True # Отмечаем логи как проверенные
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.log_error(f"❌ Ошибка декодирования JSON в URL-параметрах: {user_log.url_parameters}")
|
||||||
|
except Exception as e:
|
||||||
|
self.log_error(f"❌ Ошибка при обработке логов: {e}")
|
||||||
|
|
||||||
|
# Добавляем пропущенные бронирования (неявки)
|
||||||
|
for reserv in reservations:
|
||||||
|
if reserv not in checked_reservations:
|
||||||
|
violations.append(RoomDiscrepancy(
|
||||||
|
hotel=reserv.hotel,
|
||||||
|
room_number=reserv.room_number,
|
||||||
|
discrepancy_type='missed',
|
||||||
|
booking_id=reserv.reservation_id,
|
||||||
|
check_in_date_expected=reserv.check_in,
|
||||||
|
))
|
||||||
|
self.log_warning(f"⚠️ Обнаружена неявка (missed) | Отель: {reserv.hotel.hotel_id}, Номер: {reserv.room_number}, Ожидаемая дата заезда: {reserv.check_in}")
|
||||||
|
|
||||||
|
# Массово сохраняем все записи, включая корректные совпадения
|
||||||
|
if violations:
|
||||||
|
RoomDiscrepancy.objects.bulk_create(violations)
|
||||||
|
self.log_info(f"✅ Записано {len(violations)} новых записей в RoomDiscrepancy.")
|
||||||
|
|
||||||
|
# Обновляем флаги fraud_checked
|
||||||
|
UserActivityLog.objects.filter(id__in=[log.id for log in user_logs]).update(fraud_checked=True)
|
||||||
|
Reservation.objects.filter(id__in=[res.id for res in reservations]).update(fraud_checked=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_error(f"Ошибка при выполнении проверки: {e}")
|
self.log_error(f"❌ Ошибка при выполнении проверки: {e}")
|
||||||
self.log_info("Проверка завершена.")
|
|
||||||
|
self.log_info("✅ Проверка фродовых данных завершена.")
|
||||||
|
|
||||||
# Функция для запуска из планировщика
|
# Функция для запуска из планировщика
|
||||||
def run_reservation_check():
|
def run_reservation_check():
|
||||||
logger.info("Планировщик вызывает run_reservation_check.")
|
"""Запуск проверки через планировщик."""
|
||||||
|
logger.info("📅 Планировщик вызывает run_reservation_check.")
|
||||||
try:
|
try:
|
||||||
checker = ReservationChecker()
|
checker = ReservationChecker()
|
||||||
checker.run_check()
|
checker.run_check()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при запуске проверки: {e}")
|
logger.error(f"❌ Ошибка при запуске проверки: {e}")
|
||||||
logger.info("run_reservation_check завершена.")
|
logger.info("✅ run_reservation_check завершена.")
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ def scheduled_sync():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error syncing connection {db_settings}: {e}")
|
logger.error(f"Error syncing connection {db_settings}: {e}")
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||||
futures = [executor.submit(sync_task, db_settings) for db_settings in active_db_settings]
|
futures = [executor.submit(sync_task, db_settings) for db_settings in active_db_settings]
|
||||||
for future in futures:
|
for future in futures:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-02-01 09:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('antifroud', '0005_roomdiscrepancy_fraud_checked_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='roomdiscrepancy',
|
||||||
|
name='fraud_checked',
|
||||||
|
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='useractivitylog',
|
||||||
|
name='fraud_checked',
|
||||||
|
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-02-01 09:48
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('antifroud', '0006_alter_roomdiscrepancy_fraud_checked_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='roomdiscrepancy',
|
||||||
|
name='fraud_checked',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -31,7 +31,7 @@ class UserActivityLog(models.Model):
|
|||||||
honeypot = models.BooleanField(verbose_name="Метка honeypot", blank=True, null=True)
|
honeypot = models.BooleanField(verbose_name="Метка honeypot", blank=True, null=True)
|
||||||
reply = models.BooleanField(verbose_name="Ответ пользователя", blank=True, null=True)
|
reply = models.BooleanField(verbose_name="Ответ пользователя", blank=True, null=True)
|
||||||
page_url = models.URLField(blank=True, null=True, verbose_name="URL страницы")
|
page_url = models.URLField(blank=True, null=True, verbose_name="URL страницы")
|
||||||
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия")
|
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def formatted_timestamp(self):
|
def formatted_timestamp(self):
|
||||||
@@ -79,10 +79,10 @@ class UserActivityLog(models.Model):
|
|||||||
except AddressNotFoundError:
|
except AddressNotFoundError:
|
||||||
return "IP-адрес не найден в базе"
|
return "IP-адрес не найден в базе"
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}")
|
# logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}")
|
||||||
return "Файл базы данных GeoIP не найден"
|
return "Файл базы данных GeoIP не найден"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при определении местоположения: {e}")
|
# logger.error(f"Ошибка при определении местоположения: {e}")
|
||||||
return "Местоположение недоступно"
|
return "Местоположение недоступно"
|
||||||
class ExternalDBSettings(models.Model):
|
class ExternalDBSettings(models.Model):
|
||||||
name = models.CharField(max_length=255, unique=True, help_text="Имя подключения для идентификации.")
|
name = models.CharField(max_length=255, unique=True, help_text="Имя подключения для идентификации.")
|
||||||
@@ -117,7 +117,6 @@ class RoomDiscrepancy(models.Model):
|
|||||||
verbose_name="Тип несоответствия"
|
verbose_name="Тип несоответствия"
|
||||||
)
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||||
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия")
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.hotel.name} - Room {self.room_number}: {self.discrepancy_type}"
|
return f"{self.hotel.name} - Room {self.room_number}: {self.discrepancy_type}"
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ class HotelAdmin(admin.ModelAdmin):
|
|||||||
class UserHotelAdmin(admin.ModelAdmin):
|
class UserHotelAdmin(admin.ModelAdmin):
|
||||||
list_display = ('user', 'hotel')
|
list_display = ('user', 'hotel')
|
||||||
search_fields = ('user__username', 'hotel__name')
|
search_fields = ('user__username', 'hotel__name')
|
||||||
# list_filter = ('hotel',)
|
list_filter = ('hotel',)
|
||||||
# ordering = ('-hotel',)
|
ordering = ('-hotel',)
|
||||||
|
|
||||||
@admin.register(Reservation)
|
@admin.register(Reservation)
|
||||||
class ReservationAdmin(admin.ModelAdmin):
|
class ReservationAdmin(admin.ModelAdmin):
|
||||||
list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
|
list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'fraud_checked')
|
||||||
search_fields = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
|
search_fields = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status')
|
||||||
list_filter = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
|
list_filter = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status')
|
||||||
ordering = ('-check_in',)
|
ordering = ('-check_in',)
|
||||||
|
|
||||||
@admin.register(Room)
|
@admin.register(Room)
|
||||||
|
|||||||
18
hotels/migrations/0004_reservation_fraud_checked.py
Normal file
18
hotels/migrations/0004_reservation_fraud_checked.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-02-01 09:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('hotels', '0003_rename_external_id_hotel_external_id_pms_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='reservation',
|
||||||
|
name='fraud_checked',
|
||||||
|
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-02-02 00:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('hotels', '0004_reservation_fraud_checked'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='reservation',
|
||||||
|
name='check_in',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата заезда'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='reservation',
|
||||||
|
name='check_out',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата выезда'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -111,11 +111,12 @@ class Reservation(models.Model):
|
|||||||
reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
|
reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
|
||||||
room_number = models.CharField(max_length=255, null=True, blank=True, verbose_name="Номер комнаты")
|
room_number = models.CharField(max_length=255, null=True, blank=True, verbose_name="Номер комнаты")
|
||||||
room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
|
room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
|
||||||
check_in = models.DateTimeField(verbose_name="Дата заезда")
|
check_in = models.DateTimeField(verbose_name="Дата заезда", null=True, blank=True)
|
||||||
check_out = models.DateTimeField(verbose_name="Дата выезда")
|
check_out = models.DateTimeField(verbose_name="Дата выезда", null=True, blank=True)
|
||||||
status = models.CharField(max_length=50, verbose_name="Статус")
|
status = models.CharField(max_length=50, verbose_name="Статус")
|
||||||
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Цена")
|
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Цена")
|
||||||
discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Скидка")
|
discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Скидка")
|
||||||
|
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.check_out and self.check_in and self.check_out <= self.check_in:
|
if self.check_out and self.check_in and self.check_out <= self.check_in:
|
||||||
|
|||||||
@@ -1,4 +1,144 @@
|
|||||||
|
# # ecvi_pms.py
|
||||||
|
# import logging
|
||||||
|
# import requests
|
||||||
|
# import json
|
||||||
|
# import os
|
||||||
|
# from datetime import datetime, timedelta
|
||||||
|
# from asgiref.sync import sync_to_async
|
||||||
|
# from hotels.models import Hotel, Reservation
|
||||||
|
# from .base_plugin import BasePMSPlugin
|
||||||
|
|
||||||
|
# class EcviPMSPlugin(BasePMSPlugin):
|
||||||
|
# """
|
||||||
|
# Плагин для интеграции с PMS Ecvi.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# def __init__(self, hotel):
|
||||||
|
# super().__init__(hotel.pms)
|
||||||
|
# self.hotel = hotel
|
||||||
|
|
||||||
|
# if not self.hotel.pms:
|
||||||
|
# raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
|
||||||
|
|
||||||
|
# self.api_url = self.hotel.pms.url.rstrip("/")
|
||||||
|
# self.token = self.hotel.pms.token
|
||||||
|
# self.username = self.hotel.pms.username
|
||||||
|
# self.password = self.hotel.pms.password
|
||||||
|
|
||||||
|
# self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
# handler_console = logging.StreamHandler()
|
||||||
|
# handler_file = logging.FileHandler('var/log/ecvi_pms_plugin.log')
|
||||||
|
# formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
# handler_console.setFormatter(formatter)
|
||||||
|
# handler_file.setFormatter(formatter)
|
||||||
|
# self.logger.addHandler(handler_console)
|
||||||
|
# self.logger.addHandler(handler_file)
|
||||||
|
# self.logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# def get_default_parser_settings(self):
|
||||||
|
# return {
|
||||||
|
# "field_mapping": {
|
||||||
|
# "check_in": "checkin",
|
||||||
|
# "check_out": "checkout",
|
||||||
|
# "room_number": "room_name",
|
||||||
|
# "room_type_name": "room_type",
|
||||||
|
# "status": "occupancy",
|
||||||
|
# },
|
||||||
|
# "date_format": "%Y-%m-%d %H:%M:%S"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# async def _fetch_data(self):
|
||||||
|
# headers = {"Content-Type": "application/json"}
|
||||||
|
# data = {"token": self.token}
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# response = await sync_to_async(requests.post)(
|
||||||
|
# self.api_url, headers=headers, json=data, auth=(self.username, self.password)
|
||||||
|
# )
|
||||||
|
# response.raise_for_status()
|
||||||
|
# response_data = response.json()
|
||||||
|
# self.logger.debug(f"Полученные данные с API: {response_data}")
|
||||||
|
|
||||||
|
# # Группировка данных по номеру комнаты
|
||||||
|
# structured_data = {}
|
||||||
|
# for item in response_data:
|
||||||
|
# room_number = item.get("room_name", "unknown")
|
||||||
|
# if room_number not in structured_data:
|
||||||
|
# structured_data[room_number] = []
|
||||||
|
# structured_data[room_number].append(item)
|
||||||
|
|
||||||
|
# # Сохранение данных во временный JSON-файл
|
||||||
|
# temp_dir = os.path.join("temp", "ecvi")
|
||||||
|
# os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
# temp_file = os.path.join(temp_dir, f"ecvi_data_{datetime.now().strftime('%Y%m%d%H%M%S')}.json")
|
||||||
|
# with open(temp_file, 'w') as file:
|
||||||
|
# json.dump(structured_data, file, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
# return await self._process_data(response_data)
|
||||||
|
# except requests.exceptions.RequestException as e:
|
||||||
|
# self.logger.error(f"Ошибка API: {e}")
|
||||||
|
# return {
|
||||||
|
# "processed_intervals": 0,
|
||||||
|
# "processed_items": 0,
|
||||||
|
# "errors": [str(e)]
|
||||||
|
# }
|
||||||
|
|
||||||
|
# async def _process_data(self, data):
|
||||||
|
# processed_items = 0
|
||||||
|
# errors = []
|
||||||
|
# date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
|
||||||
|
|
||||||
|
# for item in data:
|
||||||
|
# try:
|
||||||
|
# checkin = item['checkin']
|
||||||
|
# checkout = item['checkout']
|
||||||
|
# if checkin in [None, "0000-00-00 00:00:00"] or checkout in [None, "0000-00-00 00:00:00"]:
|
||||||
|
# continue
|
||||||
|
# checkin = self._parse_date(checkin, date_formats)
|
||||||
|
# checkout = self._parse_date(checkout, date_formats)
|
||||||
|
|
||||||
|
# reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||||
|
# reservation_id=item['task_id'],
|
||||||
|
# defaults={
|
||||||
|
# 'room_number': item['room_name'],
|
||||||
|
# 'room_type': item['room_type'],
|
||||||
|
# 'check_in': checkin,
|
||||||
|
# 'check_out': checkout,
|
||||||
|
# 'status': item['occupancy'],
|
||||||
|
# 'hotel': self.hotel,
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
# processed_items += 1
|
||||||
|
# except Exception as e:
|
||||||
|
# self.logger.error(f"Ошибка обработки записи: {e}")
|
||||||
|
# errors.append(str(e))
|
||||||
|
|
||||||
|
# return {
|
||||||
|
# "processed_intervals": 1,
|
||||||
|
# "processed_items": processed_items,
|
||||||
|
# "errors": errors
|
||||||
|
# }
|
||||||
|
|
||||||
|
# @staticmethod
|
||||||
|
# def _parse_date(date_str, formats):
|
||||||
|
# for fmt in formats:
|
||||||
|
# try:
|
||||||
|
# return datetime.strptime(date_str, fmt)
|
||||||
|
# except ValueError:
|
||||||
|
# continue
|
||||||
|
# raise ValueError(f"Дата '{date_str}' не соответствует ожидаемым форматам: {formats}")
|
||||||
|
|
||||||
|
# def validate_plugin(self):
|
||||||
|
# required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"]
|
||||||
|
# for method in required_methods:
|
||||||
|
# if not hasattr(self, method):
|
||||||
|
# raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
|
||||||
|
# self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
|
||||||
|
# return True
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import requests
|
import requests
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
@@ -11,20 +151,17 @@ class EcviPMSPlugin(BasePMSPlugin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hotel):
|
def __init__(self, hotel):
|
||||||
super().__init__(hotel.pms) # Передаем PMS-конфигурацию в базовый класс
|
super().__init__(hotel.pms)
|
||||||
self.hotel = hotel # Сохраняем объект отеля
|
self.hotel = hotel
|
||||||
|
|
||||||
# Проверка PMS-конфигурации
|
|
||||||
if not self.hotel.pms:
|
if not self.hotel.pms:
|
||||||
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
|
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
|
||||||
|
|
||||||
# Инициализация параметров API
|
|
||||||
self.api_url = self.hotel.pms.url
|
self.api_url = self.hotel.pms.url
|
||||||
self.token = self.hotel.pms.token
|
self.token = self.hotel.pms.token
|
||||||
self.username = self.hotel.pms.username
|
self.username = self.hotel.pms.username
|
||||||
self.password = self.hotel.pms.password
|
self.password = self.hotel.pms.password
|
||||||
|
|
||||||
# Настройка логгера
|
|
||||||
self.logger = logging.getLogger(self.__class__.__name__)
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
handler_console = logging.StreamHandler()
|
handler_console = logging.StreamHandler()
|
||||||
handler_file = logging.FileHandler('var/log/ecvi_pms_plugin.log')
|
handler_file = logging.FileHandler('var/log/ecvi_pms_plugin.log')
|
||||||
@@ -35,6 +172,10 @@ class EcviPMSPlugin(BasePMSPlugin):
|
|||||||
self.logger.addHandler(handler_file)
|
self.logger.addHandler(handler_file)
|
||||||
self.logger.setLevel(logging.WARNING)
|
self.logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Директория для сохранения JSON-файлов
|
||||||
|
self.data_dir = "var/data/ecvi"
|
||||||
|
os.makedirs(self.data_dir, exist_ok=True)
|
||||||
|
|
||||||
def get_default_parser_settings(self):
|
def get_default_parser_settings(self):
|
||||||
"""
|
"""
|
||||||
Возвращает настройки парсера по умолчанию.
|
Возвращает настройки парсера по умолчанию.
|
||||||
@@ -47,7 +188,7 @@ class EcviPMSPlugin(BasePMSPlugin):
|
|||||||
"room_type_name": "room_type",
|
"room_type_name": "room_type",
|
||||||
"status": "occupancy",
|
"status": "occupancy",
|
||||||
},
|
},
|
||||||
"date_format": "%Y-%m-%d %H:%M:%S" # Формат изменен на соответствующий данным
|
"date_format": "%Y-%m-%d %H:%M:%S"
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _fetch_data(self):
|
async def _fetch_data(self):
|
||||||
@@ -58,14 +199,23 @@ class EcviPMSPlugin(BasePMSPlugin):
|
|||||||
data = {"token": self.token}
|
data = {"token": self.token}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Запрос данных из PMS API
|
|
||||||
response = await sync_to_async(requests.post)(
|
response = await sync_to_async(requests.post)(
|
||||||
self.api_url, headers=headers, json=data, auth=(self.username, self.password)
|
self.api_url, headers=headers, json=data, auth=(self.username, self.password)
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
self.logger.debug(f"Полученные данные с API: {response_data}")
|
self.logger.debug(f"Полученные данные с API: {response_data}")
|
||||||
|
|
||||||
|
# Сохраняем весь ответ API в файл для анализа
|
||||||
|
file_name = f"ecvi_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||||
|
file_path = os.path.join(self.data_dir, file_name)
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(response_data, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
self.logger.info(f"API-ответ сохранен в файл: {file_path}")
|
||||||
|
|
||||||
return await self._process_data(response_data)
|
return await self._process_data(response_data)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
self.logger.error(f"Ошибка API: {e}")
|
self.logger.error(f"Ошибка API: {e}")
|
||||||
return {
|
return {
|
||||||
@@ -80,22 +230,62 @@ class EcviPMSPlugin(BasePMSPlugin):
|
|||||||
"""
|
"""
|
||||||
processed_items = 0
|
processed_items = 0
|
||||||
errors = []
|
errors = []
|
||||||
|
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
|
||||||
|
unix_epoch = datetime(1970, 1, 1, 0, 0, 0)
|
||||||
|
|
||||||
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] # Поддержка нескольких форматов даты
|
valid_reservations = []
|
||||||
|
print(data)
|
||||||
for item in data:
|
for item in data:
|
||||||
try:
|
try:
|
||||||
# Парсинг даты с поддержкой нескольких форматов
|
checkin = item.get('checkin')
|
||||||
checkin = self._parse_date(item['checkin'], date_formats)
|
checkout = item.get('checkout')
|
||||||
checkout = self._parse_date(item['checkout'], date_formats)
|
|
||||||
|
|
||||||
|
# Фильтруем записи с некорректными датами
|
||||||
|
if checkin in [None, "0000-00-00 00:00:00", "1970-01-01 00:00:00", 0] or \
|
||||||
|
checkout in [None, "0000-00-00 00:00:00", "1970-01-01 00:00:00", 0]:
|
||||||
|
self.logger.warning(f"Игнорируется запись с некорректной датой: {item}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
checkin = self._parse_date(checkin, date_formats)
|
||||||
|
checkout = self._parse_date(checkout, date_formats)
|
||||||
|
|
||||||
|
# Проверяем на Unix epoch
|
||||||
|
if checkin == unix_epoch or checkout == unix_epoch:
|
||||||
|
self.logger.warning(f"Игнорируется запись с Unix epoch датой: {item}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверяем timestamp
|
||||||
|
if checkin.timestamp() == 0 or checkout.timestamp() == 0:
|
||||||
|
self.logger.warning(f"Игнорируется запись с timestamp = 0: {item}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid_reservations.append(item)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Ошибка обработки записи: {e}")
|
||||||
|
errors.append(str(e))
|
||||||
|
|
||||||
|
# Логируем количество отфильтрованных записей
|
||||||
|
self.logger.info(f"Обработано бронирований: {len(valid_reservations)}")
|
||||||
|
|
||||||
|
# Сохранение валидных бронирований в JSON для проверки
|
||||||
|
valid_file_name = f"valid_reservations_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||||
|
valid_file_path = os.path.join(self.data_dir, valid_file_name)
|
||||||
|
with open(valid_file_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(valid_reservations, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
self.logger.info(f"Валидные бронирования сохранены в файл: {valid_file_path}")
|
||||||
|
|
||||||
|
# Сохранение данных в БД
|
||||||
|
for item in valid_reservations:
|
||||||
|
try:
|
||||||
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||||
reservation_id=item['task_id'],
|
reservation_id=item['task_id'],
|
||||||
defaults={
|
defaults={
|
||||||
'room_number': item['room_name'],
|
'room_number': item['room_name'],
|
||||||
'room_type': item['room_type'],
|
'room_type': item['room_type'],
|
||||||
'check_in': checkin,
|
'check_in': self._parse_date(item['checkin'], date_formats),
|
||||||
'check_out': checkout,
|
'check_out': self._parse_date(item['checkout'], date_formats),
|
||||||
'status': item['occupancy'],
|
'status': item['occupancy'],
|
||||||
'hotel': self.hotel,
|
'hotel': self.hotel,
|
||||||
}
|
}
|
||||||
@@ -109,7 +299,7 @@ class EcviPMSPlugin(BasePMSPlugin):
|
|||||||
processed_items += 1
|
processed_items += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Ошибка обработки записи: {e}")
|
self.logger.error(f"Ошибка сохранения бронирования: {e}")
|
||||||
errors.append(str(e))
|
errors.append(str(e))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,313 +1,111 @@
|
|||||||
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import hashlib
|
|
||||||
import json
|
import json
|
||||||
from .base_plugin import BasePMSPlugin
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
from touchh.utils.log import CustomLogger
|
|
||||||
from hotels.models import Hotel, Reservation
|
from hotels.models import Hotel, Reservation
|
||||||
from app_settings.models import GlobalHotelSettings
|
from .base_plugin import BasePMSPlugin
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
class RealtyCalendarPlugin(BasePMSPlugin):
|
class RealtyCalendarPMSPlugin(BasePMSPlugin):
|
||||||
def __init__(self, config):
|
def __init__(self, hotel):
|
||||||
super().__init__(config)
|
super().__init__(hotel.pms)
|
||||||
self.public_key = config.public_key
|
self.hotel = hotel
|
||||||
self.private_key = config.private_key
|
|
||||||
self.api_url = config.url.rstrip("/")
|
if not self.hotel.pms:
|
||||||
self.logger = CustomLogger(name="RealtyCalendarPlugin", log_level="DEBUG").get_logger()
|
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
|
||||||
if not self.public_key or not self.private_key:
|
|
||||||
raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar")
|
self.api_url = self.hotel.pms.url.rstrip("/")
|
||||||
|
self.public_key = self.hotel.pms.public_key
|
||||||
|
self.private_key = self.hotel.pms.private_key
|
||||||
|
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
handler_console = logging.StreamHandler()
|
||||||
|
handler_file = logging.FileHandler('var/log/realtycalendar_pms_plugin.log')
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
handler_console.setFormatter(formatter)
|
||||||
|
handler_file.setFormatter(formatter)
|
||||||
|
self.logger.addHandler(handler_console)
|
||||||
|
self.logger.addHandler(handler_file)
|
||||||
|
self.logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
def get_default_parser_settings(self):
|
def get_default_parser_settings(self):
|
||||||
"""
|
|
||||||
Возвращает настройки по умолчанию для обработки данных.
|
|
||||||
"""
|
|
||||||
return {
|
return {
|
||||||
"date_format": "%Y-%m-%dT%H:%M:%S",
|
"field_mapping": {
|
||||||
"timezone": "UTC"
|
"check_in": "begin_date",
|
||||||
|
"check_out": "end_date",
|
||||||
|
"room_number": "apartment_id",
|
||||||
|
"room_type_name": "notes",
|
||||||
|
"status": "status",
|
||||||
|
},
|
||||||
|
"date_format": "%Y-%m-%d %H:%M:%S"
|
||||||
}
|
}
|
||||||
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_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):
|
|
||||||
# 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"),
|
|
||||||
# }
|
|
||||||
# data["sign"] = self._generate_sign(data)
|
|
||||||
|
|
||||||
# try:
|
|
||||||
# response = requests.post(url=base_url, headers=headers, json=data)
|
|
||||||
# self.logger.debug(f"Статус ответа: {response.status_code}")
|
|
||||||
|
|
||||||
# if response.status_code != 200:
|
|
||||||
# self.logger.error(f"Ошибка API: {response.status_code}, {response.text}")
|
|
||||||
# raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}")
|
|
||||||
|
|
||||||
# try:
|
|
||||||
# response_data = response.json()
|
|
||||||
# bookings = response_data.get("bookings", [])
|
|
||||||
# # self.logger.debug(f"Полученные данные: {bookings}")
|
|
||||||
|
|
||||||
# if not isinstance(bookings, list):
|
|
||||||
# self.logger.error(f"Ожидался список, но получен: {type(bookings)}")
|
|
||||||
# raise ValueError("Некорректный формат данных для bookings")
|
|
||||||
# except json.JSONDecodeError as e:
|
|
||||||
# self.logger.error(f"Ошибка декодирования JSON: {e}")
|
|
||||||
# raise ValueError("Ответ не является корректным JSON.")
|
|
||||||
# except Exception as e:
|
|
||||||
# self.logger.error(f"Ошибка обработки ответа API: {e}")
|
|
||||||
# raise
|
|
||||||
|
|
||||||
# except Exception as e:
|
|
||||||
# self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}")
|
|
||||||
# raise
|
|
||||||
|
|
||||||
# try:
|
|
||||||
# hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
|
|
||||||
# hotel_tz = hotel.timezone
|
|
||||||
# self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}")
|
|
||||||
|
|
||||||
# hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)()
|
|
||||||
# check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00"
|
|
||||||
# check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00"
|
|
||||||
# except Exception as e:
|
|
||||||
# self.logger.error(f"Ошибка получения настроек отеля: {e}")
|
|
||||||
# check_in_time, check_out_time = "14:00:00", "12:00:00"
|
|
||||||
|
|
||||||
# filtered_data = []
|
|
||||||
# for item in bookings:
|
|
||||||
# try:
|
|
||||||
# if not isinstance(item, dict):
|
|
||||||
# self.logger.error(f"Некорректный формат элемента: {item}")
|
|
||||||
# continue
|
|
||||||
|
|
||||||
# reservation_id = item.get('id')
|
|
||||||
# if not reservation_id:
|
|
||||||
# self.logger.error(f"ID резервации отсутствует: {item}")
|
|
||||||
# continue
|
|
||||||
|
|
||||||
# begin_date = item.get('begin_date')
|
|
||||||
# end_date = item.get('end_date')
|
|
||||||
# if not begin_date or not end_date:
|
|
||||||
# self.logger.error(f"Отсутствуют даты в записи: {item}")
|
|
||||||
# continue
|
|
||||||
|
|
||||||
# checkin = timezone.make_aware(
|
|
||||||
# datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S")
|
|
||||||
# )
|
|
||||||
# checkout = timezone.make_aware(
|
|
||||||
# datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S")
|
|
||||||
# )
|
|
||||||
|
|
||||||
# filtered_data.append({
|
|
||||||
# 'reservation_id': reservation_id,
|
|
||||||
# 'checkin': checkin,
|
|
||||||
# 'checkout': checkout,
|
|
||||||
# 'room_number': item.get('apartment_id'),
|
|
||||||
# 'room_type': item.get('notes', 'Описание отсутствует'),
|
|
||||||
# 'status': item.get('status')
|
|
||||||
# })
|
|
||||||
# except Exception as e:
|
|
||||||
# self.logger.error(f"Ошибка обработки элемента: {e}")
|
|
||||||
|
|
||||||
# # self.logger.debug(f"Отфильтрованные данные: {filtered_data}")
|
|
||||||
# await self._save_to_db(filtered_data)
|
|
||||||
|
|
||||||
async def _fetch_data(self):
|
async def _fetch_data(self):
|
||||||
self.logger.debug("Начало выполнения функции _fetch_data")
|
headers = {"Accept": "application/json", "Content-Type": "application/json"}
|
||||||
base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/"
|
|
||||||
headers = {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
data = {
|
data = {
|
||||||
"begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"),
|
"begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"),
|
||||||
"end_date": now.strftime("%Y-%m-%d"),
|
"end_date": now.strftime("%Y-%m-%d"),
|
||||||
|
"sign": self._generate_sign()
|
||||||
}
|
}
|
||||||
data["sign"] = self._generate_sign(data)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url=base_url, headers=headers, json=data)
|
response = await sync_to_async(requests.post)(
|
||||||
self.logger.debug(f"Статус ответа: {response.status_code}")
|
f"{self.api_url}/api/v1/bookings/{self.public_key}/",
|
||||||
|
headers=headers, json=data
|
||||||
if response.status_code != 200:
|
)
|
||||||
self.logger.error(f"Ошибка API: {response.status_code}, {response.text}")
|
response.raise_for_status()
|
||||||
return {
|
|
||||||
"processed_intervals": 0,
|
|
||||||
"processed_items": 0,
|
|
||||||
"errors": [f"Ошибка API RealtyCalendar: {response.status_code}"]
|
|
||||||
}
|
|
||||||
|
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
bookings = response_data.get("bookings", [])
|
return await self._process_data(response_data)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.logger.error(f"Ошибка API: {e}")
|
||||||
|
return {"processed_intervals": 0, "processed_items": 0, "errors": [str(e)]}
|
||||||
|
|
||||||
if not isinstance(bookings, list):
|
async def _process_data(self, data):
|
||||||
self.logger.error(f"Ожидался список, но получен: {type(bookings)}")
|
|
||||||
return {
|
|
||||||
"processed_intervals": 0,
|
|
||||||
"processed_items": 0,
|
|
||||||
"errors": ["Некорректный формат данных для bookings"]
|
|
||||||
}
|
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
self.logger.error(f"Ошибка декодирования JSON: {e}")
|
|
||||||
return {
|
|
||||||
"processed_intervals": 0,
|
|
||||||
"processed_items": 0,
|
|
||||||
"errors": ["Ответ не является корректным JSON."]
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}")
|
|
||||||
return {
|
|
||||||
"processed_intervals": 0,
|
|
||||||
"processed_items": 0,
|
|
||||||
"errors": [str(e)]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Получение настроек отеля
|
|
||||||
try:
|
|
||||||
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
|
|
||||||
hotel_tz = hotel.timezone
|
|
||||||
self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}")
|
|
||||||
|
|
||||||
hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)()
|
|
||||||
check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00"
|
|
||||||
check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00"
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Ошибка получения настроек отеля: {e}")
|
|
||||||
check_in_time, check_out_time = "14:00:00", "12:00:00"
|
|
||||||
|
|
||||||
# Обработка записей
|
|
||||||
processed_items = 0
|
processed_items = 0
|
||||||
errors = []
|
errors = []
|
||||||
filtered_data = []
|
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
|
||||||
|
|
||||||
for item in bookings:
|
for item in data.get("bookings", []):
|
||||||
try:
|
try:
|
||||||
if not isinstance(item, dict):
|
checkin = self._parse_date(item['begin_date'], date_formats)
|
||||||
raise ValueError(f"Некорректный формат элемента: {item}")
|
checkout = self._parse_date(item['end_date'], date_formats)
|
||||||
|
|
||||||
reservation_id = item.get('id')
|
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||||
if not reservation_id:
|
reservation_id=item['id'],
|
||||||
raise ValueError(f"ID резервации отсутствует: {item}")
|
defaults={
|
||||||
|
'room_number': item['apartment_id'],
|
||||||
begin_date = item.get('begin_date')
|
'room_type': item.get('notes', 'Описание отсутствует'),
|
||||||
end_date = item.get('end_date')
|
'check_in': checkin,
|
||||||
if not begin_date or not end_date:
|
'check_out': checkout,
|
||||||
raise ValueError(f"Отсутствуют даты в записи: {item}")
|
'status': item['status'],
|
||||||
|
'hotel': self.hotel,
|
||||||
checkin = timezone.make_aware(
|
}
|
||||||
datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S")
|
|
||||||
)
|
)
|
||||||
checkout = timezone.make_aware(
|
|
||||||
datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S")
|
|
||||||
)
|
|
||||||
|
|
||||||
filtered_data.append({
|
|
||||||
'reservation_id': reservation_id,
|
|
||||||
'checkin': checkin,
|
|
||||||
'checkout': checkout,
|
|
||||||
'room_number': item.get('apartment_id'),
|
|
||||||
'room_type': item.get('notes', 'Описание отсутствует'),
|
|
||||||
'status': item.get('status')
|
|
||||||
})
|
|
||||||
processed_items += 1
|
processed_items += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Ошибка обработки элемента: {e}")
|
self.logger.error(f"Ошибка обработки записи: {e}")
|
||||||
errors.append(str(e))
|
errors.append(str(e))
|
||||||
|
|
||||||
# Сохранение в БД
|
return {"processed_intervals": 1, "processed_items": processed_items, "errors": errors}
|
||||||
try:
|
|
||||||
await self._save_to_db(filtered_data)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Ошибка сохранения данных в БД: {e}")
|
|
||||||
errors.append(f"Ошибка сохранения данных в БД: {str(e)}")
|
|
||||||
|
|
||||||
# Формирование отчета
|
def _generate_sign(self):
|
||||||
report = {
|
return hashlib.md5((self.public_key + self.private_key).encode("utf-8")).hexdigest()
|
||||||
"processed_intervals": 1, # Пример значения
|
|
||||||
"processed_items": processed_items,
|
|
||||||
"errors": errors
|
|
||||||
}
|
|
||||||
self.logger.debug(f"Сформированный отчет: {report}")
|
|
||||||
return report
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
async def _save_to_db(self, data):
|
def _parse_date(date_str, formats):
|
||||||
if not isinstance(data, list):
|
for fmt in formats:
|
||||||
self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}")
|
|
||||||
return
|
|
||||||
|
|
||||||
for index, item in enumerate(data, start=1):
|
|
||||||
try:
|
try:
|
||||||
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
|
return datetime.strptime(date_str, fmt)
|
||||||
reservation_id = item.get('reservation_id')
|
except ValueError:
|
||||||
if not reservation_id:
|
continue
|
||||||
self.logger.error(f"Пропущена запись {index}: отсутствует 'id'")
|
raise ValueError(f"Дата '{date_str}' не соответствует ожидаемым форматам: {formats}")
|
||||||
continue
|
|
||||||
|
|
||||||
existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id)
|
|
||||||
existing_reservation = await sync_to_async(existing_reservation.first)()
|
|
||||||
|
|
||||||
defaults = {
|
|
||||||
'room_number': item['room_number'],
|
|
||||||
'room_type': item['room_type'],
|
|
||||||
'check_in': item['checkin'],
|
|
||||||
'check_out': item['checkout'],
|
|
||||||
'status': item['status'],
|
|
||||||
'hotel': hotel
|
|
||||||
}
|
|
||||||
|
|
||||||
if existing_reservation:
|
|
||||||
await sync_to_async(Reservation.objects.update_or_create)(
|
|
||||||
reservation_id=reservation_id, defaults=defaults
|
|
||||||
)
|
|
||||||
self.logger.debug(f"Резервация {reservation_id} обновлена. ")
|
|
||||||
else:
|
|
||||||
await sync_to_async(Reservation.objects.create)(
|
|
||||||
reservation_id=reservation_id, **defaults
|
|
||||||
)
|
|
||||||
self.logger.debug(f"Создана новая резервация {reservation_id}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Ошибка при обработке записи {index}: {e}")
|
|
||||||
|
|
||||||
def validate_plugin(self):
|
def validate_plugin(self):
|
||||||
required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"]
|
required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"]
|
||||||
for m in required_methods:
|
for method in required_methods:
|
||||||
if not hasattr(self, m):
|
if not hasattr(self, method):
|
||||||
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.")
|
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
|
||||||
self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
|
self.logger.debug(f"Плагин {self.__class__.__name__} успешно прошел валидацию.")
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
def test_function():
|
|
||||||
"""тестовая функция для проверки планировщика
|
|
||||||
|
|
||||||
"""
|
|
||||||
print("Hello, World!")
|
|
||||||
return "Hello, World!"
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
def test_function():
|
||||||
|
"""тестовая функция для проверки планировщика
|
||||||
|
|
||||||
|
"""
|
||||||
|
print("Hello, World!")
|
||||||
|
return "Hello, World!"
|
||||||
|
|||||||
18
staticfiles/admin/custom.css
Normal file
18
staticfiles/admin/custom.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.ml-4 {
|
||||||
|
margin-left: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ml-6 {
|
||||||
|
margin-left: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar ul.nav-treeview .nav-link {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
.nav-sidebar .nav-link {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.nav-sidebar .nav-link .nav-icon {
|
||||||
|
margin-top: .2rem;
|
||||||
|
margin-right: .3rem;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
18
tmp/tests.py
Normal file
18
tmp/tests.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
import django
|
||||||
|
import unittest
|
||||||
|
from antifroud.check_fraud import run_reservation_check
|
||||||
|
|
||||||
|
# Установка переменной окружения для Django settings
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings")
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
class FraudCheckTests(unittest.TestCase):
|
||||||
|
def test_run_reservation_check(self):
|
||||||
|
"""Тест проверки бронирования на мошенничество"""
|
||||||
|
result = run_reservation_check()
|
||||||
|
self.assertIsNotNone(result) # Проверяем, что результат не None
|
||||||
|
self.assertIsInstance(result, dict) # Проверяем, что возвращается словарь
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user