Compare commits

...

3 Commits

@ -0,0 +1,698 @@
import os
import json
import re
import shutil
import subprocess
import zipfile
from collections import defaultdict
from datetime import datetime
from pathlib import Path
# ---------- ЦВЕТА ДЛЯ КОНСОЛИ (НЕЙТРАЛЬНАЯ ПАЛИТРА) ----------
COLOR_RESET = "\033[0m"
COLOR_WHITE = "\033[97m" # белый (для заголовков)
COLOR_LIGHT_GRAY = "\033[37m" # светло-серый (для меню)
COLOR_GRAY = "\033[90m" # тёмно-серый (для информации)
COLOR_YELLOW = "\033[93m" # жёлтый (для ввода/предупреждений)
COLOR_GREEN = "\033[32m" # обычный зелёный (успех)
COLOR_RED = "\033[31m" # красный (ошибки)
COLOR_CYAN = "\033[36m" # голубой (акценты, откат)
COLOR_BOLD = "\033[1m"
def color_text(text, color=COLOR_RESET):
return f"{color}{text}{COLOR_RESET}"
# ---------- Функции сбора проекта ----------
def collect_project_files(root_dir, extensions=('.py', '.html'),
exclude_dirs=('venv', '__pycache__', '.git', 'env', 'node_modules')):
files_data = []
root_dir = os.path.abspath(root_dir)
for dirpath, dirnames, filenames in os.walk(root_dir):
dirnames[:] = [d for d in dirnames if d not in exclude_dirs]
for filename in filenames:
if filename.endswith(extensions):
file_path = os.path.join(dirpath, filename)
rel_path = os.path.relpath(file_path, root_dir)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except UnicodeDecodeError:
try:
with open(file_path, 'r', encoding='cp1251') as f:
content = f.read()
except Exception:
content = f"[Не удалось прочитать файл {rel_path} — возможно, бинарный]"
except Exception as e:
content = f"[Ошибка чтения: {e}]"
files_data.append({'path': rel_path, 'content': content})
return files_data
def build_tree(files_paths):
tree = defaultdict(dict)
for path in sorted(files_paths):
parts = path.split(os.sep)
current = tree
for part in parts:
if part not in current:
current[part] = {}
current = current[part]
current['__is_file__'] = True
def render(node, prefix=''):
lines = []
items = sorted([k for k in node.keys() if not k.startswith('__')])
for i, key in enumerate(items):
is_last = (i == len(items) - 1)
is_file = node[key].get('__is_file__', False)
connector = '└── ' if is_last else '├── '
lines.append(prefix + connector + key + (' (файл)' if is_file else '/'))
if not is_file:
extension = ' ' if is_last else '│ '
lines.extend(render(node[key], prefix + extension))
return lines
return '\n'.join(render(tree))
def build_prompt(main_prompt, files_data):
parts = []
parts.append("=" * 80)
parts.append("ОСНОВНОЙ ЗАПРОС (ПРОМТ)")
parts.append("=" * 80)
parts.append(main_prompt)
parts.append("")
parts.append("=" * 80)
parts.append("СТРУКТУРА ПРОЕКТА (ДЕРЕВО ПАПОК И ФАЙЛОВ)")
parts.append("=" * 80)
if files_data:
paths = [item['path'] for item in files_data]
tree_str = build_tree(paths)
parts.append(tree_str)
else:
parts.append("Файлы с расширениями .py или .html не найдены.")
parts.append("")
parts.append("=" * 80)
parts.append("СОДЕРЖИМОЕ ФАЙЛОВ (ПО ОТНОСИТЕЛЬНЫМ ПУТЯМ)")
parts.append("=" * 80)
if files_data:
for item in files_data:
parts.append(f"\n--- Файл: {item['path']} ---")
parts.append(item['content'])
parts.append("")
else:
parts.append("Нет файлов для отображения.")
parts.append("")
parts.append("=" * 80)
parts.append("ИНСТРУКЦИЯ ПО ФОРМАТУ ОТВЕТА (ОБЯЗАТЕЛЬНО К ВЫПОЛНЕНИЮ)")
parts.append("=" * 80)
parts.append(
"Ты — ассистент по разработке. На основе структуры проекта и запроса пользователя предложи изменения в коде.\n"
"Твой ответ должен состоять из двух частей:\n"
"1. Краткое описание того, какие изменения ты предлагаешь (на русском, 1–3 предложения).\n"
"2. Блок кода с JSON-структурой, который будет использован для автоматического применения изменений.\n"
" Блок должен быть оформлен как ```json ... ``` (это позволит скопировать его одной кнопкой).\n"
"\n"
"Структура JSON:\n"
"{\n"
" \"actions\": [\n"
" {\n"
" \"action\": \"create\" | \"update\" | \"delete\",\n"
" \"file_path\": \"относительный/путь/к/файлу\",\n"
" \"content\": \"полное новое содержимое файла (для create/update)\",\n"
" \"description\": \"краткое описание этого изменения\"\n"
" }\n"
" ]\n"
"}\n"
"\n"
"Важно:\n"
"- Все пути должны быть относительно корня проекта (как в дереве выше).\n"
"- Для update передавай ПОЛНОЕ новое содержимое файла (заменяй целиком).\n"
"- Для delete можно не указывать content (или оставить пустую строку).\n"
"- Если изменений не требуется, верни пустой массив actions.\n"
"- Весь JSON должен быть внутри блока ```json ... ```. Никакого другого кода вне блока.\n"
"- Текст описания должен быть перед блоком, а не после.\n"
"- Используй только двойные кавычки для строк в JSON.\n"
)
parts.append("")
return "\n".join(parts)
# ---------- Извлечение JSON ----------
def extract_json_from_text(text):
json_block_pattern = r"```json\s*(\{.*?\})\s*```"
match = re.search(json_block_pattern, text, re.DOTALL | re.IGNORECASE)
if match:
json_str = match.group(1)
try:
return json.loads(json_str)
except json.JSONDecodeError:
pass
start = text.find('{')
if start == -1:
return None
end = text.rfind('}')
if end == -1:
return None
json_candidate = text[start:end+1]
try:
return json.loads(json_candidate)
except json.JSONDecodeError:
return None
# ---------- Полное резервное копирование проекта ----------
def create_full_backup(project_root, ai_response_dir):
"""Создаёт ZIP-архив всего проекта (исключая папку _ai_response)."""
backup_full_dir = os.path.join(ai_response_dir, 'backup', 'full')
os.makedirs(backup_full_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
zip_filename = f"project_backup_{timestamp}.zip"
zip_path = os.path.join(backup_full_dir, zip_filename)
ignore_dir = os.path.basename(ai_response_dir)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(project_root):
if ignore_dir in root.split(os.sep):
continue
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, project_root)
zipf.write(file_path, arcname)
return zip_path
def list_full_backups(ai_response_dir):
backup_full_dir = os.path.join(ai_response_dir, 'backup', 'full')
if not os.path.exists(backup_full_dir):
return []
backups = []
for f in os.listdir(backup_full_dir):
if f.startswith('project_backup_') and f.endswith('.zip'):
full_path = os.path.join(backup_full_dir, f)
try:
time_str = f.replace('project_backup_', '').replace('.zip', '')
dt = datetime.strptime(time_str, "%Y%m%d_%H%M%S")
except:
dt = datetime.fromtimestamp(os.path.getmtime(full_path))
backups.append((full_path, dt, f))
backups.sort(key=lambda x: x[1], reverse=True)
return backups
def restore_full_backup(project_root, backup_path):
for item in os.listdir(project_root):
if item == os.path.basename(os.path.join(project_root, '_ai_response')):
continue
item_path = os.path.join(project_root, item)
if os.path.isfile(item_path):
os.remove(item_path)
else:
shutil.rmtree(item_path)
with zipfile.ZipFile(backup_path, 'r') as zipf:
zipf.extractall(project_root)
return True
# ---------- Бэкапирование отдельных файлов (для отката) ----------
def backup_file(abs_path, ai_response_dir):
backup_root = os.path.join(ai_response_dir, 'backup')
os.makedirs(backup_root, exist_ok=True)
project_root = os.getcwd()
rel_path = os.path.relpath(abs_path, project_root)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_rel = f"{rel_path}.{timestamp}.bak"
backup_abs = os.path.join(backup_root, backup_rel)
os.makedirs(os.path.dirname(backup_abs), exist_ok=True)
shutil.copy2(abs_path, backup_abs)
return backup_abs
# ---------- Применение изменений (с бэкапами и полным архивом) ----------
def apply_changes(project_root, actions, ai_response_dir, user_prompt=''):
reports = []
errors = False
project_root = os.path.abspath(project_root)
# Добавляем запрос пользователя в отчёт
if user_prompt:
reports.append(f"📝 Запрос пользователя: {user_prompt}")
# Создаём полный бэкап проекта
try:
backup_zip = create_full_backup(project_root, ai_response_dir)
reports.append(f"📦 Полный бэкап проекта сохранён: {backup_zip}")
except Exception as e:
reports.append(f"❌ Ошибка при создании полного бэкапа: {e}")
errors = True
# Сохраняем данные о применении (включая запрос)
last_apply_file = os.path.join(ai_response_dir, 'last_apply.json')
apply_data = {
'user_prompt': user_prompt,
'actions': actions,
'timestamp': datetime.now().isoformat()
}
with open(last_apply_file, 'w', encoding='utf-8') as f:
json.dump(apply_data, f, ensure_ascii=False, indent=2)
for action in actions:
action_type = action.get('action')
file_path = action.get('file_path')
content = action.get('content', '')
description = action.get('description', '')
if not action_type or not file_path:
reports.append("❌ Пропущено действие: отсутствует 'action' или 'file_path'")
errors = True
continue
norm_path = os.path.normpath(file_path.replace('/', os.sep))
abs_path = os.path.join(project_root, norm_path)
if not os.path.abspath(abs_path).startswith(project_root):
reports.append(f"❌ Опасный путь: {file_path} — выход за пределы проекта")
errors = True
continue
try:
if action_type == 'create':
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
with open(abs_path, 'w', encoding='utf-8') as f:
f.write(content)
reports.append(f"✅ Создан: {file_path} — {description}")
elif action_type == 'update':
if not os.path.exists(abs_path):
reports.append(f"⚠️ Файл {file_path} не существует, создаём новый (как create)")
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
else:
backup_file(abs_path, ai_response_dir)
with open(abs_path, 'w', encoding='utf-8') as f:
f.write(content)
reports.append(f"✅ Обновлён: {file_path} — {description}")
elif action_type == 'delete':
if os.path.exists(abs_path):
backup_file(abs_path, ai_response_dir)
os.remove(abs_path)
reports.append(f"✅ Удалён: {file_path} — {description}")
else:
reports.append(f"⚠️ Файл {file_path} не найден, пропускаем удаление")
else:
reports.append(f"❌ Неизвестное действие '{action_type}' для {file_path}")
errors = True
except Exception as e:
reports.append(f"❌ Ошибка при обработке {file_path}: {e}")
errors = True
return reports, errors
# ---------- Откат изменений (только последнего набора) ----------
def rollback_last_apply(project_root, ai_response_dir):
reports = []
errors = False
last_apply_file = os.path.join(ai_response_dir, 'last_apply.json')
if not os.path.exists(last_apply_file):
reports.append("❌ Нет сохранённых действий для отката (файл last_apply.json не найден).")
return reports, True
try:
with open(last_apply_file, 'r', encoding='utf-8') as f:
data = json.load(f)
actions = data.get('actions', [])
except Exception as e:
reports.append(f"❌ Ошибка чтения last_apply.json: {e}")
return reports, True
for action in reversed(actions):
action_type = action.get('action')
file_path = action.get('file_path')
description = action.get('description', '')
if not action_type or not file_path:
continue
norm_path = os.path.normpath(file_path.replace('/', os.sep))
abs_path = os.path.join(project_root, norm_path)
if not os.path.abspath(abs_path).startswith(project_root):
reports.append(f"❌ Опасный путь: {file_path} — выход за пределы проекта")
errors = True
continue
try:
if action_type == 'create':
if os.path.exists(abs_path):
os.remove(abs_path)
reports.append(f"🗑️ Удалён (откат create): {file_path} — {description}")
else:
reports.append(f"⚠️ Файл {file_path} не найден, пропускаем удаление (откат create)")
elif action_type == 'update':
backup_root = os.path.join(ai_response_dir, 'backup')
backup_files = []
for root, dirs, files in os.walk(backup_root):
for f in files:
if f.startswith(norm_path + '.') and f.endswith('.bak'):
full = os.path.join(root, f)
backup_files.append((full, os.path.getmtime(full)))
if backup_files:
backup_files.sort(key=lambda x: x[1], reverse=True)
latest_backup = backup_files[0][0]
shutil.copy2(latest_backup, abs_path)
reports.append(f"♻️ Восстановлен (откат update): {file_path} — из {os.path.basename(latest_backup)}")
else:
reports.append(f"❌ Не найден бэкап для {file_path}, пропускаем (откат update)")
errors = True
elif action_type == 'delete':
backup_root = os.path.join(ai_response_dir, 'backup')
backup_files = []
for root, dirs, files in os.walk(backup_root):
for f in files:
if f.startswith(norm_path + '.') and f.endswith('.bak'):
full = os.path.join(root, f)
backup_files.append((full, os.path.getmtime(full)))
if backup_files:
backup_files.sort(key=lambda x: x[1], reverse=True)
latest_backup = backup_files[0][0]
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
shutil.copy2(latest_backup, abs_path)
reports.append(f"♻️ Восстановлен (откат delete): {file_path} — из {os.path.basename(latest_backup)}")
else:
reports.append(f"❌ Не найден бэкап для {file_path}, пропускаем (откат delete)")
errors = True
else:
reports.append(f"❌ Неизвестное действие '{action_type}' для {file_path} (откат)")
errors = True
except Exception as e:
reports.append(f"❌ Ошибка при откате {file_path}: {e}")
errors = True
return reports, errors
# ---------- Вспомогательные функции ----------
def get_downloads_folder():
home = Path.home()
downloads = home / 'Downloads'
if downloads.exists():
return str(downloads)
alt = home / 'Загрузки'
if alt.exists():
return str(alt)
return os.getcwd()
def find_latest_json_file(directory, prefix='deepseek_json_'):
files = []
for f in os.listdir(directory):
if f.startswith(prefix) and f.endswith('.json'):
full_path = os.path.join(directory, f)
if os.path.isfile(full_path):
files.append((full_path, os.path.getmtime(full_path)))
if not files:
return None
files.sort(key=lambda x: x[1], reverse=True)
return files[0][0]
def run_tests(project_root, test_path=None):
reports = []
manage_path = os.path.join(project_root, 'manage.py')
cmd = []
if os.path.isfile(manage_path):
cmd = ['python', 'manage.py', 'test']
if test_path:
cmd.append(test_path)
cmd.append('--verbosity=2')
reports.append(f"🔍 Запуск тестов Django: {' '.join(cmd)}")
else:
cmd = ['pytest', '-v']
if test_path:
cmd.append(test_path)
reports.append(f"🔍 Запуск тестов pytest: {' '.join(cmd)}")
try:
result = subprocess.run(cmd, cwd=project_root, capture_output=True, text=True)
output = result.stdout + result.stderr
success = result.returncode == 0
if success:
reports.append(f"✅ Тесты прошли успешно (код {result.returncode})")
else:
reports.append(f"❌ Тесты завершились с ошибкой (код {result.returncode})")
return success, reports, output
except Exception as e:
reports.append(f"❌ Ошибка при запуске тестов: {e}")
return False, reports, str(e)
# ---------- Логирование в суточный файл ----------
def write_log_entry(log_file_path, entry_type, lines, extra_info=''):
with open(log_file_path, 'a', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write(f"ТИП: {entry_type}\n")
f.write(f"ВРЕМЯ: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
if extra_info:
f.write(f"ДОП. ИНФО: {extra_info}\n")
f.write("-" * 80 + "\n")
for line in lines:
f.write(line + "\n")
f.write("=" * 80 + "\n\n")
# ---------- ОСНОВНАЯ ФУНКЦИЯ (ЦИКЛ) ----------
def main():
project_root = os.getcwd()
ai_response_dir = os.path.join(project_root, '_ai_response')
os.makedirs(ai_response_dir, exist_ok=True)
print(color_text("=" * 60, COLOR_WHITE + COLOR_BOLD))
print(color_text(" Инструмент для работы с DeepSeek", COLOR_WHITE + COLOR_BOLD))
print(color_text("=" * 60, COLOR_WHITE + COLOR_BOLD))
while True:
print("\n" + color_text("Выберите режим:", COLOR_LIGHT_GRAY + COLOR_BOLD))
print(color_text(" 0 — Выход", COLOR_GRAY))
print(color_text(" 1 — Создать промт (собрать проект и инструкцию)", COLOR_LIGHT_GRAY))
print(color_text(" 2 — Применить ответ DeepSeek (автоматически найти JSON в Downloads)", COLOR_LIGHT_GRAY))
print(color_text(" 3 — Запустить тесты (все или указать папку/файл)", COLOR_LIGHT_GRAY))
print(color_text(" 4 — Откатить последние изменения (восстановить из бэкапов файлов)", COLOR_LIGHT_GRAY))
print(color_text(" 5 — Восстановить проект из полного бэкапа (архива)", COLOR_LIGHT_GRAY))
mode = input(color_text("Ваш выбор (0-5): ", COLOR_LIGHT_GRAY + COLOR_BOLD)).strip()
if mode == '0':
print(color_text("Выход. До свидания!", COLOR_GREEN))
break
if mode == '1':
print(color_text("\nСбор информации о проекте для отправки в DeepSeek.", COLOR_LIGHT_GRAY))
main_prompt = input(color_text("Введите ваш промт (одна строка, Enter для завершения): ", COLOR_YELLOW)).strip()
if not main_prompt:
print(color_text("Промт не может быть пустым. Завершение.", COLOR_RED))
continue
# Сохраняем запрос пользователя для последующего логирования
prompt_file = os.path.join(ai_response_dir, 'last_user_prompt.txt')
try:
with open(prompt_file, 'w', encoding='utf-8') as f:
f.write(main_prompt)
except Exception as e:
print(color_text(f"⚠️ Не удалось сохранить запрос: {e}", COLOR_YELLOW))
print(color_text(f"\nСканируем папку: {project_root}", COLOR_GRAY))
files_data = collect_project_files(project_root)
full_prompt = build_prompt(main_prompt, files_data)
output_file = os.path.join(ai_response_dir, "ai_promt.txt")
try:
with open(output_file, 'w', encoding='utf-8') as f:
f.write(full_prompt)
print(color_text(f"\n✅ Готово! Промт сохранён в файл: {output_file}", COLOR_GREEN))
print(color_text(f"Всего собрано файлов: {len(files_data)}", COLOR_GRAY))
print(color_text("\nТеперь скопируйте содержимое этого файла и отправьте в DeepSeek.", COLOR_YELLOW))
print(color_text("DeepSeek вернёт ответ с описанием и JSON-блоком.", COLOR_YELLOW))
print(color_text("Скопируйте весь ответ и сохраните в файл с именем deepseek_json_YYYYMMDD_HHMMSS.json в папке Downloads.", COLOR_YELLOW))
print(color_text("Затем запустите этот скрипт в режиме 2 для автоматического применения.", COLOR_YELLOW))
except Exception as e:
print(color_text(f"❌ Ошибка при сохранении: {e}", COLOR_RED))
elif mode == '2':
downloads_dir = get_downloads_folder()
json_file = find_latest_json_file(downloads_dir)
if json_file is None:
print(color_text(f"❌ Не найден JSON-файл с префиксом 'deepseek_json_' в папке {downloads_dir}", COLOR_RED))
continue
print(color_text(f"📂 Найден файл: {json_file}", COLOR_GRAY))
try:
with open(json_file, 'r', encoding='utf-8') as f:
text = f.read()
except Exception as e:
print(color_text(f"❌ Ошибка чтения файла: {e}", COLOR_RED))
continue
data = extract_json_from_text(text)
if data is None:
print(color_text("❌ Не удалось извлечь JSON из ответа. Убедитесь, что в ответе есть блок ```json ... ``` или валидный JSON.", COLOR_RED))
continue
actions = data.get('actions')
if actions is None:
print(color_text("❌ В JSON отсутствует поле 'actions'.", COLOR_RED))
continue
if not isinstance(actions, list):
print(color_text("❌ Поле 'actions' должно быть массивом.", COLOR_RED))
continue
if len(actions) == 0:
print(color_text("ℹ️ Нет действий для применения.", COLOR_YELLOW))
continue
# Читаем сохранённый запрос пользователя
user_prompt = ''
prompt_file = os.path.join(ai_response_dir, 'last_user_prompt.txt')
if os.path.exists(prompt_file):
try:
with open(prompt_file, 'r', encoding='utf-8') as f:
user_prompt = f.read().strip()
except Exception as e:
print(color_text(f"⚠️ Не удалось прочитать запрос пользователя: {e}", COLOR_YELLOW))
print(color_text(f"\n📁 Применяем изменения в: {project_root}", COLOR_LIGHT_GRAY))
reports, errors = apply_changes(project_root, actions, ai_response_dir, user_prompt)
dest_filename = f"applied_{os.path.basename(json_file)}"
dest_path = os.path.join(ai_response_dir, dest_filename)
shutil.move(json_file, dest_path)
move_info = f"JSON перемещён в {dest_path}"
today = datetime.now().strftime('%Y-%m-%d')
log_file = os.path.join(ai_response_dir, f'ai_log_{today}.txt')
write_log_entry(log_file, 'APPLY', reports, extra_info=move_info)
print(color_text("\n" + "=" * 60, COLOR_WHITE + COLOR_BOLD))
print(color_text("ОТЧЁТ О ВЫПОЛНЕННЫХ ДЕЙСТВИЯХ", COLOR_WHITE + COLOR_BOLD))
print(color_text("=" * 60, COLOR_WHITE + COLOR_BOLD))
for line in reports:
if line.startswith("✅"):
print(color_text(line, COLOR_GREEN))
elif line.startswith("❌"):
print(color_text(line, COLOR_RED))
elif line.startswith("⚠️"):
print(color_text(line, COLOR_YELLOW))
elif line.startswith("📦"):
print(color_text(line, COLOR_CYAN))
else:
print(line)
if errors:
print(color_text("\n❌ Были ошибки при применении изменений.", COLOR_RED))
else:
print(color_text("\n✅ Все изменения применены успешно.", COLOR_GREEN))
print(color_text(f"\n📄 Лог изменений записан в: {log_file}", COLOR_GRAY))
elif mode == '3':
test_path = input(color_text("Введите относительный путь к папке или файлу с тестами (оставьте пустым для всех тестов): ", COLOR_YELLOW)).strip()
if test_path == '':
test_path = None
print(color_text("\n🔍 Запуск тестов...", COLOR_LIGHT_GRAY))
success, reports, output = run_tests(project_root, test_path)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
test_report_file = os.path.join(ai_response_dir, f"test_report_{timestamp}.txt")
with open(test_report_file, 'w', encoding='utf-8') as f:
f.write("=" * 60 + "\n")
f.write("ОТЧЁТ О ТЕСТИРОВАНИИ\n")
f.write(f"Дата: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"Путь тестов: {test_path if test_path else 'все тесты'}\n")
f.write("=" * 60 + "\n")
f.write(output)
today = datetime.now().strftime('%Y-%m-%d')
log_file = os.path.join(ai_response_dir, f'ai_log_{today}.txt')
brief_lines = [
f"Тесты запущены для пути: {test_path if test_path else 'все тесты'}",
f"Результат: {'✅ УСПЕШНО' if success else '❌ ОШИБКА'}",
f"Полный отчёт сохранён в: {test_report_file}"
]
write_log_entry(log_file, 'TEST', brief_lines, extra_info=f"Путь: {test_path if test_path else 'все тесты'}")
print(color_text("\n" + "=" * 60, COLOR_WHITE + COLOR_BOLD))
print(color_text("РЕЗУЛЬТАТ ТЕСТИРОВАНИЯ", COLOR_WHITE + COLOR_BOLD))
print(color_text("=" * 60, COLOR_WHITE + COLOR_BOLD))
for line in reports:
if line.startswith("✅"):
print(color_text(line, COLOR_GREEN))
elif line.startswith("❌"):
print(color_text(line, COLOR_RED))
else:
print(line)
if success:
print(color_text("\n✅ Тесты прошли успешно.", COLOR_GREEN))
else:
print(color_text("\n❌ Тесты завершились с ошибками. Проверьте полный отчёт.", COLOR_RED))
print(color_text(f"\n📄 Полный отчёт сохранён в: {test_report_file}", COLOR_GRAY))
print(color_text(f"📄 Краткая запись добавлена в суточный лог: {log_file}", COLOR_GRAY))
elif mode == '4':
print(color_text("\n🔄 Откат последних применённых изменений...", COLOR_LIGHT_GRAY))
reports, errors = rollback_last_apply(project_root, ai_response_dir)
today = datetime.now().strftime('%Y-%m-%d')
log_file = os.path.join(ai_response_dir, f'ai_log_{today}.txt')
write_log_entry(log_file, 'ROLLBACK', reports, extra_info="Откат выполненных изменений")
print(color_text("\n" + "=" * 60, COLOR_WHITE + COLOR_BOLD))
print(color_text("ОТЧЁТ ОБ ОТКАТЕ", COLOR_WHITE + COLOR_BOLD))
print(color_text("=" * 60, COLOR_WHITE + COLOR_BOLD))
for line in reports:
if line.startswith("✅"):
print(color_text(line, COLOR_GREEN))
elif line.startswith("❌"):
print(color_text(line, COLOR_RED))
elif line.startswith("⚠️"):
print(color_text(line, COLOR_YELLOW))
elif line.startswith("🗑️") or line.startswith("♻️"):
print(color_text(line, COLOR_CYAN))
else:
print(line)
if errors:
print(color_text("\n❌ Были ошибки при откате.", COLOR_RED))
else:
print(color_text("\n✅ Откат выполнен успешно.", COLOR_GREEN))
print(color_text(f"\n📄 Лог отката записан в: {log_file}", COLOR_GRAY))
elif mode == '5':
backups = list_full_backups(ai_response_dir)
if not backups:
print(color_text("❌ Нет доступных полных бэкапов.", COLOR_RED))
continue
print(color_text("\nДоступные полные бэкапы:", COLOR_LIGHT_GRAY))
for idx, (path, dt, name) in enumerate(backups, 1):
print(color_text(f" {idx}. {name} ({dt.strftime('%Y-%m-%d %H:%M:%S')})", COLOR_GRAY))
choice = input(color_text("Выберите номер бэкапа для восстановления (или 0 для отмены): ", COLOR_YELLOW)).strip()
if choice == '0':
continue
try:
idx = int(choice) - 1
if idx < 0 or idx >= len(backups):
print(color_text("❌ Неверный номер.", COLOR_RED))
continue
backup_path = backups[idx][0]
except ValueError:
print(color_text("❌ Неверный ввод.", COLOR_RED))
continue
print(color_text(f"\n⚠️ Восстановление из бэкапа {os.path.basename(backup_path)} полностью заменит текущий проект.", COLOR_YELLOW))
confirm = input(color_text("Вы уверены? (введите 'да' для подтверждения): ", COLOR_YELLOW)).strip().lower()
if confirm != 'да':
print(color_text("Восстановление отменено.", COLOR_GRAY))
continue
try:
restore_full_backup(project_root, backup_path)
print(color_text(f"\n✅ Проект восстановлен из бэкапа: {os.path.basename(backup_path)}", COLOR_GREEN))
today = datetime.now().strftime('%Y-%m-%d')
log_file = os.path.join(ai_response_dir, f'ai_log_{today}.txt')
write_log_entry(log_file, 'RESTORE_FULL', [f"Восстановлен проект из {os.path.basename(backup_path)}"], extra_info="Полное восстановление")
except Exception as e:
print(color_text(f"❌ Ошибка при восстановлении: {e}", COLOR_RED))
else:
print(color_text("❌ Неверный выбор. Пожалуйста, введите число от 0 до 5.", COLOR_RED))
if __name__ == "__main__":
main()

@ -0,0 +1,74 @@
-----BEGIN CERTIFICATE-----
MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
/3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIHQjCCBSqgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
Q0EwHhcNMjIwMzAyMTEyNTE5WhcNMjcwMzA2MTEyNTE5WjBvMQswCQYDVQQGEwJS
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi
IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9YPqBKOk19NFymrE
wehzrhBEgT2atLezpduB24mQ7CiOa/HVpFCDRZzdxqlh8drku408/tTmWzlNH/br
HuQhZ/miWKOf35lpKzjyBd6TPM23uAfJvEOQ2/dnKGGJbsUo1/udKSvxQwVHpVv3
S80OlluKfhWPDEXQpgyFqIzPoxIQTLZ0deirZwMVHarZ5u8HqHetRuAtmO2ZDGQn
vVOJYAjls+Hiueq7Lj7Oce7CQsTwVZeP+XQx28PAaEZ3y6sQEt6rL06ddpSdoTMp
BnCqTbxW+eWMyjkIn6t9GBtUV45yB1EkHNnj2Ex4GwCiN9T84QQjKSr+8f0psGrZ
vPbCbQAwNFJjisLixnjlGPLKa5vOmNwIh/LAyUW5DjpkCx004LPDuqPpFsKXNKpa
L2Dm6uc0x4Jo5m+gUTVORB6hOSzWnWDj2GWfomLzzyjG81DRGFBpco/O93zecsIN
3SL2Ysjpq1zdoS01CMYxie//9zWvYwzI25/OZigtnpCIrcd2j1Y6dMUFQAzAtHE+
qsXflSL8HIS+IJEFIQobLlYhHkoE3avgNx5jlu+OLYe0dF0Ykx1PGNjbwqvTX37R
Cn32NMjlotW2QcGEZhDKj+3urZizp5xdTPZitA+aEjZM/Ni71VOdiOP0igbw6asZ
2fxdozZ1TnSSYNYvNATwthNmZysCAwEAAaOCAeUwggHhMBIGA1UdEwEB/wQIMAYB
Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTR4XENCy2BTm6KSo9MI7NM
XqtpCzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzCBxwYIKwYBBQUH
AQEEgbowgbcwOwYIKwYBBQUHMAKGL2h0dHA6Ly9yb3N0ZWxlY29tLnJ1L2NkcC9y
b290Y2Ffc3NsX3JzYTIwMjIuY3J0MDsGCCsGAQUFBzAChi9odHRwOi8vY29tcGFu
eS5ydC5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNydDA7BggrBgEFBQcwAoYv
aHR0cDovL3JlZXN0ci1wa2kucnUvY2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQw
gbAGA1UdHwSBqDCBpTA1oDOgMYYvaHR0cDovL3Jvc3RlbGVjb20ucnUvY2RwL3Jv
b3RjYV9zc2xfcnNhMjAyMi5jcmwwNaAzoDGGL2h0dHA6Ly9jb21wYW55LnJ0LnJ1
L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMDWgM6Axhi9odHRwOi8vcmVlc3Ry
LXBraS5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG9w0BAQsF
AAOCAgEARBVzZls79AdiSCpar15dA5Hr/rrT4WbrOfzlpI+xrLeRPrUG6eUWIW4v
Sui1yx3iqGLCjPcKb+HOTwoRMbI6ytP/ndp3TlYua2advYBEhSvjs+4vDZNwXr/D
anbwIWdurZmViQRBDFebpkvnIvru/RpWud/5r624Wp8voZMRtj/cm6aI9LtvBfT9
cfzhOaexI/99c14dyiuk1+6QhdwKaCRTc1mdfNQmnfWNRbfWhWBlK3h4GGE9JK33
Gk8ZS8DMrkdAh0xby4xAQ/mSWAfWrBmfzlOqGyoB1U47WTOeqNbWkkoAP2ys94+s
Jg4NTkiDVtXRF6nr6fYi0bSOvOFg0IQrMXO2Y8gyg9ARdPJwKtvWX8VPADCYMiWH
h4n8bZokIrImVKLDQKHY4jCsND2HHdJfnrdL2YJw1qFskNO4cSNmZydw0Wkgjv9k
F+KxqrDKlB8MZu2Hclph6v/CZ0fQ9YuE8/lsHZ0Qc2HyiSMnvjgK5fDc3TD4fa8F
E8gMNurM+kV8PT8LNIM+4Zs+LKEV8nqRWBaxkIVJGekkVKO8xDBOG/aN62AZKHOe
GcyIdu7yNMMRihGVZCYr8rYiJoKiOzDqOkPkLOPdhtVlgnhowzHDxMHND/E2WA5p
ZHuNM/m0TXt2wTTPL7JH2YC0gPz/BvvSzjksgzU5rLbRyUKQkgU=
-----END CERTIFICATE-----

@ -1,8 +1,9 @@
from django.urls import path from django.urls import path
from content_bot.views import api_default_load from content_bot.views import api_default_load, ui_menu_view
from max_bot.views import api_start_max_v1, iiko_webhook, iiko_send_message from max_bot.views import api_start_max_v1, iiko_webhook, iiko_send_message
urlpatterns = [ urlpatterns = [
path('default-load/<str:token>', api_default_load), path('default-load', api_default_load),
path('ui/', ui_menu_view, name='ui_menu'),
] ]

@ -1,34 +1,19 @@
from django.http import JsonResponse from django.http import JsonResponse
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import render
from max_bot.models import Client, ProductCategory, Product from max_bot.models import Client, ProductCategory, Product
# Create your views here. # Create your views here.
@csrf_exempt @csrf_exempt
def api_default_load(request: WSGIRequest, token): def api_default_load(request: WSGIRequest):
# получаем информацию по токену клиента и делаем проверку rt = {'success': True, 'data': ''}
client = Client.objects.filter(token=token).first() return JsonResponse(rt, status=200)
if not client:
rt = {'success': False, 'error': 'Unauthorized token', 'data': {}}
return JsonResponse(rt, status=401)
# делаем на базе клиента Тэнгис ID 50
categorys = ProductCategory.objects.filter(client=50, src=1, status=1)
for category in categorys:
if not ProductCategory.objects.filter(client=client, src=1, status=1, title=category.title):
pk = category.pk
new_category = category
new_category.pk = None
new_category.client = client
new_category.save()
products = Product.objects.filter(client=50, status=1, up=pk)
for product in products:
new_product = product
new_product.pk = None
new_product.client = client
new_product.up = new_category.pk
new_product.save()
rt = {'success': False, 'data': ''} def ui_menu_view(request):
return JsonResponse(rt, status=200) """Отображает главную страницу MAX UI с двумя кнопками."""
# В будущем здесь можно будет получать данные клиента из сессии или параметров запроса
# Сейчас просто рендерим шаблон
return render(request, 'max_bot/ui_menu.html')

@ -0,0 +1,755 @@
# Generated by Django 5.2 on 2026-08-08 02:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Client',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(blank=True, max_length=300)),
('title_full', models.CharField(blank=True, max_length=300)),
('token', models.CharField(blank=True, max_length=255)),
('status', models.BooleanField(default=True)),
('params', models.TextField(null=True)),
('type', models.IntegerField(null=True)),
('manager_id', models.IntegerField(null=True)),
('partner', models.BooleanField(null=True)),
('code', models.CharField(max_length=50, verbose_name='Код клиента в таблице cdr')),
('brand', models.CharField(max_length=300, verbose_name='Код клиента в таблице cdr')),
('logo', models.CharField(max_length=300)),
('email', models.CharField(blank=True, max_length=50)),
('server', models.CharField(max_length=200, verbose_name='сервер на котором работает клиент')),
('phone', models.CharField(blank=True, max_length=12)),
('dt', models.DateTimeField(auto_now_add=True)),
('managment', models.CharField(max_length=300)),
('tz', models.CharField(blank=True, max_length=50)),
('balancecontrol', models.FloatField(null=True)),
('promised_payment', models.DateTimeField(auto_now_add=True)),
('details', models.TextField(null=True)),
],
options={
'db_table': 'lk_client',
},
),
migrations.CreateModel(
name='Billing',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('submit_fd', models.DateTimeField(auto_now_add=True, verbose_name='Время начала действия записи')),
('submit_td', models.DateTimeField(verbose_name='Время завершения действия записи')),
('balance', models.FloatField(default=10, verbose_name='Баланс пользователя')),
('payment', models.FloatField(default=10)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_billing',
},
),
migrations.CreateModel(
name='BillingOrder',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('paysys', models.CharField(max_length=100)),
('descr', models.CharField(max_length=300)),
('amount', models.FloatField()),
('status', models.SmallIntegerField()),
('error', models.CharField(max_length=100)),
('payment_id', models.CharField(max_length=100)),
('payment', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Время начала действия записи')),
('updated_at', models.DateTimeField(verbose_name='Время завершения действия записи')),
('control_status', models.SmallIntegerField(blank=False)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_billing_order',
},
),
migrations.CreateModel(
name='AuthAccess',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Время начала действия записи')),
('ip_address', models.CharField(max_length=30, verbose_name='ip адрес сети')),
('ip_netmask', models.IntegerField(default=32, verbose_name='Маска подсети')),
('status', models.BooleanField(default=True, verbose_name='включен или нет')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'au_access',
},
),
migrations.CreateModel(
name='AuthPhone',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('phone', models.CharField(max_length=11, verbose_name='Номер телефона для аутификации')),
('data_last', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Время последнего использования')),
('code_auth', models.CharField(max_length=6, null=True, verbose_name='Код аутификации')),
('status', models.BooleanField(default=True, verbose_name='Включение номера')),
('operator', models.CharField(max_length=6, null=True, verbose_name='Оператор')),
('server', models.CharField(max_length=6, null=True, verbose_name='ссылка на сервер')),
('phone_type', models.CharField(max_length=20, null=True, verbose_name='Используется для определенного типа авторизации')),
('counter', models.IntegerField(default=0, verbose_name='счетчик звонков')),
('device_info', models.CharField(max_length=255, null=True, verbose_name='ссылка на сервер')),
('device_prefix', models.CharField(max_length=255, null=True, verbose_name='ссылка на сервер')),
('call_error', models.IntegerField(default=0, verbose_name='счетчик ошибок')),
('call_limit', models.IntegerField(default=0, verbose_name='лимит запросов в день')),
('data_last_auth', models.DateTimeField(null=True, verbose_name='Время последнего использования обратной авторизации')),
('user_blocking', models.BooleanField(default=True, verbose_name='Включение номера')),
('creation_date', models.DateTimeField(auto_now_add=True)),
('closing_date', models.DateTimeField()),
],
options={
'db_table': 'au_phone',
},
),
migrations.CreateModel(
name='AuthLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Время создание записи')),
('code_auth', models.CharField(max_length=6, null=True, verbose_name='Код аутификации')),
('status', models.IntegerField(default=0, verbose_name='Статус выполнения запроса 0 - не выполнен, 1 - отправлен на выполнение, 2 - подтвержден')),
('status_info', models.CharField(max_length=200, null=True)),
('status_sms', models.CharField(max_length=200, null=True)),
('phone', models.CharField(max_length=11, null=True, verbose_name='Номер телефона которому отправлен код дозвона')),
('price', models.FloatField(default=0, verbose_name='Стоимость услуги')),
('uid', models.CharField(max_length=100, null=True, verbose_name='Индификатор отправки сообщения')),
('auth_type', models.CharField(max_length=11, null=True, verbose_name='Тип авторизации')),
('web_hook', models.TextField(null=True)),
('web_hook_status', models.TextField(null=True)),
('sms_send', models.BooleanField(verbose_name='Отправка смс если не прошел звонок')),
('operator', models.CharField(max_length=250, verbose_name='Имя оператора')),
('auth_phone', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='max_bot.authphone', verbose_name='Данные аутификации')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
('auth_billing', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='max_bot.billing', verbose_name='Сылка на билинг')),
],
options={
'db_table': 'au_log',
},
),
migrations.CreateModel(
name='AuthPayment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Время создание записи')),
('partner_id', models.IntegerField(verbose_name='Данные партнера')),
('payment', models.FloatField(verbose_name='Сумма платежа')),
('source', models.CharField(default='api', max_length=10, verbose_name='Источник платежа')),
('info', models.CharField(max_length=500, null=True, verbose_name='Дополнительная информация')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_payment',
},
),
migrations.CreateModel(
name='Users',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, null=True)),
('avatar', models.CharField(max_length=300, null=True)),
('surname', models.CharField(max_length=50, null=True)),
('phone', models.CharField(default='89000000000', max_length=11, null=True, verbose_name='Номер телефона')),
('status', models.BooleanField(default=True, verbose_name='Включение номера')),
('role', models.IntegerField(default=2)),
('uid', models.CharField(max_length=50)),
('email', models.CharField(max_length=50)),
('passwd', models.CharField(max_length=100)),
('dt', models.DateTimeField(auto_now_add=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_usr',
},
),
migrations.CreateModel(
name='Contact',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('up', models.IntegerField()),
('group', models.IntegerField()),
('surname', models.CharField(max_length=100, null=True)),
('name', models.CharField(max_length=100, null=True)),
('patronymic', models.CharField(max_length=100, null=True)),
('phone', models.CharField(max_length=40, null=True)),
('telegram', models.CharField(max_length=100, null=True)),
('maxx', models.CharField(max_length=100, null=True)),
('status', models.IntegerField(default=1)),
('dt', models.DateTimeField(auto_now_add=True)),
('qr_menu', models.BooleanField(default=False)),
('webast_bot', models.BooleanField(default=False)),
('descr', models.CharField(max_length=500, null=True)),
('field1', models.CharField(max_length=100, null=True)),
('field2', models.CharField(max_length=100, null=True)),
('field3', models.CharField(max_length=100, null=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='max_bot.client', verbose_name=' id client')),
],
options={
'db_table': 'lk_contact',
},
),
migrations.CreateModel(
name='CdrRecord',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('calldate', models.DateTimeField(null=True)),
('src', models.CharField(max_length=80, null=True)),
('dst', models.CharField(max_length=80, null=True)),
('billsec', models.IntegerField(default=0, null=True)),
('accountcode', models.CharField(max_length=20, null=True)),
('uniqueid', models.CharField(max_length=32)),
('disposition', models.CharField(max_length=45)),
('duration', models.IntegerField(null=True)),
('userfield', models.CharField(max_length=32)),
('dstchannel', models.CharField(max_length=80)),
('category_call', models.CharField(max_length=50)),
('send_sms', models.BooleanField()),
('filename', models.CharField(max_length=250)),
('channel', models.CharField(max_length=80)),
('lastdata', models.TextField()),
('lastapp', models.TextField()),
],
options={
'db_table': 'cdr',
},
),
migrations.CreateModel(
name='QueueTime',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('src', models.CharField(max_length=20, verbose_name='Номер абонента')),
('dst', models.CharField(max_length=20, verbose_name='Номер агента')),
('calldate', models.DateTimeField(auto_now_add=True, verbose_name='Время записи')),
('billsec', models.IntegerField(default=0, verbose_name='Время ожидания')),
('agent_code', models.IntegerField(verbose_name='Время ожидания')),
('sla', models.IntegerField(default=0, verbose_name='параметр качества обслуживания 10 - до 10 секунд, 15 - до 15 сек, 20, 30, 100 более 30 секунд')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_queue_time',
},
),
migrations.CreateModel(
name='SmsConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.CharField(max_length=40, verbose_name='Имя пользователя')),
('password', models.CharField(max_length=40, verbose_name='Пароль')),
('url', models.CharField(max_length=255, verbose_name='Ссылка')),
('type', models.CharField(max_length=10, verbose_name='тип рассылки')),
('params', models.CharField(max_length=255, verbose_name='Ссылка')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_sms_config',
},
),
migrations.CreateModel(
name='EmailBase',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Время создание записи')),
('email', models.CharField(max_length=11, verbose_name='email')),
('mac_address', models.CharField(blank=True, max_length=20, verbose_name='mac')),
('description', models.CharField(max_length=50, verbose_name='Дополнительная информация')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_emails',
},
),
migrations.CreateModel(
name='Operator',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('prefix', models.CharField(max_length=11, verbose_name='Префикс оператора сети')),
('operator', models.CharField(max_length=11, verbose_name='Оператор')),
],
options={
'db_table': 'au_operator',
},
),
migrations.CreateModel(
name='DefCode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code_def', models.IntegerField()),
('code_from', models.IntegerField()),
('code_to', models.IntegerField()),
('container', models.IntegerField()),
('operator', models.CharField(max_length=255, verbose_name='Оператор')),
('inn', models.IntegerField()),
('region', models.CharField(max_length=255, verbose_name='Оператор')),
],
options={
'db_table': 'au_defcode',
},
),
migrations.CreateModel(
name='Term',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('up', models.IntegerField()),
('term', models.CharField(max_length=200)),
('val', models.CharField(max_length=100)),
('vali', models.IntegerField()),
('html', models.TextField()),
('params', models.TextField()),
('status', models.BooleanField()),
('dt', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'lk_term',
},
),
migrations.CreateModel(
name='App',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('param', models.TextField()),
('descr', models.CharField(max_length=255)),
('type', models.CharField(max_length=100)),
('status', models.SmallIntegerField()),
('dt', models.DateTimeField(auto_now_add=True, verbose_name='Время создание записи')),
('update_at', models.DateTimeField(verbose_name='Время обновления записи')),
('app', models.CharField(max_length=255, verbose_name='приложение обработки')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_app',
},
),
migrations.CreateModel(
name='Task',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('mess', models.TextField()),
('status', models.IntegerField()),
('prev_status', models.IntegerField()),
('reason', models.CharField(max_length=255)),
('priority', models.IntegerField()),
('dt_start', models.DateTimeField(auto_now_add=True, verbose_name='Время запуска приложения')),
('dt', models.DateTimeField(auto_now_add=True, verbose_name='Время создание записи')),
('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.app', verbose_name='Сылка на приложение')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_task',
},
),
migrations.CreateModel(
name='TaskDetail',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=255)),
('mess', models.TextField()),
('cnt', models.IntegerField()),
('status', models.IntegerField()),
('reason', models.CharField(max_length=255)),
('dt', models.DateTimeField(verbose_name='Время создание записи')),
('update_at', models.DateTimeField(verbose_name='Время обновления записи')),
('external_id', models.CharField(max_length=255)),
('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.contact', verbose_name='Данные пользователя')),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.task', verbose_name='Сылка на приложение')),
],
options={
'db_table': 'lk_task_detail',
},
),
migrations.CreateModel(
name='Price',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('price', models.FloatField()),
('description', models.CharField(max_length=255)),
('service', models.CharField(max_length=255)),
('category', models.CharField(max_length=255)),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Время начала действия записи')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_price',
},
),
migrations.CreateModel(
name='Partner',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('title', models.CharField(max_length=100)),
('logo', models.CharField(max_length=100)),
('descr', models.CharField(max_length=300)),
('status', models.IntegerField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('slug', models.CharField(max_length=30)),
('setting', models.TextField()),
('token_partner', models.CharField(max_length=50)),
('token_api', models.CharField(max_length=50)),
('application_id', models.IntegerField()),
],
options={
'db_table': 'lk_integration_partner',
},
),
migrations.CreateModel(
name='Integration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(max_length=100)),
('setting', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('status', models.IntegerField()),
('updated_at', models.DateTimeField(auto_now=True)),
('partner_client', models.CharField(max_length=255)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client')),
('partner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.partner')),
],
options={
'db_table': 'lk_integration',
},
),
migrations.CreateModel(
name='SipTrunk',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('provider', models.CharField(max_length=50)),
('phone', models.CharField(max_length=50)),
('passwd', models.CharField(max_length=50)),
('status', models.BooleanField()),
('dt', models.DateTimeField(auto_now=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client')),
],
options={
'db_table': 'lk_sip_trunk',
},
),
migrations.CreateModel(
name='Balance',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(max_length=50)),
('accountcode', models.CharField(max_length=100)),
('domain', models.CharField(max_length=100)),
('login', models.CharField(max_length=50)),
('secret', models.CharField(max_length=100)),
('balance', models.FloatField()),
('status', models.IntegerField()),
('params', models.CharField(max_length=500)),
('description', models.CharField(max_length=500)),
('error', models.CharField(max_length=300)),
('dt', models.DateTimeField(auto_now_add=True)),
('informing_blocking', models.DateTimeField()),
('informing_balance', models.DateTimeField()),
('balance_save', models.FloatField()),
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='max_bot.client')),
],
options={
'db_table': 'lk_balance',
},
),
migrations.CreateModel(
name='SipPeer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('num', models.CharField(max_length=100)),
('secret', models.CharField(max_length=100)),
('ipaddress', models.CharField(max_length=100)),
('ipport', models.CharField(max_length=100)),
('accountcode', models.CharField(max_length=100)),
('description', models.CharField(max_length=100)),
('type', models.CharField(max_length=100)),
('status', models.BooleanField()),
('op_position', models.CharField(max_length=100)),
('op_fio', models.CharField(max_length=100)),
('op_email', models.CharField(max_length=100)),
('dt', models.DateTimeField(auto_now_add=True)),
('cfg_server', models.CharField(max_length=50)),
('cfg_port', models.CharField(max_length=10)),
('operator', models.CharField(max_length=100)),
('control_password', models.DateTimeField()),
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='max_bot.client')),
],
options={
'db_table': 'lk_sip_peer',
},
),
migrations.CreateModel(
name='BillingLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(verbose_name='Время начала действия записи')),
('accruals', models.FloatField()),
('function_init', models.CharField(max_length=255)),
('description', models.CharField(max_length=255)),
('billing', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='max_bot.billing', verbose_name='Данные пользователя')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
('price', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='max_bot.price', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_billing_log',
},
),
migrations.CreateModel(
name='SmsLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Время создание записи')),
('phone', models.CharField(max_length=20, verbose_name='Номер телефона')),
('text', models.TextField(blank=True, verbose_name='Текст')),
('status', models.IntegerField(default=False, verbose_name='Статус')),
('status_info', models.CharField(blank=True, max_length=300, verbose_name='Информация о статусе')),
('uid', models.CharField(max_length=50, verbose_name='Индификатор сообщения от оператора')),
('price', models.FloatField()),
('provider', models.CharField(max_length=50, verbose_name='Индификатор сообщения от оператора')),
('user_answer', models.CharField(max_length=255, null=True, verbose_name='ответ на сообщение')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
('task', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='max_bot.task', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_sms_log',
},
),
migrations.CreateModel(
name='BillingService',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Время начала действия записи')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата проведения билинга')),
('status', models.IntegerField(default=True, verbose_name='Статус')),
('periodicity', models.CharField(max_length=255, verbose_name='Частотан начисления date, month')),
('is_blocked', models.IntegerField(default=False, verbose_name='Статус блокировки услуги')),
('blocked_at', models.DateTimeField(auto_now=True, verbose_name='Дата блокировки')),
('app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='max_bot.app')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
('price', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.price')),
],
options={
'db_table': 'lk_billing_service',
},
),
migrations.CreateModel(
name='SmsSession',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('submit_fd', models.DateTimeField(auto_now_add=True, verbose_name='Время создание записи')),
('submit_td', models.DateTimeField(verbose_name='Время создание записи')),
('phone', models.CharField(max_length=11, verbose_name='Номер телефона')),
('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.app')),
('sms_log', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.smslog', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_sms_session',
},
),
migrations.CreateModel(
name='SmsAnswer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('request', models.CharField(max_length=255, verbose_name='Номер телефона')),
('answer', models.TextField()),
('constantly', models.BooleanField(default=False)),
('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.app')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_sms_answer',
},
),
migrations.CreateModel(
name='HotelBooking',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uid', models.IntegerField()),
('arrival', models.DateTimeField()),
('departure', models.DateTimeField()),
('regdate', models.DateTimeField()),
('upddate', models.DateTimeField()),
('folio', models.BigIntegerField()),
('name', models.CharField(max_length=100)),
('phone', models.CharField(max_length=11)),
('email', models.CharField(max_length=50)),
('summa', models.FloatField()),
('uid_bitrix', models.IntegerField()),
('status_booking', models.IntegerField()),
('contact_id', models.BigIntegerField()),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_hotel_booking',
},
),
migrations.CreateModel(
name='WP',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('host', models.CharField(max_length=100)),
('port', models.IntegerField()),
('session', models.CharField(max_length=50)),
('status', models.IntegerField()),
('created_at', models.DateTimeField()),
('updated_at', models.DateTimeField()),
('phone', models.CharField(max_length=50)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_whapsapp',
},
),
migrations.CreateModel(
name='DialplanConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('webhook', models.CharField(max_length=255)),
('tg_send_contact', models.BooleanField()),
('tg_send_missed', models.BooleanField()),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'lk_sip_dialplan_config',
},
),
migrations.CreateModel(
name='ServerConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('server_type', models.CharField(max_length=10)),
],
options={
'db_table': 'lk_sip_server',
},
),
migrations.CreateModel(
name='MessageTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('type', models.CharField(max_length=100)),
('html', models.TextField()),
('params', models.JSONField()),
('app_id', models.IntegerField()),
('dt', models.DateTimeField(auto_now_add=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client', verbose_name='Данные пользователя')),
],
options={
'db_table': 'vi_mess_template',
},
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('up', models.IntegerField()),
('title', models.CharField(help_text='Наименование продукции', max_length=200, null=True)),
('descr', models.CharField(help_text='Описание продукции', max_length=200, null=True)),
('img', models.CharField(help_text='Картинка продукции', max_length=200, null=True)),
('url', models.CharField(help_text='Картинка продукции', max_length=200, null=True)),
('price', models.FloatField(help_text='Стоимотсь продукции')),
('sku', models.CharField(help_text='идентификационный код товарной позиции у партнера', max_length=100, null=True)),
('status', models.IntegerField()),
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='max_bot.client')),
],
options={
'db_table': 'lk_product',
},
),
migrations.CreateModel(
name='ProductCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('emojii', models.CharField(help_text='Картинка товара', max_length=20, null=True)),
('title', models.CharField(help_text='Наименование категории', max_length=250, null=True)),
('slug', models.CharField(help_text='Наименование категории', max_length=250, null=True)),
('descr', models.CharField(help_text='Описание', max_length=200, null=True)),
('src', models.IntegerField(help_text='для кого сделана категория 01-1 telegram, 10-2-qrmenu, 11-3 qrmenu and telegram')),
('status', models.BooleanField(default=0, help_text='отображать или нет категорию')),
('ord', models.IntegerField(help_text='порядок отображения')),
('dt', models.DateTimeField(help_text='дата создания')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='max_bot.client', verbose_name='id client')),
],
options={
'db_table': 'lk_product_category',
},
),
migrations.CreateModel(
name='BonusTransaction',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('source', models.CharField(max_length=100)),
('sum', models.FloatField()),
('balance', models.FloatField()),
('status', models.IntegerField()),
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='max_bot.client')),
('contact', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='max_bot.contact')),
],
options={
'db_table': 'lk_qr_bonus_transaction',
},
),
migrations.CreateModel(
name='Qr',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('domain', models.CharField(blank=True, max_length=100, null=True)),
('keywords', models.CharField(blank=True, max_length=500, null=True)),
('brand', models.CharField(blank=True, max_length=300, null=True)),
('title', models.CharField(blank=True, max_length=300, null=True)),
('description', models.TextField(blank=True, null=True)),
('activity', models.CharField(blank=True, max_length=100, null=True)),
('thumb', models.CharField(blank=True, max_length=300, null=True)),
('slider', models.CharField(blank=True, max_length=300, null=True)),
('sections', models.TextField(blank=True, null=True)),
('addr', models.CharField(blank=True, max_length=300, null=True)),
('phone', models.CharField(blank=True, max_length=50, null=True)),
('worktime', models.CharField(blank=True, max_length=100, null=True)),
('links', models.TextField(blank=True, null=True)),
('metrika', models.TextField(blank=True, null=True)),
('status', models.IntegerField(blank=True, null=True)),
('dt', models.DateTimeField(auto_now_add=True, null=True)),
('logo', models.CharField(blank=True, max_length=255, null=True)),
('feedback', models.CharField(blank=True, max_length=255, null=True)),
('type', models.CharField(blank=True, max_length=30, null=True)),
('lng', models.CharField(default='ru', max_length=10)),
('setting', models.CharField(blank=True, max_length=255, null=True)),
('theme', models.CharField(blank=True, max_length=100, null=True)),
('base_limit', models.IntegerField(blank=True, null=True)),
('vkpxl', models.CharField(blank=True, max_length=100, null=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='max_bot.client')),
],
options={
'db_table': 'lk_qr',
},
),
migrations.CreateModel(
name='PromoCode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('promo', models.CharField(max_length=50, unique=True)),
('chat_id', models.CharField(max_length=50)),
('messenger', models.CharField(default='max', max_length=20)),
('used', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('used_at', models.DateTimeField(blank=True, null=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='max_bot.client')),
],
options={
'db_table': 'lk_promo_code',
},
),
]

@ -6,4 +6,5 @@ urlpatterns = [
path('iiko/webhook/<str:token>', iiko_webhook), path('iiko/webhook/<str:token>', iiko_webhook),
path('iiko/send-message/<str:token>', iiko_send_message), path('iiko/send-message/<str:token>', iiko_send_message),
] ]

@ -8,7 +8,8 @@ from max_bot.models import Partner, Integration, Client, BonusTransaction, App,
from restoran_max_bot import settings from restoran_max_bot import settings
from restoran_max_bot.bot_started import bot_started from restoran_max_bot.bot_started import bot_started
from restoran_max_bot.bot_stopped import bot_stopped from restoran_max_bot.bot_stopped import bot_stopped
from restoran_max_bot.bot_message import bot_message, bot_promo from restoran_max_bot.bot_message import bot_message
from restoran_max_bot.promo_handlers import bot_promo
from restoran_max_bot.common import common_get_contact, common_check_registration from restoran_max_bot.common import common_get_contact, common_check_registration
from restoran_max_bot.settings import DEBUG from restoran_max_bot.settings import DEBUG
from restoran_max_bot.utils import is_json, has_key from restoran_max_bot.utils import is_json, has_key
@ -62,7 +63,6 @@ def api_decorator(func):
@csrf_exempt @csrf_exempt
@api_decorator @api_decorator
def api_start_max_v1(request, token, **kwargs): def api_start_max_v1(request, token, **kwargs):
# основной обработчик MAX-БОТ
data = json.loads(request.body.decode('utf-8')) data = json.loads(request.body.decode('utf-8'))
client = kwargs['client'] client = kwargs['client']
settings_max = kwargs['settings_max'] settings_max = kwargs['settings_max']
@ -83,9 +83,6 @@ def api_start_max_v1(request, token, **kwargs):
# start - запуск бота # start - запуск бота
if data['update_type'] == 'bot_started': if data['update_type'] == 'bot_started':
# https://dev.max.ru/docs/chatbots/bots-coding/prepare
# https://max.ru/id032606883607_2_bot?start=source_site
# роверка на промокод
if data.get("payload", None): if data.get("payload", None):
bot_promo(client=client, message=data, settings_max=settings_max) bot_promo(client=client, message=data, settings_max=settings_max)
if not common_check_registration(client=client, chat_id=data['chat_id']): if not common_check_registration(client=client, chat_id=data['chat_id']):

@ -0,0 +1,33 @@
# ai_handler.py
from ai_agent.api_common import get_ai_agent, call_ai_agent
from max_bot.models import Client
from restoran_max_bot.user_logger import log_user_action
def process_with_ai(client: Client, chat_id: str, user_query: str, contact) -> dict:
"""Вызывает AI-агента и возвращает структурированный ответ."""
ai_agent = get_ai_agent(client)
if not ai_agent.get('ai_agent'):
return None
real_name = contact.name.strip() if contact else None
response = call_ai_agent(
client=client,
session_id=str(chat_id),
user_query=user_query,
contact_name=real_name
)
# Логируем сам факт обращения к AI (детали будут добавлены в bot_message)
# Здесь мы можем залогировать, что AI был вызван, но основное логирование уже есть в bot_message
# Поэтому оставляем пустым или можно залогировать вызов
log_user_action(
client=client,
chat_id=chat_id,
action_type='ai_call',
user_query=user_query,
details={
'ai_agent': ai_agent.get('ai_agent'),
'contact_name': real_name
}
)
return response

@ -1,327 +1,133 @@
# bot_message.py # bot_message.py
import re import re
import json
import qrcode import qrcode
from ai_agent.api_common import get_ai_agent, call_ai_agent, get_contact_meta from max_bot.models import Client, Contact, PromoCode
from max_bot.max_api import *
from max_bot.models import PromoCode
from restoran_max_bot.bot_started import check_registration from restoran_max_bot.bot_started import check_registration
from restoran_max_bot.common import * from restoran_max_bot.common import common_get_data, common_set_contact, common_check_registration
from restoran_max_bot.iiko_api import customer_info, customer_create, customer_wallet from restoran_max_bot.message_sender import send_text, send_image, send_menu, send_feedback_buttons, send_booking_people_buttons, send_link_button
from restoran_max_bot.rkiper_api import rkiper_customer_info, rkiper_customer_create from restoran_max_bot.command_handlers import handle_standard_command, handle_ai_response
from restoran_max_bot.ai_handler import process_with_ai
from restoran_max_bot.settings import STATICFILES_DIRS, BASE_URL from restoran_max_bot.settings import STATICFILES_DIRS, BASE_URL
from restoran_max_bot.utils import has_key, is_json from restoran_max_bot.utils import is_json
from restoran_max_bot.user_logger import log_user_action
# ---------- вспомогательные функции ---------- def bot_message(client: Client, message: dict, settings_max: dict, message_type: str) -> bool:
def send_booking_to_crm(client: Client, date, time, people, phone, name): """
# реализация бронирования столика Основной обработчик входящих сообщений от Max бота.
msg = f'Бронирование столика:\nНа дату: {date}-{time} \nКоличество: {people}\nКонтакт: {phone}\nИмя: {name}' """
send_message_all_manager(client, msg)
return True
def send_contact_to_admin(client: Client, phone, contact_method, question, preferred_time, name):
# реализовать отправку администратору
msg = (f'Гость попросил связаться:\n {contact_method}\nВопрос: {question}\n'
f'Удобное время: {preferred_time}\nКонтакт: {phone}\nИмя: {name}')
send_message_all_manager(client, msg)
return True
def send_feedback_to_crm(client: Client, rating, feedback_text, name, phone):
# реализовать отправку отзыва в CRM
msg = f'Отзыв от {name} (тел. {phone}):\nОценка: {rating}/5\nТекст: {feedback_text}'
send_message_all_manager(client, msg)
return True
# ---------- получение бонусов ----------
def get_bonus(client: Client, phone: str, default_bonus: float, contact: Contact):
integration = common_get_integration(client=client)
bonus = 0
date = contact.field1
if len(date) < 2:
date = '0' + date
month = contact.field2
if len(month) < 2:
month = '0' + month
if integration:
if integration.partner.slug == 'iiko':
config = json.loads(integration.setting)
customerinfo = customer_info(config['token'], phone, config['id_org'])
if not customerinfo:
birthday = f"1990-{month}-{date} 00:00:00.000"
dt = customer_create(api_key=config['token'], phone=phone, name=contact.name,
organization_id=config['id_org'], birthday=birthday)
dt = customer_info(config['token'], phone, config['id_org'])
customer_wallet(api_key=config['token'], iiko_wallet_id=dt['walletBalances'][0]['id'],
customer_id=dt['id'], organization_id=config['id_org'],
bonus=default_bonus)
bonus = default_bonus
else:
bonus = customerinfo['walletBalances'][0]['balance']
if integration.partner.slug == 'rkeeper':
config = json.loads(integration.setting)
rs = rkiper_customer_info(rkiper_server=config['url'], rkiper_token=config['token'], phone=phone)
if len(rs):
rs = rs[0]
if has_key(rs, 'card_use'):
bonus = int(rs['card_use']['sum1']) / 100
else:
birthday = f"1990-{month}-{date}"
rs = rkiper_customer_create(rkiper_server=config['url'], rkiper_token=config['token'], phone=phone,
name=contact.name, birthday=birthday, bonus=config['bonus'],
discount=config['discount'])
bonus = 0
return bonus
# ---------- получение вложений ----------
def get_attachments(message: dict):
if 'attachments' in message['message']['body']:
if 'vcf_info' in message['message']['body']['attachments'][0]['payload']:
match = re.search(r'TEL;TYPE=cell:(\d+)',
message['message']['body']['attachments'][0]['payload']['vcf_info'])
if match:
phone_number = match.group(1)
return {'attachments_type': 'vcf_info', 'data': phone_number}
return {'attachments_type': False, 'data': ''}
# ---------- основной обработчик ----------
def bot_message(client: Client, message: dict, settings_max: dict, message_type):
chat_id = message['message']['recipient']['chat_id'] chat_id = message['message']['recipient']['chat_id']
attachments = get_attachments(message=message) token = settings_max['token']
# регистрация по vcf # 1. Обработка вложений (контакт из vcf)
if attachments['attachments_type'] == 'vcf_info': attachments = _get_attachments(message)
common_set_contact(client=client, uid_client=chat_id, name=message['message']['sender']['name'], if attachments['type'] == 'vcf_info':
phone=attachments['data']) common_set_contact(
client=client,
uid_client=chat_id,
name=message['message']['sender']['name'],
phone=attachments['data']
)
# Подменяем текст на системную команду регистрации
message['message']['body']['text'] = 'system_registration' message['message']['body']['text'] = 'system_registration'
# 2. Проверка регистрации
contact = check_registration(client=client, chat_id=chat_id, message=message, settings=settings_max) contact = check_registration(client=client, chat_id=chat_id, message=message, settings=settings_max)
if not contact: if not contact:
return False return False
# ---------- проверка и запрос имени ---------- # 3. Получение текста запроса
meta = get_contact_meta(contact) user_query = _extract_query(message, message_type)
if not user_query:
# ---------- обработка стандартных команд (меню, бонусы и т.п.) ---------- send_menu(chat_id, token, client)
data = None
if message_type == 'message_created':
data = common_get_data(client=client, key=str(message['message']['body']['text']).lower())
elif message_type == 'message_callback':
data_payload = json.loads(message['callback']['payload'])
data = common_get_data(client=client, key=str(data_payload['data']).lower())
if data:
for dt in data:
if dt.title == '##bonus##':
img = qrcode.make(contact.phone)
img.save(f"{STATICFILES_DIRS[0]}/client_qr/{contact.phone}.png")
url = f"https://maxbot.telefon-ip.ru/static/client_qr/{contact.phone}.png"
maxbot_send_img_message(chat_id=chat_id, max_token=settings_max['token'], img=url,
message='QR-код для начисления, списания бонусов.')
bonus = get_bonus(client=client, phone=contact.phone, default_bonus=float(dt.price), contact=contact)
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'],
message=f"💰 Ваш бонусный баланс: {bonus} руб")
elif dt.title == '##feedback##':
maxbot_send_feedback_button(client=client, message='👇', chat_id=chat_id,
max_token=settings_max['token'])
return True
elif dt.title == '##booking##':
maxbot_send_booking_count_people_button(client=client, message='👇',
chat_id=chat_id, max_token=settings_max['token'])
return True
# сложные кнопки
if is_json(dt.title):
data_json = json.loads(dt.title)
if 'type' in dt.title and data_json.get('type') == 'linkbutton':
maxbot_send_link_button(text=dt.descr, title_button=data_json['button'], link=dt.url,
chat_id=chat_id, max_token=settings_max['token'])
if dt.img:
url = f"https://cdn.telefon-ip.ru/{dt.img}?thumb=600"
maxbot_send_img_message(chat_id=chat_id, max_token=settings_max['token'], img=url, message=dt.descr)
if '##' not in dt.title and not dt.img and not is_json(dt.title):
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.descr)
if dt.url and not is_json(dt.title):
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.url)
maxbot_send_menu_button(chat_id=chat_id, max_token=settings_max['token'], message='👇 Выберите раздел',
client=client)
return True return True
else:
# если при нажатии на кнопку небыл найден контент, тогда для AI передаем имя кнопки
if message_type == 'message_callback':
message['message']['body']['text'] = str(data_payload['data']).lower()
# ---------- обработка через AI ---------- # Логируем входящий запрос
# Проверяем, есть ли AI-агент у клиента log_user_action(
ai_agent = get_ai_agent(client)
if not ai_agent.get('ai_agent'):
maxbot_send_menu_button(chat_id=chat_id, max_token=settings_max['token'],
message='👇 Выберите раздел', client=client)
return True
real_name = contact.name.strip()
# Вызываем AI-агента через универсальную функцию, передавая реальное имя
response = call_ai_agent(
client=client, client=client,
session_id=str(chat_id),
user_query=message['message']['body']['text'],
contact_name=real_name # передаём имя для персонализации
)
# ---------- обработка ответа AI ----------
intent = response.get('intent')
message_text = response.get('message', '')
entities = response.get('entities', {})
status = response.get('status', 'complete')
if intent == 'general':
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
elif intent == 'booking':
if status == 'need_more_info':
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
elif status == 'complete':
date = entities.get('date')
time = entities.get('time')
people = entities.get('people')
phone = entities.get('phone')
if date and time and people:
success = send_booking_to_crm(client, date, time, people, phone, real_name)
if success:
confirm_msg = (f"✅ Бронирование на {date} в {time} на {people} чел. принято! "
f"Администратор свяжется с вами.")
else:
confirm_msg = "❌ Не удалось забронировать. Попробуйте позже."
maxbot_send_text_message(chat_id, settings_max['token'], confirm_msg)
else:
maxbot_send_text_message(chat_id, settings_max['token'], "⚠️ Не хватает данных для бронирования.")
else:
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
elif intent == 'contact_admin':
if status == 'need_more_info':
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
elif status == 'complete':
phone = entities.get('phone')
contact_method = entities.get('contact_method')
question = entities.get('question', '')
preferred_time = entities.get('preferred_time', '')
if phone and contact_method:
success = send_contact_to_admin(client, phone, contact_method, question, preferred_time, real_name)
if success:
confirm_msg = f"✅ Ваш запрос передан администратору. Способ связи: {contact_method}. Скоро с вами свяжутся."
else:
confirm_msg = "❌ Не удалось отправить запрос. Попробуйте позже."
maxbot_send_text_message(chat_id, settings_max['token'], confirm_msg)
else:
maxbot_send_text_message(chat_id, settings_max['token'],
"⚠️ Не хватает данных для связи с администратором.")
else:
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
elif intent == 'feedback':
if status == 'need_more_info':
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
elif status == 'complete':
rating = entities.get('rating')
feedback_text = entities.get('feedback_text', '')
if rating and feedback_text:
success = send_feedback_to_crm(client, rating, feedback_text, real_name, contact.phone)
if success:
confirm_msg = "✅ Спасибо за ваш отзыв! Мы учтём его."
else:
confirm_msg = "❌ Не удалось отправить отзыв. Попробуйте позже."
maxbot_send_text_message(chat_id, settings_max['token'], confirm_msg)
else:
maxbot_send_text_message(chat_id, settings_max['token'], "⚠️ Не хватает данных для отзыва.")
else:
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
else:
maxbot_send_text_message(chat_id, settings_max['token'], message_text)
# всегда отправляем меню после ответа
maxbot_send_menu_button(chat_id=chat_id, max_token=settings_max['token'],
message='👇 Выберите раздел', client=client)
return True
def bot_promo(client: Client, message: dict, settings_max: dict):
chat_id = message['chat_id']
promo = message['payload'].lower()
messenger = 'max-bot'
client_id = client.id
# Проверяем, существует ли промокод в системе (через common_get_data)
data = common_get_data(client=client, key=promo)
if not data:
# Если промокод не заведён – ничего не отправляем (скрываем факт)
return True
# Пытаемся найти или создать запись в БД
promo_obj, created = PromoCode.objects.get_or_create(
promo=promo,
chat_id=chat_id, chat_id=chat_id,
client=client, action_type='incoming_message',
defaults={ user_query=user_query,
'messenger': messenger, details={
'used': False, 'message_type': message_type,
'contact_name': contact.name,
'phone': contact.phone
} }
) )
# Если запись уже существовала # 4. Обработка стандартных команд (меню, бонусы и т.п.)
if not created: data = _get_command_data(client, message, message_type)
if promo_obj.used: if data:
# Уже использован – уведомляем handled = handle_standard_command(client, chat_id, settings_max, data, contact, command_key=user_query)
maxbot_send_text_message( if handled:
log_user_action(
client=client,
chat_id=chat_id, chat_id=chat_id,
max_token=settings_max['token'], action_type='standard_command_handled',
message="⚠️ Этот промокод уже был использован." command=user_query,
details={
'command_type': 'standard',
'contact_name': contact.name
}
) )
else: return True
# QR-код уже был отправлен ранее – напоминаем
maxbot_send_text_message( # 5. Если не стандартная команда — пробуем AI
response = process_with_ai(client, chat_id, user_query, contact)
if response:
handle_ai_response(client, chat_id, token, response, contact)
# Логируем ответ AI
log_user_action(
client=client,
chat_id=chat_id, chat_id=chat_id,
max_token=settings_max['token'], action_type='ai_response',
message="ℹ️ QR-код уже был отправлен." user_query=user_query,
intent=response.get('intent'),
entities=response.get('entities'),
response=response.get('message'),
details={
'status': response.get('status'),
'contact_name': contact.name
}
) )
send_menu(chat_id, token, client)
return True return True
# Если запись создана впервые – генерируем QR-код # 6. Если AI недоступен — просто меню
base_url = f"{BASE_URL}/promo/status/" send_menu(chat_id, token, client)
url_promo = f"{base_url}{promo}/{chat_id}/{client_id}" return True
img = qrcode.make(url_promo)
file_name = f"{promo}-{chat_id}-{client_id}.png"
img.save(f"{STATICFILES_DIRS[0]}/client_qr/{file_name}")
qr_url = f"{BASE_URL}/static/client_qr/{file_name}"
maxbot_send_img_message(
chat_id=chat_id,
max_token=settings_max['token'],
img=qr_url,
message='Ваш QR-код.'
)
# Отправляем дополнительный контент, если есть def _get_attachments(message: dict) -> dict:
for dt in data: """Извлекает информацию о вложениях (например, vCard)."""
if dt.img: attachments = message.get('message', {}).get('body', {}).get('attachments', [])
img_url = f"https://cdn.telefon-ip.ru/{dt.img}?thumb=600" for att in attachments:
maxbot_send_img_message(chat_id=chat_id, max_token=settings_max['token'], img=img_url, message=dt.descr) payload = att.get('payload', {})
if dt.title: if 'vcf_info' in payload:
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.descr) match = re.search(r'TEL;TYPE=cell:(\d+)', payload['vcf_info'])
if dt.url: if match:
maxbot_send_text_message(chat_id=chat_id, max_token=settings_max['token'], message=dt.url) return {'type': 'vcf_info', 'data': match.group(1)}
return {'type': None, 'data': ''}
return True def _extract_query(message: dict, message_type: str) -> str:
"""Извлекает текст запроса из сообщения или callback'а."""
if message_type == 'message_created':
return message.get('message', {}).get('body', {}).get('text', '')
elif message_type == 'message_callback':
payload = message.get('callback', {}).get('payload', '{}')
try:
data = json.loads(payload)
return data.get('data', '')
except json.JSONDecodeError:
return ''
return ''
def _get_command_data(client: Client, message: dict, message_type: str):
"""Получает данные команды (контент) по ключу."""
key = _extract_query(message, message_type)
if not key:
return None
return common_get_data(client=client, key=str(key).lower())

@ -1,6 +1,7 @@
from max_bot.max_api import * from max_bot.max_api import *
from max_bot.models import Client from max_bot.models import Client
from restoran_max_bot.common import common_get_data, common_get_contact, common_set_contact from restoran_max_bot.common import common_get_data, common_get_contact, common_set_contact
from restoran_max_bot.user_logger import log_user_action
def bot_started(client: Client, message: dict, settings: dict): def bot_started(client: Client, message: dict, settings: dict):
@ -8,6 +9,17 @@ def bot_started(client: Client, message: dict, settings: dict):
name = message['user']['name'] name = message['user']['name']
user_id = message['user']['user_id'] user_id = message['user']['user_id']
data = common_get_data(client=client, key='start') data = common_get_data(client=client, key='start')
log_user_action(
client=client,
chat_id=chat_id,
action_type='bot_started',
details={
'user_name': name,
'user_id': user_id
}
)
if data: if data:
for dt in data: for dt in data:
if dt.img: if dt.img:
@ -69,6 +81,17 @@ def check_registration(client: Client, chat_id: str, message: dict, settings: di
contact = common_set_contact(client=client, uid_client=chat_id, month=dt['data']) contact = common_set_contact(client=client, uid_client=chat_id, month=dt['data'])
maxbot_send_content(client=client, chat_id=chat_id, max_token=settings['token'], maxbot_send_content(client=client, chat_id=chat_id, max_token=settings['token'],
message='system_registration') message='system_registration')
# Логируем успешную регистрацию
log_user_action(
client=client,
chat_id=chat_id,
action_type='registration_completed',
details={
'name': contact.name,
'phone': contact.phone,
'birthday': f"{contact.field1}.{contact.field2}"
}
)
return contact return contact
else: else:
dt = maxbot_send_content(client=client, chat_id=chat_id, max_token=settings['token'], dt = maxbot_send_content(client=client, chat_id=chat_id, max_token=settings['token'],

@ -0,0 +1,321 @@
# command_handlers.py
import json
import qrcode
from max_bot.models import Client, Contact
from restoran_max_bot.common import common_get_data, common_get_integration
from restoran_max_bot.iiko_api import customer_info, customer_create, customer_wallet
from restoran_max_bot.rkiper_api import rkiper_customer_info, rkiper_customer_create
from restoran_max_bot.message_sender import send_text, send_image, send_menu, send_feedback_buttons, send_booking_people_buttons, send_link_button
from restoran_max_bot.settings import STATICFILES_DIRS, BASE_URL
from restoran_max_bot.utils import is_json
from restoran_max_bot.bot_started import check_registration
from restoran_max_bot.promo_handlers import bot_promo
from restoran_max_bot.user_logger import log_user_action
def handle_standard_command(client: Client, chat_id: str, settings_max: dict, data, contact: Contact, command_key: str = None) -> bool:
"""Обрабатывает стандартные команды (меню, бонусы, отзывы, бронирование, промо).
Возвращает True, если команда была обработана, иначе False."""
if not data:
return False
token = settings_max['token']
for item in data:
# Промокод (команда ##promo##)
if item.title == '##promo##' and command_key:
fake_message = {'chat_id': chat_id, 'payload': command_key}
result = bot_promo(client, fake_message, settings_max)
log_user_action(
client=client,
chat_id=chat_id,
action_type='standard_command',
command='promo',
details={'promo_key': command_key}
)
# После обработки промо-кода отправляем меню
send_menu(chat_id, token, client)
return result
# Бонусы
if item.title == '##bonus##':
_handle_bonus(client, chat_id, token, contact)
log_user_action(
client=client,
chat_id=chat_id,
action_type='standard_command',
command='bonus',
details={'contact_name': contact.name}
)
continue
# Отзыв
if item.title == '##feedback##':
send_feedback_buttons(chat_id, token, client)
log_user_action(
client=client,
chat_id=chat_id,
action_type='standard_command',
command='feedback',
details={'contact_name': contact.name}
)
return True
# Бронирование
if item.title == '##booking##':
send_booking_people_buttons(chat_id, token, client)
log_user_action(
client=client,
chat_id=chat_id,
action_type='standard_command',
command='booking',
details={'contact_name': contact.name}
)
return True
# Кнопка-ссылка
if is_json(item.title):
data_json = json.loads(item.title)
if data_json.get('type') == 'linkbutton':
send_link_button(chat_id, token, item.descr, data_json['button'], item.url)
log_user_action(
client=client,
chat_id=chat_id,
action_type='standard_command',
command='linkbutton',
details={'button_text': data_json['button']}
)
continue
# Изображение
if item.img:
url = f"https://cdn.telefon-ip.ru/{item.img}?thumb=600"
send_image(chat_id, token, url, item.descr)
# Текст (если не специальный тег и нет картинки)
if '##' not in item.title and not item.img and not is_json(item.title):
send_text(chat_id, token, item.descr)
# URL
if item.url and not is_json(item.title):
send_text(chat_id, token, item.url)
send_menu(chat_id, token, client)
return True
def _handle_bonus(client: Client, chat_id: str, token: str, contact: Contact) -> None:
"""Обработка команды бонусов: генерация QR и запрос баланса."""
# Генерируем QR-код
img = qrcode.make(contact.phone)
img.save(f"{STATICFILES_DIRS[0]}/client_qr/{contact.phone}.png")
qr_url = f"https://maxbot.telefon-ip.ru/static/client_qr/{contact.phone}.png"
send_image(chat_id, token, qr_url, 'QR-код для начисления, списания бонусов.')
# Получаем баланс
bonus = _get_bonus(client, contact.phone, 0.0, contact)
send_text(chat_id, token, f"💰 Ваш бонусный баланс: {bonus} руб")
def _get_bonus(client: Client, phone: str, default_bonus: float, contact: Contact) -> float:
"""Получает бонусный баланс из интеграции (IIKO или R-Keeper)."""
integration = common_get_integration(client=client)
if not integration:
return 0.0
date = contact.field1 or '01'
month = contact.field2 or '01'
if len(date) < 2:
date = '0' + date
if len(month) < 2:
month = '0' + month
config = json.loads(integration.setting)
if integration.partner.slug == 'iiko':
customerinfo = customer_info(config['token'], phone, config['id_org'])
if not customerinfo:
birthday = f"1990-{month}-{date} 00:00:00.000"
customer_create(api_key=config['token'], phone=phone, name=contact.name,
organization_id=config['id_org'], birthday=birthday)
customerinfo = customer_info(config['token'], phone, config['id_org'])
if customerinfo:
customer_wallet(api_key=config['token'],
iiko_wallet_id=customerinfo['walletBalances'][0]['id'],
customer_id=customerinfo['id'],
organization_id=config['id_org'],
bonus=default_bonus)
return default_bonus
else:
return customerinfo['walletBalances'][0]['balance']
elif integration.partner.slug == 'rkeeper':
rs = rkiper_customer_info(config['url'], config['token'], phone)
if rs:
rs = rs[0]
if 'card_use' in rs:
return int(rs['card_use']['sum1']) / 100
else:
birthday = f"1990-{month}-{date}"
rkiper_customer_create(config['url'], config['token'], phone, contact.name,
birthday, config['discount'], config['bonus'])
return 0.0
def handle_ai_response(client: Client, chat_id: str, token: str, response: dict, contact: Contact) -> None:
"""Обрабатывает ответ от AI-агента и выполняет соответствующие действия."""
intent = response.get('intent')
message_text = response.get('message', '')
entities = response.get('entities', {})
status = response.get('status', 'complete')
if intent == 'general':
send_text(chat_id, token, message_text)
elif intent == 'booking':
_handle_booking_intent(chat_id, token, client, response, contact)
elif intent == 'contact_admin':
_handle_contact_admin_intent(chat_id, token, client, response, contact)
elif intent == 'feedback':
_handle_feedback_intent(chat_id, token, client, response, contact)
else:
send_text(chat_id, token, message_text)
def _handle_booking_intent(chat_id: str, token: str, client: Client, response: dict, contact: Contact) -> None:
"""Обрабатывает намерение 'booking'."""
message_text = response.get('message', '')
status = response.get('status', 'complete')
entities = response.get('entities', {})
if status == 'need_more_info':
send_text(chat_id, token, message_text)
return
if status == 'complete':
date = entities.get('date')
time = entities.get('time')
people = entities.get('people')
phone = entities.get('phone')
if date and time and people:
success = _send_booking_to_crm(client, date, time, people, phone, contact.name)
if success:
send_text(chat_id, token, f"✅ Бронирование на {date} в {time} на {people} чел. принято! Администратор свяжется с вами.")
log_user_action(
client=client,
chat_id=chat_id,
action_type='booking_completed',
details={
'date': date,
'time': time,
'people': people,
'phone': phone,
'contact_name': contact.name
}
)
else:
send_text(chat_id, token, "❌ Не удалось забронировать. Попробуйте позже.")
else:
send_text(chat_id, token, "⚠️ Не хватает данных для бронирования.")
else:
send_text(chat_id, token, message_text)
def _handle_contact_admin_intent(chat_id: str, token: str, client: Client, response: dict, contact: Contact) -> None:
"""Обрабатывает намерение 'contact_admin'."""
message_text = response.get('message', '')
status = response.get('status', 'complete')
entities = response.get('entities', {})
if status == 'need_more_info':
send_text(chat_id, token, message_text)
return
if status == 'complete':
phone = entities.get('phone')
contact_method = entities.get('contact_method')
question = entities.get('question', '')
preferred_time = entities.get('preferred_time', '')
if phone and contact_method:
success = _send_contact_to_admin(client, phone, contact_method, question, preferred_time, contact.name)
if success:
send_text(chat_id, token, f"✅ Ваш запрос передан администратору. Способ связи: {contact_method}. Скоро с вами свяжутся.")
log_user_action(
client=client,
chat_id=chat_id,
action_type='contact_admin_completed',
details={
'phone': phone,
'contact_method': contact_method,
'question': question,
'preferred_time': preferred_time,
'contact_name': contact.name
}
)
else:
send_text(chat_id, token, "❌ Не удалось отправить запрос. Попробуйте позже.")
else:
send_text(chat_id, token, "⚠️ Не хватает данных для связи с администратором.")
else:
send_text(chat_id, token, message_text)
def _handle_feedback_intent(chat_id: str, token: str, client: Client, response: dict, contact: Contact) -> None:
"""Обрабатывает намерение 'feedback'."""
message_text = response.get('message', '')
status = response.get('status', 'complete')
entities = response.get('entities', {})
if status == 'need_more_info':
send_text(chat_id, token, message_text)
return
if status == 'complete':
rating = entities.get('rating')
feedback_text = entities.get('feedback_text', '')
if rating and feedback_text:
success = _send_feedback_to_crm(client, rating, feedback_text, contact.name, contact.phone)
if success:
send_text(chat_id, token, "✅ Спасибо за ваш отзыв! Мы учтём его.")
log_user_action(
client=client,
chat_id=chat_id,
action_type='feedback_completed',
details={
'rating': rating,
'feedback_text': feedback_text,
'contact_name': contact.name
}
)
else:
send_text(chat_id, token, "❌ Не удалось отправить отзыв. Попробуйте позже.")
else:
send_text(chat_id, token, "⚠️ Не хватает данных для отзыва.")
else:
send_text(chat_id, token, message_text)
def _send_booking_to_crm(client: Client, date, time, people, phone, name) -> bool:
"""Отправляет бронирование в CRM/менеджерам."""
from restoran_max_bot.common import send_message_all_manager
msg = f'Бронирование столика:\nНа дату: {date}-{time} \nКоличество: {people}\nКонтакт: {phone}\nИмя: {name}'
return send_message_all_manager(client, msg)
def _send_contact_to_admin(client: Client, phone, contact_method, question, preferred_time, name) -> bool:
"""Отправляет запрос на связь с администратором."""
from restoran_max_bot.common import send_message_all_manager
msg = (f'Гость попросил связаться:\n {contact_method}\nВопрос: {question}\n'
f'Удобное время: {preferred_time}\nКонтакт: {phone}\nИмя: {name}')
return send_message_all_manager(client, msg)
def _send_feedback_to_crm(client: Client, rating, feedback_text, name, phone) -> bool:
"""Отправляет отзыв в CRM/менеджерам."""
from restoran_max_bot.common import send_message_all_manager
msg = f'Отзыв от {name} (тел. {phone}):\nОценка: {rating}/5\nТекст: {feedback_text}'
return send_message_all_manager(client, msg)

@ -0,0 +1,34 @@
# message_sender.py
from max_bot.max_api import maxbot_send_text_message, maxbot_send_img_message, maxbot_send_menu_button, maxbot_send_feedback_button, maxbot_send_booking_count_people_button, maxbot_send_link_button
from max_bot.models import Client
def send_text(chat_id: str, token: str, text: str) -> None:
"""Отправить текстовое сообщение."""
if text:
maxbot_send_text_message(chat_id=chat_id, max_token=token, message=text)
def send_image(chat_id: str, token: str, image_url: str, caption: str = '') -> None:
"""Отправить сообщение с изображением."""
maxbot_send_img_message(chat_id=chat_id, max_token=token, img=image_url, message=caption)
def send_menu(chat_id: str, token: str, client: Client, text: str = '👇 Выберите раздел') -> None:
"""Отправить меню с кнопками."""
maxbot_send_menu_button(chat_id=chat_id, max_token=token, message=text, client=client)
def send_feedback_buttons(chat_id: str, token: str, client: Client, text: str = '👇') -> None:
"""Отправить кнопки для оценки."""
maxbot_send_feedback_button(client=client, message=text, chat_id=chat_id, max_token=token)
def send_booking_people_buttons(chat_id: str, token: str, client: Client, text: str = '👇') -> None:
"""Отправить кнопки выбора количества гостей."""
maxbot_send_booking_count_people_button(client=client, message=text, chat_id=chat_id, max_token=token)
def send_link_button(chat_id: str, token: str, text: str, button_title: str, url: str) -> None:
"""Отправить сообщение с кнопкой-ссылкой."""
maxbot_send_link_button(text=text, title_button=button_title, link=url, chat_id=chat_id, max_token=token)

@ -1,10 +1,16 @@
import os.path import os.path
import sys
from pathlib import Path from pathlib import Path
from environ import environ from environ import environ
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Создаём папку для логов, если её нет
LOG_DIR = os.path.join(BASE_DIR, 'log')
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
env = environ.Env() env = environ.Env()
env_file = f'{BASE_DIR}/.env' env_file = f'{BASE_DIR}/.env'
env.read_env(env_file=env_file) env.read_env(env_file=env_file)
@ -26,7 +32,6 @@ if DEBUG:
else: else:
BASE_URL = "https://maxbot.telefon-ip.ru" BASE_URL = "https://maxbot.telefon-ip.ru"
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
@ -76,6 +81,19 @@ WSGI_APPLICATION = 'restoran_max_bot.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases # https://docs.djangoproject.com/en/5.2/ref/settings/#databases
# Если запускаются тесты, используем SQLite in-memory для изоляции
# и отключаем сериализацию данных, чтобы не пытаться копировать данные из существующей БД
if 'test' in sys.argv:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
'TEST': {
'SERIALIZE': False, # не копировать данные из основной БД
}
}
}
else:
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
@ -133,3 +151,47 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOG_SYSTEM_PROMPT = False # False - не логировать системный промт LOG_SYSTEM_PROMPT = False # False - не логировать системный промт
PROMO_ADMIN_PIN = os.environ.get('PROMO_ADMIN_PIN', '1111') PROMO_ADMIN_PIN = os.environ.get('PROMO_ADMIN_PIN', '1111')
# ---------- Настройка логирования ошибок ----------
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(BASE_DIR, 'log', 'error.log'),
'when': 'midnight',
'interval': 1,
'backupCount': 30,
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': True,
},
'django.request': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': False,
},
'django.server': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': False,
},
},
}

@ -0,0 +1,372 @@
# restoran_max_bot/tests.py
import json
import unittest
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from max_bot.models import Client, Contact, Product, ProductCategory, PromoCode
from restoran_max_bot.bot_message import bot_message, _get_attachments, _extract_query, _get_command_data
from restoran_max_bot.promo_handlers import bot_promo
from restoran_max_bot.message_sender import send_text, send_image, send_menu
from restoran_max_bot.command_handlers import handle_standard_command, handle_ai_response
from restoran_max_bot.ai_handler import process_with_ai
class BotMessageTestCase(TestCase):
"""Тесты для функции bot_message и её вспомогательных функций."""
def setUp(self):
# Создаём реального клиента в БД
self.client = Client.objects.create(
id=1,
token='test_token',
status=True,
title='Test Client',
code='001',
brand='TestBrand',
logo='',
server='test',
managment='test'
)
# Настройки Max
self.settings_max = {'token': 'max_token'}
# Создаём контакт (можно использовать Mock, но лучше реальный объект)
self.contact = Contact.objects.create(
client=self.client,
name='Test User',
phone='79001234567',
field1='01',
field2='01',
maxx='chat123',
status=1,
up=0,
group=1
)
# Базовое сообщение
self.message_created = {
'message': {
'recipient': {'chat_id': 'chat123'},
'sender': {'name': 'Test User'},
'body': {'text': 'menu'}
},
'update_type': 'message_created'
}
self.message_callback = {
'message': {
'recipient': {'chat_id': 'chat123'},
'sender': {'name': 'Test User'},
'body': {}
},
'callback': {
'payload': json.dumps({'data': 'bonus'})
},
'update_type': 'message_callback'
}
# ---------- Тесты вспомогательных функций ----------
def test_get_attachments_vcf(self):
"""Проверка извлечения номера из vCard."""
message = {
'message': {
'body': {
'attachments': [
{'payload': {'vcf_info': 'TEL;TYPE=cell:79161234567'}}
]
}
}
}
result = _get_attachments(message)
self.assertEqual(result['type'], 'vcf_info')
self.assertEqual(result['data'], '79161234567')
def test_get_attachments_no_vcf(self):
"""Нет вложений или нет vcf."""
message = {'message': {'body': {}}}
result = _get_attachments(message)
self.assertIsNone(result['type'])
self.assertEqual(result['data'], '')
def test_extract_query_message_created(self):
"""Извлечение текста из обычного сообщения."""
query = _extract_query(self.message_created, 'message_created')
self.assertEqual(query, 'menu')
def test_extract_query_message_callback(self):
"""Извлечение данных из callback."""
query = _extract_query(self.message_callback, 'message_callback')
self.assertEqual(query, 'bonus')
def test_extract_query_empty(self):
"""Пустой запрос."""
message = {'message': {'body': {}}}
query = _extract_query(message, 'message_created')
self.assertEqual(query, '')
@patch('restoran_max_bot.bot_message.common_get_data')
def test_get_command_data(self, mock_get_data):
"""Получение данных команды."""
mock_get_data.return_value = [Mock(title='menu')]
result = _get_command_data(self.client, self.message_created, 'message_created')
self.assertIsNotNone(result)
mock_get_data.assert_called_with(client=self.client, key='menu')
# ---------- Тесты основной функции bot_message ----------
@patch('restoran_max_bot.bot_message.check_registration')
@patch('restoran_max_bot.bot_message.handle_standard_command')
@patch('restoran_max_bot.bot_message.process_with_ai')
@patch('restoran_max_bot.bot_message.handle_ai_response')
@patch('restoran_max_bot.bot_message.send_menu')
@patch('restoran_max_bot.bot_message.common_set_contact')
@patch('restoran_max_bot.bot_message.common_get_data')
def test_bot_message_vcf_registration(self, mock_get_data, mock_set_contact, mock_send_menu,
mock_handle_ai, mock_process_ai,
mock_handle_std, mock_check_reg):
"""Обработка vCard -> регистрация -> вызов check_registration."""
mock_get_data.return_value = None # чтобы не искало команду
# Сообщение с vcf
message = {
'message': {
'recipient': {'chat_id': 'chat123'},
'sender': {'name': 'Vasya'},
'body': {
'attachments': [
{'payload': {'vcf_info': 'TEL;TYPE=cell:79161234567'}}
]
}
},
'update_type': 'message_created'
}
mock_check_reg.return_value = self.contact
mock_handle_std.return_value = False
mock_process_ai.return_value = None
result = bot_message(self.client, message, self.settings_max, 'message_created')
self.assertTrue(result)
mock_set_contact.assert_called_once()
mock_check_reg.assert_called_once()
mock_send_menu.assert_called_once()
@patch('restoran_max_bot.bot_message.check_registration')
@patch('restoran_max_bot.bot_message.handle_standard_command')
@patch('restoran_max_bot.bot_message.send_menu')
@patch('restoran_max_bot.bot_message.common_get_data')
def test_bot_message_standard_command(self, mock_get_data, mock_send_menu, mock_handle_std, mock_check_reg):
"""Обработка стандартной команды (меню)."""
mock_get_data.return_value = [Mock(title='menu')] # имитируем наличие данных
mock_check_reg.return_value = self.contact
mock_handle_std.return_value = True # команда обработана
result = bot_message(self.client, self.message_created, self.settings_max, 'message_created')
self.assertTrue(result)
mock_handle_std.assert_called_once()
mock_send_menu.assert_not_called()
@patch('restoran_max_bot.bot_message.check_registration')
@patch('restoran_max_bot.bot_message.handle_standard_command')
@patch('restoran_max_bot.bot_message.process_with_ai')
@patch('restoran_max_bot.bot_message.handle_ai_response')
@patch('restoran_max_bot.bot_message.send_menu')
@patch('restoran_max_bot.bot_message.common_get_data')
def test_bot_message_ai_handling(self, mock_get_data, mock_send_menu, mock_handle_ai, mock_process_ai,
mock_handle_std, mock_check_reg):
"""Обработка через AI, когда нет стандартной команды."""
mock_get_data.return_value = None # нет стандартной команды
mock_check_reg.return_value = self.contact
mock_handle_std.return_value = False
mock_process_ai.return_value = {'intent': 'general', 'message': 'Hello'}
result = bot_message(self.client, self.message_created, self.settings_max, 'message_created')
self.assertTrue(result)
mock_process_ai.assert_called_once()
mock_handle_ai.assert_called_once()
mock_send_menu.assert_called_once()
@patch('restoran_max_bot.bot_message.check_registration')
def test_bot_message_not_registered(self, mock_check_reg):
"""Пользователь не зарегистрирован -> выход."""
mock_check_reg.return_value = None
result = bot_message(self.client, self.message_created, self.settings_max, 'message_created')
self.assertFalse(result)
# ---------- Тесты bot_promo ----------
def test_bot_promo_new(self):
"""Создание нового промокода."""
with patch('restoran_max_bot.promo_handlers.common_get_data') as mock_get_data, \
patch('restoran_max_bot.promo_handlers.PromoCode.objects.get_or_create') as mock_get_or_create, \
patch('restoran_max_bot.promo_handlers.qrcode.make') as mock_qr, \
patch('restoran_max_bot.promo_handlers.send_image') as mock_send_image, \
patch('restoran_max_bot.promo_handlers.send_text') as mock_send_text:
mock_get_data.return_value = [Mock(img='', title='', url='')]
mock_get_or_create.return_value = (Mock(used=False), True) # created=True
mock_qr.return_value = Mock()
message = {'chat_id': 'chat123', 'payload': 'promo123'}
result = bot_promo(self.client, message, self.settings_max)
self.assertTrue(result)
mock_send_image.assert_called_once()
mock_send_text.assert_not_called()
@patch('restoran_max_bot.promo_handlers.common_get_data')
def test_bot_promo_no_data(self, mock_get_data):
"""Промокод не найден в системе."""
mock_get_data.return_value = None
message = {'chat_id': 'chat123', 'payload': 'unknown'}
result = bot_promo(self.client, message, self.settings_max)
self.assertTrue(result)
# Ничего не отправляем
class CommandHandlersTestCase(TestCase):
"""Тесты для command_handlers.py"""
def setUp(self):
self.client = Client.objects.create(
id=2,
token='test2',
status=True,
title='Test Client 2',
code='002',
brand='TestBrand2',
logo='',
server='test',
managment='test'
)
self.contact = Contact.objects.create(
client=self.client,
name='Test',
phone='79001234567',
maxx='chat123',
status=1,
up=0,
group=1
)
self.chat_id = 'chat123'
self.token = 'token'
self.settings_max = {'token': self.token}
@patch('restoran_max_bot.command_handlers.send_menu')
@patch('restoran_max_bot.command_handlers.send_text')
def test_handle_standard_command_bonus(self, mock_send_text, mock_send_menu):
from restoran_max_bot.command_handlers import handle_standard_command
mock_item = Mock()
mock_item.title = '##bonus##'
mock_item.img = ''
mock_item.descr = ''
mock_item.url = ''
with patch('restoran_max_bot.command_handlers._handle_bonus') as mock_handle_bonus:
result = handle_standard_command(self.client, self.chat_id, self.settings_max,
[mock_item], self.contact)
self.assertTrue(result)
mock_handle_bonus.assert_called_once()
mock_send_menu.assert_called_once()
@patch('restoran_max_bot.command_handlers.send_feedback_buttons')
@patch('restoran_max_bot.command_handlers.send_menu')
def test_handle_standard_command_feedback(self, mock_send_menu, mock_send_feedback):
from restoran_max_bot.command_handlers import handle_standard_command
mock_item = Mock()
mock_item.title = '##feedback##'
result = handle_standard_command(self.client, self.chat_id, self.settings_max,
[mock_item], self.contact)
self.assertTrue(result)
mock_send_feedback.assert_called_once()
mock_send_menu.assert_not_called() # feedback возвращает True сразу
@patch('restoran_max_bot.command_handlers.send_booking_people_buttons')
@patch('restoran_max_bot.command_handlers.send_menu')
def test_handle_standard_command_booking(self, mock_send_menu, mock_send_booking):
from restoran_max_bot.command_handlers import handle_standard_command
mock_item = Mock()
mock_item.title = '##booking##'
result = handle_standard_command(self.client, self.chat_id, self.settings_max,
[mock_item], self.contact)
self.assertTrue(result)
mock_send_booking.assert_called_once()
mock_send_menu.assert_not_called()
@patch('restoran_max_bot.command_handlers.send_link_button')
@patch('restoran_max_bot.command_handlers.send_menu')
def test_handle_standard_command_linkbutton(self, mock_send_menu, mock_send_link):
from restoran_max_bot.command_handlers import handle_standard_command
mock_item = Mock()
mock_item.title = json.dumps({'type': 'linkbutton', 'button': 'Click'})
mock_item.descr = 'Text'
mock_item.url = 'http://example.com'
result = handle_standard_command(self.client, self.chat_id, self.settings_max,
[mock_item], self.contact)
self.assertTrue(result)
mock_send_link.assert_called_once()
mock_send_menu.assert_called_once()
class AiHandlerTestCase(TestCase):
"""Тесты для ai_handler.py"""
def setUp(self):
self.client = Client.objects.create(
id=3,
token='test3',
status=True,
title='Test Client 3',
code='003',
brand='TestBrand3',
logo='',
server='test',
managment='test'
)
@patch('restoran_max_bot.ai_handler.get_ai_agent')
@patch('restoran_max_bot.ai_handler.call_ai_agent')
def test_process_with_ai_available(self, mock_call, mock_get):
from restoran_max_bot.ai_handler import process_with_ai
mock_get.return_value = {'ai_agent': 'deepseek'}
mock_call.return_value = {'intent': 'general'}
contact = Mock()
contact.name = 'Test'
result = process_with_ai(self.client, 'chat123', 'hello', contact)
self.assertEqual(result, {'intent': 'general'})
mock_call.assert_called_once()
@patch('restoran_max_bot.ai_handler.get_ai_agent')
def test_process_with_ai_not_available(self, mock_get):
from restoran_max_bot.ai_handler import process_with_ai
mock_get.return_value = {'ai_agent': None}
result = process_with_ai(self.client, 'chat123', 'hello', None)
self.assertIsNone(result)
class MessageSenderTestCase(TestCase):
"""Тесты для message_sender.py (проверка вызовов API)."""
def setUp(self):
self.client = Client.objects.create(
id=4,
token='test4',
status=True,
title='Test Client 4',
code='004',
brand='TestBrand4',
logo='',
server='test',
managment='test'
)
@patch('restoran_max_bot.message_sender.maxbot_send_text_message')
def test_send_text(self, mock_send):
from restoran_max_bot.message_sender import send_text
send_text('chat', 'token', 'text')
mock_send.assert_called_once_with(chat_id='chat', max_token='token', message='text')
@patch('restoran_max_bot.message_sender.maxbot_send_img_message')
def test_send_image(self, mock_send):
from restoran_max_bot.message_sender import send_image
send_image('chat', 'token', 'http://img', 'caption')
mock_send.assert_called_once_with(chat_id='chat', max_token='token', img='http://img', message='caption')
@patch('restoran_max_bot.message_sender.maxbot_send_menu_button')
def test_send_menu(self, mock_send):
from restoran_max_bot.message_sender import send_menu
send_menu('chat', 'token', self.client, 'text')
mock_send.assert_called_once_with(chat_id='chat', max_token='token', message='text', client=self.client)
Loading…
Cancel
Save