diff --git a/.env b/.env
index 9e8058a..e6c673b 100644
--- a/.env
+++ b/.env
@@ -1,2 +1,4 @@
-TELEGRAM_BOT_TOKEN=7387413773:AAFgKfuf-REN5cn1ZXmCQgHHPaVJhY8RLZw
-ADMIN_CHAT_ID=872379852
+TELEGRAM_BOT_TOKEN=7387413773:AAFgKfuf-REN5cn1ZXmCQg
+OWNER_CHAT_ID=872379852
+ADMIN_CHAT_ID=2019512207
+MANAGER_CONTACT=https://t.me/Thisissaymoon
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..a365bde
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+# Скопируйте этот файл в .env и заполните реальными данными.
+TELEGRAM_BOT_TOKEN=PASTE_TELEGRAM_BOT_TOKEN_HERE
+OWNER_CHAT_ID=0
+ADMIN_CHAT_ID=0
+MANAGER_CONTACT=https://t.me/your_manager_username
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..79d5cb8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+__pycache__/
+*.py[cod]
+*.log
+venv/
+data/
+logs/
+clients/
+*.sqlite3
+orders.json
+orders.xlsx
+user_logs.json
+prices.json
+*.pyc
diff --git a/README.md b/README.md
index 52e7b02..6e1643b 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,99 @@
-# gipsrbot
\ No newline at end of file
+# Gipsr Bot
+
+Телеграм-бот для оформления учебных работ. Пользователи оформляют заявки, прикрепляют методички любого формата и получают персональные предложения, а администратор отслеживает всё происходящее в одной панели.
+
+**Что внутри:**
+
+- интерактивный заказ с выбором типа, срочности (в том числе «🔥 24 часа») и объёма работы;
+- автоматическое напоминание прикрепить задания и методические рекомендации, поддержка файлов любого типа;
+- автоскидка 7% на первый заказ (до 3000 ₽), учёт бонусов и реферальных вознаграждений по 300 ₽;
+- обновлённый прайс с нормоконтролем, презентацией+речью и личным куратором;
+- «кайфовый» профиль клиента: статус, бонусный баланс, история заказов и реферальная ссылка;
+- расширенная админ-панель: статистика с рефералами, журнал действий, смена статусов с уведомлением клиента и экспорт заказов.
+
+## Как развернуть без доступа к консоли
+
+Все, что вам нужно — загрузить папку проекта на сервер через FileZilla, вставить готовый скрипт в панель cloud-init и перезагрузить машину.
+
+### 1. `.env` уже заполнен
+
+Файл `.env` уже содержит готовый токен бота, ID владельца (`872379852`), администратора для уведомлений (`2019512207`) и ссылку менеджера `https://t.me/Thisissaymoon`. Вам ничего не нужно изменять — просто загрузите файл на сервер вместе с остальными.
+
+> ⚠️ Не выкладывайте `.env` в открытый доступ: в нём лежит реальный токен бота.
+
+### 2. Какие файлы загружать на сервер
+
+Перетащите в `/root/gipsr_bot` (как на вашем скриншоте) весь каталог проекта со следующими файлами:
+
+| Файл/папка | Зачем нужен |
+|------------|-------------|
+| `bot.py` | основной код бота |
+| `config.py` | дефолтные настройки, статусы и тексты |
+| `.env` | ваши токены и контакты |
+| `requirements.txt` | список Python-зависимостей |
+| `scripts/` | вспомогательные скрипты установки и автозапуска |
+| `deploy/supervisor.conf` | конфигурация supervisor (запасной вариант автозапуска) |
+| `cloud/cloud-init.yaml` | готовый cloud-init скрипт для панели управления |
+| `README.md` (опционально) | инструкция под рукой |
+
+Сохраняйте структуру каталогов (`scripts`, `cloud`, `deploy` и т. д.).
+
+### 3. Загрузите проект через FileZilla
+
+1. Подключитесь к серверу в FileZilla.
+2. В правой части окна перейдите в `/root/gipsr_bot`.
+3. Перетащите все файлы из локальной папки проекта в окно FileZilla. Если папки уже есть на сервере, разрешите замену, чтобы обновить код.
+
+### 4. Настройте cloud-init
+
+1. В панели хостинга откройте редактор cloud-init.
+2. Скопируйте содержимое файла [`cloud/cloud-init.yaml`](cloud/cloud-init.yaml) и вставьте в поле скрипта.
+3. Убедитесь, что путь `/root/gipsr_bot` совпадает с путём, куда вы загрузили файлы (если нет — отредактируйте скрипт).
+4. Сохраните изменения.
+
+### 5. Перезагрузите сервер
+
+После перезагрузки cloud-init автоматически:
+
+- установит `python3`, `pip`, `venv` и `supervisor` (если их ещё нет);
+- запустит `scripts/autodeploy.sh`, который создаст виртуальное окружение, поставит зависимости и настроит `systemd`-службу `gipsrbot.service`;
+- стартует бота и включит автозапуск при следующей перезагрузке.
+
+Логи установки сохраняются в `/root/gipsr_bot/logs/autodeploy.log`.
+
+## Как убедиться, что бот запустился
+
+1. Скачайте файл `/root/gipsr_bot/logs/autodeploy.log` через FileZilla. В конце должна быть строка `Готово! Бот запущен...`. Если увидите сообщение про `.env`, обновите токен и перезагрузите сервер.
+2. Скачайте `/root/gipsr_bot/logs/bot.log`. При успешном запуске в нём будут строки вида `Application started`.
+3. Откройте бота в Telegram, отправьте `/start` и пройдите меню. Владелец с ID `872379852` сразу увидит админ-панель, Анна будет получать уведомления о заказах и ошибках в личные сообщения.
+
+## Управление без консоли
+
+- `/admin` — открыть админ-панель (статистика, последние заказы, экспорт, смена режимов цен и т. д.) — команда доступна только владельцу Семёну (`ID 872379852`).
+- Внутри админки доступны разделы «Бонусы и рефералы», журнал действий пользователей, экспорт заказов и ручное изменение статусов с уведомлением клиента.
+- `/restart_bot` или кнопка «🔄 Перезапустить бота» в админ-панели — безопасно перезапускает службу через systemd. Используйте после загрузки обновлённого кода.
+- Все ошибки дублируются владельцу (Семёну) и администратору (Анне) и записываются в `logs/bot.log`.
+
+## Как обновить код или настройки
+
+1. Измените файлы локально (например, обновите прайс или тексты).
+2. Загрузите изменённые файлы в `/root/gipsr_bot`, разрешив замену.
+3. В Telegram (из админского аккаунта) отправьте команду `/restart_bot`, чтобы служба перечитала код. Если бот не отвечает, просто перезагрузите сервер — service стартует автоматически.
+
+## Структура проекта
+
+- `bot.py` — логика бота, сценарии заказов, калькулятор, админ-панель.
+- `config.py` — константы и дефолтные тексты.
+- `.env` — приватные настройки, которые читает `python-dotenv`.
+- `scripts/` — утилиты: установка зависимостей, запуск и автодеплой через systemd.
+- `deploy/` — пример конфигурации supervisor.
+- `cloud/cloud-init.yaml` — готовый cloud-init скрипт для вашего хостинга.
+- `data/`, `logs/`, `clients/` — создаются автоматически при первом запуске и содержат JSON-файлы с заказами, логами и выгрузками.
+
+## Частые ошибки и диагностика
+
+- **Бот не запускается, в логах ошибка про токен.** Проверьте `.env`: токен должен быть без кавычек, без пробелов, в формате `123456:ABC-DEF`.
+- **В FileZilla нет папки `logs/`.** Она появляется после первого запуска. Если её нет, скачайте `logs/autodeploy.log` — возможно, установка не завершилась (чаще всего из-за пустого `.env`).
+- **Нужно сменить владельца или адресатов уведомлений.** В `data/settings.json` обнулите `owner_chat_id`, чтобы следующий пользователь, отправивший `/start`, стал новым владельцем, или обновите `admin_chat_id`/`notification_chat_ids`, если нужно перенаправить уведомления.
+
+Разработчики могут дополнительно проверить синтаксис локально командой `python -m compileall bot.py` — это необязательно для продакшена, но полезно перед обновлением.
diff --git a/bot.py b/bot.py
index c561518..3a62ce8 100644
--- a/bot.py
+++ b/bot.py
@@ -1,746 +1,1962 @@
-import os
-import sys
-import logging
-import json
-from datetime import datetime, timedelta
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import (
- ApplicationBuilder, CommandHandler, ContextTypes,
- CallbackQueryHandler, MessageHandler, filters, ConversationHandler
-)
-from telegram.constants import ParseMode
-from telegram.error import TelegramError
-from dotenv import load_dotenv
-import pandas as pd
-
-# Загрузка переменных окружения
-load_dotenv()
-TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
-ADMIN_CHAT_ID = int(os.getenv('ADMIN_CHAT_ID', 0))
-
-# Директории
-BASE_DIR = os.path.join(os.getcwd(), 'clients')
-DATA_DIR = os.path.join(os.getcwd(), 'data')
-LOGS_DIR = os.path.join(os.getcwd(), 'logs')
-for directory in [BASE_DIR, DATA_DIR, LOGS_DIR]:
- os.makedirs(directory, exist_ok=True)
-
-# Настройка логирования
-logging.basicConfig(
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- level=logging.INFO
-)
-logger = logging.getLogger(__name__)
-file_handler = logging.FileHandler(os.path.join(LOGS_DIR, 'bot.log'))
-file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
-logger.addHandler(file_handler)
-
-# Файлы данных
-PRICES_FILE = os.path.join(DATA_DIR, 'prices.json')
-REFERRALS_FILE = os.path.join(DATA_DIR, 'referrals.json')
-ORDERS_FILE = os.path.join(DATA_DIR, 'orders.json')
-FEEDBACKS_FILE = os.path.join(DATA_DIR, 'feedbacks.json')
-USER_LOGS_FILE = os.path.join(DATA_DIR, 'user_logs.json')
-
-# Функции загрузки/сохранения с обработкой ошибок
-def load_json(file_path, default=None):
- try:
- if os.path.exists(file_path):
- with open(file_path, 'r', encoding='utf-8') as f:
- return json.load(f)
- return default or {}
- except Exception as e:
- logger.error(f"Ошибка загрузки {file_path}: {e}")
- return default or {}
-
-def save_json(file_path, data):
- try:
- with open(file_path, 'w', encoding='utf-8') as f:
- json.dump(data, f, ensure_ascii=False, indent=4)
- except Exception as e:
- logger.error(f"Ошибка сохранения {file_path}: {e}")
-
-# Глобальные данные
-PRICES = load_json(PRICES_FILE, {
- 'samostoyatelnye': {'base': 2000, 'min': 2000, 'max': 5000},
- 'kursovaya_teoreticheskaya': {'base': 8000, 'min': 8000, 'max': 12000},
- 'kursovaya_s_empirikov': {'base': 12000, 'min': 12000, 'max': 18000},
- 'diplomnaya': {'base': 35000, 'min': 35000, 'max': 50000},
- 'magisterskaya': {'base': 35000, 'min': 35000, 'max': 60000}
-})
-REFERALS = load_json(REFERRALS_FILE)
-ORDERS = load_json(ORDERS_FILE)
-FEEDBACKS = load_json(FEEDBACKS_FILE)
-USER_LOGS = load_json(USER_LOGS_FILE)
-
-ORDER_TYPES = {
- 'samostoyatelnye': {
- 'name': 'Самостоятельные, контрольные, эссе',
- 'icon': '📝',
- 'description': 'Быстрые задания: эссе, контрольные, рефераты. Идеально для студентов! Уже 5000+ выполнено 🔥',
- 'details': 'Объем до 20 страниц. Быстрое выполнение с гарантией качества.',
- 'examples': ['Эссе по литературе', 'Контрольная по математике', 'Реферат по истории']
- },
- 'kursovaya_teoreticheskaya': {
- 'name': 'Курсовая теоретическая',
- 'icon': '📘',
- 'description': 'Глубокий анализ литературы. Получите отличную оценку без стресса! 📈',
- 'details': 'Теоретическая основа, анализ источников, структура по ГОСТ.',
- 'examples': ['Теория маркетинга', 'Обзор психологии развития']
- },
- 'kursovaya_s_empirikov': {
- 'name': 'Курсовая с эмпирикой',
- 'icon': '📊',
- 'description': 'Теория + данные, анализ. Клиенты говорят: "Лучшая помощь!" ⭐',
- 'details': 'Включает опросы, статистику, рекомендации.',
- 'examples': ['Исследование рынка', 'Анализ поведения потребителей']
- },
- 'diplomnaya': {
- 'name': 'Дипломная работа',
- 'icon': '🎓',
- 'description': 'Полный цикл для успешной защиты. Скидка 10% на первый диплом! 💼',
- 'details': 'Глубокий анализ, эмпирика, презентация.',
- 'examples': ['Социальная адаптация', 'Экономический анализ компании']
- },
- 'magisterskaya': {
- 'name': 'Магистерская диссертация',
- 'icon': '🔍',
- 'description': 'Инновационное исследование. 100% оригинальность гарантирована! 🌟',
- 'details': 'Научная новизна, методология, публикации.',
- 'examples': ['Разработка моделей AI', 'Комплексные исследования экологии']
- }
-}
-
-FAQ_ITEMS = [
- {'question': 'Как сделать заказ?', 'answer': 'Выберите "Сделать заказ" и следуйте шагам. Можно заказать несколько работ сразу!'},
- {'question': 'Как рассчитывается стоимость?', 'answer': 'Зависит от типа, срочности и сложности. Используйте калькулятор для точной цены!'},
- {'question': 'Как работает реферальная программа?', 'answer': 'Поделитесь ссылкой — получите 5% от заказов друзей как бонус.'},
- {'question': 'Гарантии качества?', 'answer': 'Антиплагиат, правки бесплатно 14 дней, поддержка до защиты.'},
- {'question': 'Скидки?', 'answer': '5-15% для постоянных, 10% на первый, рефералы.'},
- {'question': 'Отслеживание заказа?', 'answer': 'В профиле статусы, уведомления от менеджера.'}
-]
-
-current_pricing_mode = 'light'
-
-# Состояния
-SELECT_MAIN_MENU, SELECT_ORDER_TYPE, VIEW_ORDER_DETAILS, INPUT_TOPIC, SELECT_DEADLINE, INPUT_REQUIREMENTS, ADD_UPSSELL, ADD_ANOTHER_ORDER, CONFIRM_CART, ADMIN_MENU, PROFILE_MENU, SHOW_PRICE_LIST, PRICE_CALCULATOR, SELECT_CALC_DEADLINE, SELECT_CALC_COMPLEXITY, SHOW_FAQ, FAQ_DETAILS, SHOW_ORDERS, LEAVE_FEEDBACK, INPUT_FEEDBACK = range(20)
-
-# Логирование действий пользователя
-def log_user_action(user_id, username, action):
- timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- USER_LOGS.setdefault(str(user_id), []).append({'timestamp': timestamp, 'action': action, 'username': username})
- save_json(USER_LOGS_FILE, USER_LOGS)
- logger.info(f"Пользователь {user_id} ({username}): {action}")
-
-# Расчет цены
-def calculate_price(order_type_key, days_left, complexity_factor=1.0):
- if order_type_key not in PRICES:
- logger.error(f"Неизвестный тип: {order_type_key}")
- return 0
- base = PRICES[order_type_key]['base']
- price = int(base * complexity_factor)
- if current_pricing_mode == 'hard':
- if days_left < 7:
- price *= 1.3
- elif days_left < 15:
- price *= 1.15
- else:
- if days_left < 3:
- price *= 1.3
- elif days_left < 7:
- price *= 1.15
- return int(price)
-
-# Обработчик ошибок
-async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- logger.error(f"Ошибка: {context.error}")
- if ADMIN_CHAT_ID:
- await context.bot.send_message(ADMIN_CHAT_ID, f"Ошибка: {context.error}")
-
-# Команда /start
-async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
- user = update.effective_user
- log_user_action(user.id, user.username, "/start")
- args = update.message.text.split()
- bot_username = (await context.bot.get_me()).username
- ref_link = f"https://t.me/{bot_username}?start={user.id}"
- context.user_data['ref_link'] = ref_link
- if len(args) > 1 and args[1].isdigit():
- referrer_id = int(args[1])
- if referrer_id != user.id:
- REFERALS.setdefault(str(referrer_id), []).append(user.id)
- save_json(REFERRALS_FILE, REFERALS)
- await context.bot.send_message(referrer_id, f"🎉 Новый реферал: {user.first_name}")
- welcome = f"👋 Добро пожаловать, {user.first_name}! Заказывайте работы. Уже 5000+ клиентов! 10% скидка на первый заказ 🔥\nПоделитесь ссылкой для бонусов: {ref_link}"
- await main_menu(update, context, welcome)
-
-# Главное меню
-async def main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE, message=None):
- user = update.effective_user
- log_user_action(user.id, user.username, "Главное меню")
- text = message or "Выберите раздел:"
- keyboard = [
- [InlineKeyboardButton("📝 Сделать заказ", callback_data='make_order')],
- [InlineKeyboardButton("💲 Прайс-лист", callback_data='price_list'), InlineKeyboardButton("🧮 Калькулятор", callback_data='price_calculator')],
- [InlineKeyboardButton("👤 Профиль", callback_data='profile'), InlineKeyboardButton("❓ FAQ", callback_data='faq')],
- [InlineKeyboardButton("📞 Администратор", url='https://t.me/Thisissaymoon')]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
- if update.callback_query:
- query = update.callback_query
- await query.answer()
- try:
- await query.edit_message_text(text, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN)
- except TelegramError as e:
- if "message is not modified" in str(e).lower():
- pass
- else:
- logger.error(f"Ошибка редактирования сообщения: {e}")
- else:
- await update.message.reply_text(text, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN)
- return SELECT_MAIN_MENU
-
-# Обработчик главного меню
-async def main_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- user = update.effective_user
- log_user_action(user.id, user.username, f"Выбор в меню: {data}")
- if data == 'make_order':
- return await select_order_type(update, context)
- elif data == 'price_list':
- return await show_price_list(update, context)
- elif data == 'price_calculator':
- return await price_calculator(update, context)
- elif data == 'profile':
- return await show_profile(update, context)
- elif data == 'faq':
- return await show_faq(update, context)
- await query.edit_message_text("Неизвестная команда. Возвращаюсь в главное меню.")
- return await main_menu(update, context)
- return SELECT_MAIN_MENU
-
-# Выбор типа заказа
-async def select_order_type(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data if query else None
- user = update.effective_user
- log_user_action(user.id, user.username, "Выбор типа заказа")
- if data == 'back_to_main':
- return await main_menu(update, context)
- text = "Выберите тип работы (добавьте несколько в корзину для скидки!):"
- keyboard = [[InlineKeyboardButton(f"{val['icon']} {val['name']}", callback_data=f'type_{key}')] for key, val in ORDER_TYPES.items()]
- keyboard.append([InlineKeyboardButton("⬅️ Меню", callback_data='back_to_main')])
- reply_markup = InlineKeyboardMarkup(keyboard)
- try:
- await query.edit_message_text(text, reply_markup=reply_markup)
- except TelegramError as e:
- if "message is not modified" in str(e).lower():
- pass
- else:
- raise
- return SELECT_ORDER_TYPE
-
-# Подробности о типе заказа
-async def view_order_details(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data.startswith('order_'):
- key = data[6:]
- context.user_data['current_order_type'] = key
- await query.edit_message_text("Введите тему:")
- return INPUT_TOPIC
- elif data == 'select_order_type':
- return await select_order_type(update, context)
- elif data.startswith('type_'):
- key = data[5:]
- if key not in ORDER_TYPES:
- await query.edit_message_text("Ошибка: неизвестный тип.")
- return SELECT_ORDER_TYPE
- val = ORDER_TYPES[key]
- text = f"{val['icon']} *{val['name']}*\n\n{val['description']}\n{val['details']}\nПримеры: {', '.join(val['examples'])}\n\nЗаказать? (Добавьте презентацию/речь для полного пакета!)"
- keyboard = [
- [InlineKeyboardButton("✅ Заказать", callback_data=f'order_{key}')],
- [InlineKeyboardButton("Назад", callback_data='select_order_type')]
- ]
- await query.edit_message_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return VIEW_ORDER_DETAILS
-
-# Ввод темы
-async def input_topic(update: Update, context: ContextTypes.DEFAULT_TYPE):
- context.user_data['topic'] = update.message.text
- user = update.effective_user
- log_user_action(user.id, user.username, f"Тема: {update.message.text}")
- text = "Выберите срок сдачи (дольше = дешевле + бонус!):"
- today = datetime.now()
- keyboard = []
- for i in range(1, 31, 5):
- row = []
- for j in range(i, min(i+5, 31)):
- date = today + timedelta(days=j)
- button_text = f"{date.day} {date.strftime('%b')} ({j} дней)"
- row.append(InlineKeyboardButton(button_text, callback_data=f'deadline_{j}'))
- keyboard.append(row)
- keyboard.append([InlineKeyboardButton("Назад", callback_data=f'type_{context.user_data["current_order_type"]}')])
- await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return SELECT_DEADLINE
-
-# Выбор срока
-async def select_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data.startswith('deadline_'):
- days = int(data[9:])
- context.user_data['days_left'] = days
- await query.edit_message_text("Введите дополнительные требования (или /skip):")
- return INPUT_REQUIREMENTS
- elif data.startswith('type_'):
- return await view_order_details(update, context)
- return SELECT_DEADLINE
-
-# Ввод требований
-async def input_requirements(update: Update, context: ContextTypes.DEFAULT_TYPE):
- context.user_data['requirements'] = update.message.text
- return await add_upsell(update, context)
-
-async def skip_requirements(update: Update, context: ContextTypes.DEFAULT_TYPE):
- context.user_data['requirements'] = 'Нет'
- return await add_upsell(update, context)
-
-# Добавление допуслуг
-async def add_upsell(update: Update, context: ContextTypes.DEFAULT_TYPE):
- text = "Добавить услуги? (Клиенты, добавляющие, получают 5% скидки на следующий заказ!)"
- keyboard = [
- [InlineKeyboardButton("Презентация (+2000₽)", callback_data='add_prez')],
- [InlineKeyboardButton("Речь (+1000₽)", callback_data='add_speech')],
- [InlineKeyboardButton("Без допов", callback_data='no_upsell')]
- ]
- if update.message:
- await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- else:
- query = update.callback_query
- await query.answer()
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return ADD_UPSSELL
-
-# Обработчик допуслуг
-async def upsell_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- upsells = context.user_data.setdefault('upsells', set())
- added = False
- if data == 'add_prez':
- if 'prez' not in upsells:
- upsells.add('prez')
- added = True
- elif data == 'add_speech':
- if 'speech' not in upsells:
- upsells.add('speech')
- added = True
- elif data == 'no_upsell':
- return await process_order(update, context)
- text = "Добавить еще? (Полный пакет экономит время!)" if added else "Уже добавлено. Добавить еще?"
- keyboard = [
- [InlineKeyboardButton("Презентация (+2000₽)", callback_data='add_prez')],
- [InlineKeyboardButton("Речь (+1000₽)", callback_data='add_speech')],
- [InlineKeyboardButton("Продолжить", callback_data='no_upsell')]
- ]
- try:
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- except TelegramError as e:
- if "message is not modified" in str(e).lower():
- pass
- return ADD_UPSSELL
-
-async def process_order(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- type_key = context.user_data.get('current_order_type')
- if not type_key or type_key not in ORDER_TYPES:
- await query.edit_message_text("Ошибка: неверный тип заказа.")
- return ConversationHandler.END
- topic = context.user_data.get('topic', 'Без темы')
- days_left = context.user_data.get('days_left', 14)
- requirements = context.user_data.get('requirements', 'Нет')
- upsells = list(context.user_data.get('upsells', set()))
- price = calculate_price(type_key, days_left)
- extra = len(upsells) * 1000
- price += extra
- order = {
- 'type': type_key,
- 'topic': topic,
- 'deadline_days': days_left,
- 'requirements': requirements,
- 'upsells': upsells,
- 'price': price,
- 'status': 'новый'
- }
- context.user_data.setdefault('cart', []).append(order)
- context.user_data.pop('upsells', None)
- context.user_data.pop('requirements', None)
- context.user_data.pop('days_left', None)
- context.user_data.pop('topic', None)
- context.user_data.pop('current_order_type', None)
- return await add_another_order(update, context)
-
-# Добавить еще заказ
-async def add_another_order(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- text = "Добавить еще заказ? (Несколько заказов = 10% скидка!)"
- keyboard = [
- [InlineKeyboardButton("Да", callback_data='add_another_yes')],
- [InlineKeyboardButton("Нет, оформить", callback_data='confirm_cart')]
- ]
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return ADD_ANOTHER_ORDER
-
-async def add_another_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data == 'add_another_yes':
- return await select_order_type(update, context)
- elif data == 'confirm_cart':
- return await confirm_cart(update, context)
- return ADD_ANOTHER_ORDER
-
-# Подтверждение корзины
-async def confirm_cart(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- cart = context.user_data.get('cart', [])
- if not cart:
- await query.edit_message_text("Корзина пуста.")
- return await main_menu(update, context)
- text = "Ваша корзина (подтвердите для специального бонуса!):\n"
- total = 0
- for i, order in enumerate(cart, 1):
- order_name = ORDER_TYPES.get(order['type'], {}).get('name', 'Неизвестно')
- text += f"{i}. {order_name} - {order['topic']} - {order['price']} ₽\n"
- total += order['price']
- if len(cart) > 1:
- discount = total * 0.1
- total -= discount
- text += f"Скидка за несколько заказов: -{discount} ₽\n"
- text += f"Итого: {total} ₽\nПодтвердить?"
- keyboard = [
- [InlineKeyboardButton("Подтвердить", callback_data='place_order')],
- [InlineKeyboardButton("Отменить", callback_data='cancel_cart')]
- ]
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return CONFIRM_CART
-
-async def confirm_cart_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data == 'place_order':
- user_id = str(update.effective_user.id)
- user_orders = ORDERS.setdefault(user_id, [])
- order_id = len(user_orders) + 1
- for order in context.user_data['cart']:
- order['order_id'] = order_id
- user_orders.append(order)
- order_id += 1
- save_json(ORDERS_FILE, ORDERS)
- text = "Заказ оформлен! Менеджер свяжется с вами. Проверьте профиль для статуса."
- await query.edit_message_text(text)
- if ADMIN_CHAT_ID:
- await context.bot.send_message(ADMIN_CHAT_ID, f"Новый заказ от {user_id}: {len(context.user_data['cart'])} позиций")
- context.user_data.pop('cart', None)
- return await main_menu(update, context, "Спасибо! Хотите заказать еще?")
- elif data == 'cancel_cart':
- context.user_data.pop('cart', None)
- return await main_menu(update, context, "Корзина отменена. Посмотрите еще?")
- return CONFIRM_CART
-
-# Показ прайс-листа
-async def show_price_list(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data.startswith('price_detail_'):
- key = data[13:]
- val = ORDER_TYPES.get(key, {})
- if not val:
- await query.edit_message_text("Ошибка: неизвестный тип.")
- return SHOW_PRICE_LIST
- prices = PRICES.get(key, {'min': 0, 'max': 0})
- text = f"{val.get('icon', '')} *{val.get('name', '')}*\n\n{val.get('description', '')}\n{val.get('details', '')}\nПримеры: {', '.join(val.get('examples', []))}\nЦена: {prices['min']}-{prices['max']} ₽\n\nЗакажите со скидкой!"
- keyboard = [
- [InlineKeyboardButton("Рассчитать", callback_data='price_calculator')],
- [InlineKeyboardButton("Заказать", callback_data=f'type_{key}')],
- [InlineKeyboardButton("Назад", callback_data='price_list')]
- ]
- await query.edit_message_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return SHOW_PRICE_LIST
- elif data == 'price_calculator':
- return await price_calculator(update, context)
- elif data == 'back_to_main':
- return await main_menu(update, context)
- user = update.effective_user
- log_user_action(user.id, user.username, "Прайс-лист")
- text = "💲 Прайс-лист (10% скидка сегодня! 🔥):\n\n"
- for key, val in ORDER_TYPES.items():
- prices = PRICES.get(key, {'base': 0})
- text += f"{val['icon']} *{val['name']}* — от {prices['base']} ₽\n"
- keyboard = [[InlineKeyboardButton(f"Подробности {val['name']}", callback_data=f'price_detail_{key}')] for key, val in ORDER_TYPES.items()]
- keyboard.append([InlineKeyboardButton("🧮 Рассчитать цену", callback_data='price_calculator')])
- keyboard.append([InlineKeyboardButton("⬅️ Меню", callback_data='back_to_main')])
- await query.edit_message_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return SHOW_PRICE_LIST
-
-# Калькулятор цен
-async def price_calculator(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data.startswith('calc_type_'):
- key = data[10:]
- context.user_data['calc_type'] = key
- text = f"Тип: {ORDER_TYPES.get(key, {}).get('name', 'Неизвестно')}\nВыберите срок (дольше = дешевле):"
- keyboard = [
- [InlineKeyboardButton("3 дня (+30%)", callback_data='calc_dead_3')],
- [InlineKeyboardButton("7 дней (+15%)", callback_data='calc_dead_7'), InlineKeyboardButton("14 дней (базовая)", callback_data='calc_dead_14')],
- [InlineKeyboardButton("30 дней (скидка!)", callback_data='calc_dead_30')],
- [InlineKeyboardButton("Назад", callback_data='price_calculator')]
- ]
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return SELECT_CALC_DEADLINE
- elif data == 'back_to_main':
- return await main_menu(update, context)
- user = update.effective_user
- log_user_action(user.id, user.username, "Калькулятор")
- text = "🧮 Выберите тип:"
- keyboard = [[InlineKeyboardButton(f"{v['icon']} {v['name']}", callback_data=f'calc_type_{k}')] for k, v in ORDER_TYPES.items()]
- keyboard.append([InlineKeyboardButton("⬅️ Меню", callback_data='back_to_main')])
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return PRICE_CALCULATOR
-
-async def calc_select_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data.startswith('calc_dead_'):
- days = int(data[10:])
- context.user_data['calc_days'] = days
- text = f"Срок: {days} дней\nВыберите сложность:"
- keyboard = [
- [InlineKeyboardButton("Простая (базовая)", callback_data='calc_comp_1.0')],
- [InlineKeyboardButton("Средняя (+10%)", callback_data='calc_comp_1.1'), InlineKeyboardButton("Сложная (+30%)", callback_data='calc_comp_1.3')],
- [InlineKeyboardButton("Назад", callback_data=f'calc_type_{context.user_data["calc_type"]}')]
- ]
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return SELECT_CALC_COMPLEXITY
- return SELECT_CALC_DEADLINE
-
-async def calc_select_complexity(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data.startswith('calc_comp_'):
- comp = float(data[10:])
- key = context.user_data.get('calc_type')
- days = context.user_data.get('calc_days', 14)
- price = calculate_price(key, days, comp)
- name = ORDER_TYPES.get(key, {}).get('name', 'Неизвестно')
- text = f"Расчет: {name}\nСрок: {days} дней\nСложность: {int((comp-1)*100)}%\nЦена: {price} ₽ (Скидка сегодня!)\n\nЗаказать?"
- keyboard = [
- [InlineKeyboardButton("📝 Заказать", callback_data=f'type_{key}')],
- [InlineKeyboardButton("Пересчитать", callback_data='price_calculator'), InlineKeyboardButton("Меню", callback_data='back_to_main')]
- ]
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return PRICE_CALCULATOR
- return SELECT_CALC_COMPLEXITY
-
-# Показ профиля
-async def show_profile(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- user = update.effective_user
- user_id = str(user.id)
- if data == 'my_orders':
- return await show_orders(update, context)
- elif data == 'leave_feedback':
- await query.edit_message_text("Введите ваш отзыв:")
- return INPUT_FEEDBACK
- elif data == 'back_to_main':
- return await main_menu(update, context)
- log_user_action(user.id, user.username, "Профиль")
- orders_count = len(ORDERS.get(user_id, []))
- feedbacks_count = len(FEEDBACKS.get(user_id, []))
- refs_count = len(REFERALS.get(user_id, []))
- ref_link = context.user_data.get('ref_link', 'Нет ссылки')
- text = f"👤 Профиль {user.first_name}\n\nЗаказов: {orders_count}\nОтзывов: {feedbacks_count}\nРефералов: {refs_count}\nРеф. ссылка: {ref_link}\n\nПриглашайте друзей за бонусы!"
- keyboard = [
- [InlineKeyboardButton("📋 Мои заказы", callback_data='my_orders')],
- [InlineKeyboardButton("⭐ Оставить отзыв", callback_data='leave_feedback')],
- [InlineKeyboardButton("⬅️ Меню", callback_data='back_to_main')]
- ]
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return PROFILE_MENU
-
-# Показ заказов
-async def show_orders(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data == 'profile':
- return await show_profile(update, context)
- user_id = str(update.effective_user.id)
- user_orders = ORDERS.get(user_id, [])
- if not user_orders:
- text = "Пока нет заказов. Сделайте заказ сейчас!"
- else:
- text = "Ваши заказы:\n"
- for order in user_orders:
- name = ORDER_TYPES.get(order.get('type'), {}).get('name', 'Неизвестно')
- text += f"#{order.get('order_id', 'N/A')}: {name} - {order.get('status', 'новый')}\n"
- keyboard = [[InlineKeyboardButton("Назад", callback_data='profile')]]
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return SHOW_ORDERS
-
-# Ввод отзыва
-async def input_feedback(update: Update, context: ContextTypes.DEFAULT_TYPE):
- user_id = str(update.effective_user.id)
- feedback = update.message.text
- FEEDBACKS.setdefault(user_id, []).append(feedback)
- save_json(FEEDBACKS_FILE, FEEDBACKS)
- await update.message.reply_text("Спасибо за отзыв! Добавлены бонусные баллы.")
- return await show_profile(update, context)
-
-# Показ FAQ
-async def show_faq(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data.startswith('faq_'):
- idx = int(data[4:])
- item = FAQ_ITEMS[idx]
- text = f"❓ {item['question']}\n\n{item['answer']}"
- keyboard = [[InlineKeyboardButton("Назад к FAQ", callback_data='faq')],
- [InlineKeyboardButton("Меню", callback_data='back_to_main')]]
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return FAQ_DETAILS
- elif data == 'back_to_main':
- return await main_menu(update, context)
- user = update.effective_user
- log_user_action(user.id, user.username, "FAQ")
- text = "❓ FAQ: Выберите вопрос"
- keyboard = [[InlineKeyboardButton(item['question'], callback_data=f'faq_{i}')] for i, item in enumerate(FAQ_ITEMS)]
- keyboard.append([InlineKeyboardButton("⬅️ Меню", callback_data='back_to_main')])
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return SHOW_FAQ
-
-# Показ админ меню
-async def show_admin_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
- keyboard = [
- [InlineKeyboardButton("📋 Заказы", callback_data='admin_orders')],
- [InlineKeyboardButton("👥 Пользователи", callback_data='admin_users'), InlineKeyboardButton("📊 Логи", callback_data='admin_logs')],
- [InlineKeyboardButton("💲 Цены", callback_data='admin_prices')],
- [InlineKeyboardButton("📤 Экспорт", callback_data='admin_export')],
- [InlineKeyboardButton("⬅️ Выход", callback_data='back_to_main')]
- ]
- text = "🔐 Админ-панель"
- if update.callback_query:
- query = update.callback_query
- await query.answer()
- await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- else:
- await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return ADMIN_MENU
-
-# Админ старт
-async def admin_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
- if update.effective_user.id != ADMIN_CHAT_ID:
- await update.message.reply_text("Доступ запрещен!")
- return
- user = update.effective_user
- log_user_action(user.id, user.username, "Админ-панель")
- return await show_admin_menu(update, context)
-
-# Обработчик админ-меню
-async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = query.data
- if data == 'admin_menu':
- return await show_admin_menu(update, context)
- text = ""
- keyboard = [[InlineKeyboardButton("Назад", callback_data='admin_menu')]]
- if data == 'admin_orders':
- text = "📋 Заказы:\n"
- for uid, ords in ORDERS.items():
- for ord in ords:
- text += f"#{ord['order_id']} от {uid}: {ord['status']}\n"
- elif data == 'admin_users':
- text = "👥 Пользователи:\n" + "\n".join(f"ID: {uid}" for uid in ORDERS.keys())
- elif data == 'admin_logs':
- text = "📊 Логи (последние 10):\n"
- for uid, logs in list(USER_LOGS.items())[-10:]:
- if logs:
- text += f"Пользователь {uid}: {logs[-1]['action']}\n"
- elif data == 'admin_prices':
- text = f"Текущий режим: {current_pricing_mode}\nВведите новый режим (hard/light):"
- context.user_data['admin_state'] = 'change_mode'
- elif data == 'admin_export':
- df = pd.DataFrame([{'user_id': uid, **ord} for uid, ords in ORDERS.items() for ord in ords])
- export_file = os.path.join(DATA_DIR, 'orders_export.csv')
- df.to_csv(export_file, index=False)
- await context.bot.send_document(ADMIN_CHAT_ID, open(export_file, 'rb'))
- os.remove(export_file)
- text = "📤 Экспорт отправлен!"
- elif data == 'back_to_main':
- return await main_menu(update, context)
- await query.edit_message_text(text or "Неизвестная команда. Возвращаюсь в админ-меню.", reply_markup=InlineKeyboardMarkup(keyboard))
- return ADMIN_MENU
-
-# Обработчик сообщений админа
-async def admin_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
- state = context.user_data.get('admin_state')
- if state == 'change_mode':
- global current_pricing_mode
- current_pricing_mode = update.message.text.lower()
- await update.message.reply_text("Режим изменен!")
- context.user_data.pop('admin_state')
- return await show_admin_menu(update, context)
- return ADMIN_MENU
-
-# Основная функция
-def main():
- application = ApplicationBuilder().token(TELEGRAM_BOT_TOKEN).build()
- conv_handler = ConversationHandler(
- entry_points=[CommandHandler('start', start), CommandHandler('admin', admin_start)],
- states={
- SELECT_MAIN_MENU: [CallbackQueryHandler(main_menu_handler)],
- SELECT_ORDER_TYPE: [CallbackQueryHandler(select_order_type)],
- VIEW_ORDER_DETAILS: [CallbackQueryHandler(view_order_details)],
- INPUT_TOPIC: [MessageHandler(filters.TEXT & ~filters.COMMAND, input_topic)],
- SELECT_DEADLINE: [CallbackQueryHandler(select_deadline)],
- INPUT_REQUIREMENTS: [MessageHandler(filters.TEXT & ~filters.COMMAND, input_requirements), CommandHandler('skip', skip_requirements)],
- ADD_UPSSELL: [CallbackQueryHandler(upsell_handler)],
- ADD_ANOTHER_ORDER: [CallbackQueryHandler(add_another_handler)],
- CONFIRM_CART: [CallbackQueryHandler(confirm_cart_handler)],
- ADMIN_MENU: [CallbackQueryHandler(admin_menu_handler), MessageHandler(filters.TEXT & ~filters.COMMAND, admin_message)],
- PROFILE_MENU: [CallbackQueryHandler(show_profile)],
- SHOW_PRICE_LIST: [CallbackQueryHandler(show_price_list)],
- PRICE_CALCULATOR: [CallbackQueryHandler(price_calculator)],
- SELECT_CALC_DEADLINE: [CallbackQueryHandler(calc_select_deadline)],
- SELECT_CALC_COMPLEXITY: [CallbackQueryHandler(calc_select_complexity)],
- SHOW_FAQ: [CallbackQueryHandler(show_faq)],
- FAQ_DETAILS: [CallbackQueryHandler(show_faq)],
- SHOW_ORDERS: [CallbackQueryHandler(show_orders)],
- INPUT_FEEDBACK: [MessageHandler(filters.TEXT & ~filters.COMMAND, input_feedback)],
- },
- fallbacks=[CommandHandler('start', start)],
- )
- application.add_handler(conv_handler)
- application.add_error_handler(error_handler)
- application.run_polling(drop_pending_updates=True)
-
-if __name__ == '__main__':
- main()
\ No newline at end of file
+from __future__ import annotations
+
+import asyncio
+import html
+import json
+import logging
+import os
+from dataclasses import dataclass, asdict, field
+from datetime import datetime, timedelta
+from pathlib import Path
+from typing import Dict, Iterable, List, Optional
+
+import pandas as pd
+from dotenv import load_dotenv
+from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
+from telegram.constants import ParseMode
+from telegram.error import BadRequest
+from telegram.ext import (
+ ApplicationBuilder,
+ CallbackQueryHandler,
+ CommandHandler,
+ ContextTypes,
+ ConversationHandler,
+ MessageHandler,
+ filters,
+)
+
+from config import (
+ BOT_TOKEN,
+ DEFAULT_ADMIN_CHAT_ID as CONFIG_ADMIN_CHAT_ID,
+ DEFAULT_ADMIN_USERNAME as CONFIG_ADMIN_USERNAME,
+ DEFAULT_PRICING_MODE,
+ MANAGER_CONTACT_URL as CONFIG_MANAGER_CONTACT_URL,
+ ORDER_STATUS_TITLES,
+ OWNER_CHAT_ID as CONFIG_OWNER_CHAT_ID,
+ OWNER_USERNAME as CONFIG_OWNER_USERNAME,
+ SECONDARY_ADMINS,
+ WELCOME_MESSAGE,
+)
+
+load_dotenv()
+
+TELEGRAM_BOT_TOKEN = (os.getenv("TELEGRAM_BOT_TOKEN") or BOT_TOKEN or "").strip()
+
+owner_id_env = os.getenv("OWNER_CHAT_ID")
+admin_id_env = os.getenv("ADMIN_CHAT_ID")
+
+try:
+ OWNER_CHAT_ID_DEFAULT = (
+ int(owner_id_env) if owner_id_env is not None else int(CONFIG_OWNER_CHAT_ID or 0)
+ )
+except ValueError:
+ OWNER_CHAT_ID_DEFAULT = int(CONFIG_OWNER_CHAT_ID or 0)
+
+try:
+ ADMIN_CHAT_ID_DEFAULT = (
+ int(admin_id_env) if admin_id_env is not None else int(CONFIG_ADMIN_CHAT_ID or 0)
+ )
+except ValueError:
+ ADMIN_CHAT_ID_DEFAULT = int(CONFIG_ADMIN_CHAT_ID or 0)
+
+DEFAULT_OWNER_USERNAME = (
+ os.getenv("OWNER_USERNAME") or CONFIG_OWNER_USERNAME or ""
+).strip()
+ADMIN_USERNAME_DEFAULT = (
+ os.getenv("ADMIN_USERNAME") or CONFIG_ADMIN_USERNAME or ""
+).strip()
+
+MANAGER_CONTACT_LINK = (
+ os.getenv("MANAGER_CONTACT") or CONFIG_MANAGER_CONTACT_URL or ""
+).strip() or CONFIG_MANAGER_CONTACT_URL
+
+if not TELEGRAM_BOT_TOKEN:
+ raise RuntimeError(
+ "TELEGRAM_BOT_TOKEN is not configured. Проверьте .env или config.py."
+ )
+
+BASE_DIR = Path(__file__).resolve().parent
+DATA_DIR = BASE_DIR / "data"
+LOGS_DIR = BASE_DIR / "logs"
+CLIENTS_DIR = BASE_DIR / "clients"
+
+for directory in (DATA_DIR, LOGS_DIR, CLIENTS_DIR):
+ directory.mkdir(parents=True, exist_ok=True)
+
+logging.basicConfig(
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ level=logging.INFO,
+)
+logger = logging.getLogger(__name__)
+file_handler = logging.FileHandler(LOGS_DIR / "bot.log")
+file_handler.setFormatter(
+ logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+)
+logger.addHandler(file_handler)
+
+DEFAULT_PRICES: Dict[str, Dict[str, int]] = {
+ "self": {"base": 2500, "min": 2500, "max": 4200},
+ "course_theory": {"base": 10000, "min": 10000, "max": 16500},
+ "course_empirical": {"base": 16500, "min": 15000, "max": 24000},
+ "normcontrol": {"base": 5000, "min": 5000, "max": 7000},
+ "vkr": {"base": 32000, "min": 30000, "max": 48000},
+ "master": {"base": 40000, "min": 40000, "max": 65000},
+}
+
+UNIVERSITIES_EXAMPLES = "МГУ, СПбГУ, ВШЭ, РАНХиГС, УрФУ и другие ведущие вузы России"
+
+ORDER_TYPES: Dict[str, Dict[str, object]] = {
+ "self": {
+ "name": "Самостоятельная работа",
+ "icon": "📝",
+ "description": "Короткие задания, контрольные, эссе и отчёты. Выполняем точно по методичкам и быстро вовлекаем автора в задачу.",
+ "details": "Подходит для работ до 20 страниц, готовим за вечер и дожидаемся подтверждения от преподавателя.",
+ "examples": [
+ "Эссе по психологии",
+ "Контрольная по педагогике",
+ "Отчёт по социальной работе",
+ ],
+ },
+ "course_theory": {
+ "name": "Курсовая (теория)",
+ "icon": "📘",
+ "description": "Теоретическая курсовая с понятной логикой, актуальными источниками и акцентом на научную новизну.",
+ "details": "Оформляем главы, список литературы и приложения с учётом требований кафедры. Результаты подбираем под вашу задачу.",
+ "examples": [
+ "Психология развития личности",
+ "Современные методики логопедии",
+ "Социальная работа с семьёй",
+ ],
+ },
+ "course_empirical": {
+ "name": "Курсовая (теория + эмпирика)",
+ "icon": "📊",
+ "description": "Полный курс с аналитикой: теория, исследование, результаты и выводы. Можем придумать данные или использовать ваши.",
+ "details": "Разрабатываем инструментарий, проводим опросы/тесты, оформляем таблицы и графики. Всё готово к защите.",
+ "examples": [
+ "Диагностика конфликтов в организации",
+ "Анализ эффективности соцслужб",
+ "Исследование логопедических методик",
+ ],
+ },
+ "normcontrol": {
+ "name": "Нормоконтроль",
+ "icon": "📏",
+ "description": "Проверим оформление, ГОСТ, ссылки и плагины. Всё приведём к стандарту вуза.",
+ "details": "Шаблоны, оглавление, списки и приложения — всё выверено под требования преподавателя.",
+ "examples": [
+ "Проверка диплома перед сдачей",
+ "Форматирование курсовой",
+ "Адаптация статей под ГОСТ",
+ ],
+ },
+ "vkr": {
+ "name": "Дипломная работа (ВКР)",
+ "icon": "🎓",
+ "description": "Выпускная работа под ключ: план, теория, практическая часть и защита. Сопровождаем до финального отзыва.",
+ "details": "Готовим презентацию, речь и сопровождаем консультациями. Опыт — 6 лет, более 4000 успешных проектов в {UNIVERSITIES_EXAMPLES}.",
+ "examples": [
+ "Психологическое сопровождение персонала",
+ "Программа развития соцслужбы",
+ "Корпоративные конфликты и их решения",
+ ],
+ },
+ "master": {
+ "name": "Магистерская диссертация",
+ "icon": "🔍",
+ "description": "Исследование уровня магистратуры: глубокая аналитика, научная новизна и публикации.",
+ "details": "Согласовываем план, проводим исследования, готовим статьи, презентацию и речь. Сопровождаем до защиты.",
+ "examples": [
+ "Инновации в социальной сфере",
+ "Психологические технологии сопровождения",
+ "Программы профилактики конфликтов",
+ ],
+ },
+}
+
+FAQ_ITEMS: List[Dict[str, str]] = [
+ {
+ "question": "Как оформить заказ?",
+ "answer": "Нажмите '📝 Сделать заказ', последовательно выберите тип, срок, объём и требования. Можно прикреплять любые файлы прямо в чат.",
+ },
+ {
+ "question": "С какими сроками работаете?",
+ "answer": "Берём срочные заказы от 24 часов с повышающим коэффициентом. Чем раньше обратитесь — тем выгоднее.",
+ },
+ {
+ "question": "Какие скидки и бонусы?",
+ "answer": "На первый заказ действует скидка 7% (до 3000 ₽). За каждого приглашённого друга — +300 ₽ на бонусный счёт.",
+ },
+ {
+ "question": "Работаете ли с моим вузом?",
+ "answer": f"Да! За 6 лет мы делали работы для {UNIVERSITIES_EXAMPLES}. Подстроимся под ваши методички.",
+ },
+ {
+ "question": "Как передать методички и данные?",
+ "answer": "На этапе требований прикрепите файлы любого формата: Word, PDF, фото, аудио. Всё автоматически прилетает администратору.",
+ },
+ {
+ "question": "Что с антиплагиатом?",
+ "answer": "Из-за детектора дубликатов Антиплагиат.ру мы не делаем предварительных проверок, но бесплатно переписываем до нужного процента. Для крупных работ правки — 6 месяцев.",
+ },
+]
+
+UPSELL_OPTIONS: Dict[str, Dict[str, int]] = {
+ "presentation_pack": {"title": "Презентация + речь", "price": 3000},
+ "mentor": {"title": "Личный куратор с ежедневной связью", "price": 1500},
+}
+
+PAGE_OPTIONS: Dict[str, Dict[str, object]] = {
+ "20": {"label": "до 20 стр.", "multiplier": 1.0},
+ "35": {"label": "до 35 стр.", "multiplier": 1.2},
+ "50": {"label": "до 50 стр.", "multiplier": 1.35},
+ "70": {"label": "70+ стр.", "multiplier": 1.55},
+}
+
+DEADLINE_CHOICES: List[tuple[int, str]] = [
+ (1, "🔥 24 часа"),
+ (2, "⚡ 2 дня"),
+ (3, "⚡ 3 дня"),
+ (5, "5 дней"),
+ (7, "Неделя"),
+ (14, "2 недели"),
+ (21, "3 недели"),
+ (30, "Месяц"),
+]
+
+FIRST_ORDER_DISCOUNT_RATE = 0.07
+FIRST_ORDER_DISCOUNT_CAP = 3000
+REFERRAL_BONUS_AMOUNT = 300
+LOYALTY_REWARD_RATE = 0.02
+
+SETTINGS_FILE = DATA_DIR / "settings.json"
+PRICES_FILE = DATA_DIR / "prices.json"
+REFERRALS_FILE = DATA_DIR / "referrals.json"
+ORDERS_FILE = DATA_DIR / "orders.json"
+FEEDBACKS_FILE = DATA_DIR / "feedbacks.json"
+USER_LOGS_FILE = DATA_DIR / "user_logs.json"
+BONUSES_FILE = DATA_DIR / "bonuses.json"
+
+
+@dataclass
+class OrderRecord:
+ order_id: int
+ type_key: str
+ topic: str
+ deadline_days: int
+ deadline_date: str
+ page_plan: str
+ requirements: str
+ attachments: List[Dict[str, str]]
+ upsells: List[str]
+ status_key: str
+ base_price: int
+ discount: int
+ price: int
+ status: str
+ created_at: str
+
+
+class DataStore:
+ def __init__(self) -> None:
+ default_notification_ids: List[int] = []
+ if OWNER_CHAT_ID_DEFAULT:
+ default_notification_ids.append(int(OWNER_CHAT_ID_DEFAULT))
+ if ADMIN_CHAT_ID_DEFAULT and ADMIN_CHAT_ID_DEFAULT not in default_notification_ids:
+ default_notification_ids.append(int(ADMIN_CHAT_ID_DEFAULT))
+ for entry in SECONDARY_ADMINS or []:
+ try:
+ extra_id = int(entry.get("chat_id", 0))
+ except (TypeError, ValueError):
+ continue
+ if extra_id and extra_id not in default_notification_ids:
+ default_notification_ids.append(extra_id)
+ default_settings = {
+ "pricing_mode": DEFAULT_PRICING_MODE,
+ "owner_chat_id": OWNER_CHAT_ID_DEFAULT,
+ "owner_username": DEFAULT_OWNER_USERNAME,
+ "admin_chat_id": ADMIN_CHAT_ID_DEFAULT,
+ "admin_username": ADMIN_USERNAME_DEFAULT,
+ "manager_contact_url": MANAGER_CONTACT_LINK,
+ "notification_chat_ids": default_notification_ids,
+ }
+ loaded_settings = self._load_json(SETTINGS_FILE, default_settings)
+ changed = False
+ for key, value in default_settings.items():
+ if key not in loaded_settings:
+ loaded_settings[key] = value
+ changed = True
+ if (
+ int(default_settings.get("owner_chat_id", 0))
+ and int(loaded_settings.get("owner_chat_id", 0)) == 0
+ ):
+ loaded_settings["owner_chat_id"] = default_settings["owner_chat_id"]
+ changed = True
+ if (
+ default_settings.get("owner_username")
+ and not loaded_settings.get("owner_username")
+ ):
+ loaded_settings["owner_username"] = default_settings["owner_username"]
+ changed = True
+ if (
+ int(default_settings.get("admin_chat_id", 0))
+ and int(loaded_settings.get("admin_chat_id", 0)) == 0
+ ):
+ loaded_settings["admin_chat_id"] = default_settings["admin_chat_id"]
+ changed = True
+ if (
+ default_settings.get("admin_username")
+ and not loaded_settings.get("admin_username")
+ ):
+ loaded_settings["admin_username"] = default_settings["admin_username"]
+ changed = True
+ notifications = loaded_settings.get("notification_chat_ids")
+ if not isinstance(notifications, list):
+ notifications = []
+ changed = True
+ for value in default_notification_ids:
+ if value and value not in notifications:
+ notifications.append(value)
+ changed = True
+ loaded_settings["notification_chat_ids"] = notifications
+ if changed:
+ self._save_json(SETTINGS_FILE, loaded_settings)
+ self.settings: Dict[str, object] = loaded_settings
+ self.prices: Dict[str, Dict[str, int]] = self._load_json(PRICES_FILE, DEFAULT_PRICES)
+ self.referrals: Dict[str, List[int]] = self._load_json(REFERRALS_FILE, {})
+ self.orders: Dict[str, List[Dict[str, object]]] = self._load_json(ORDERS_FILE, {})
+ self.feedbacks: Dict[str, List[str]] = self._load_json(FEEDBACKS_FILE, {})
+ self.user_logs: Dict[str, List[Dict[str, str]]] = self._load_json(USER_LOGS_FILE, {})
+ self.bonuses: Dict[str, Dict[str, object]] = self._load_json(BONUSES_FILE, {})
+
+ @staticmethod
+ def _load_json(path: Path, default) -> object:
+ if path.exists():
+ try:
+ with path.open("r", encoding="utf-8") as fh:
+ return json.load(fh)
+ except json.JSONDecodeError as exc:
+ logger.warning("Failed to decode %s (%s). Restoring defaults.", path, exc)
+ DataStore._save_json(path, default)
+ return json.loads(json.dumps(default))
+
+ @staticmethod
+ def _save_json(path: Path, data) -> None:
+ with path.open("w", encoding="utf-8") as fh:
+ json.dump(data, fh, ensure_ascii=False, indent=2)
+
+ def get_pricing_mode(self) -> str:
+ return self.settings.get("pricing_mode", DEFAULT_PRICING_MODE)
+
+ def set_pricing_mode(self, mode: str) -> None:
+ self.settings["pricing_mode"] = mode
+ self._save_json(SETTINGS_FILE, self.settings)
+
+ def get_owner_chat_id(self) -> int:
+ try:
+ return int(self.settings.get("owner_chat_id", 0))
+ except (TypeError, ValueError):
+ return 0
+
+ def set_owner_chat_id(self, chat_id: int, username: Optional[str] = None) -> None:
+ try:
+ normalized = int(chat_id)
+ except (TypeError, ValueError):
+ normalized = 0
+ self.settings["owner_chat_id"] = normalized
+ if username is not None:
+ self.settings["owner_username"] = username or ""
+ notifications = self.settings.get("notification_chat_ids")
+ if not isinstance(notifications, list):
+ notifications = []
+ if normalized and normalized not in notifications:
+ notifications.append(normalized)
+ self.settings["notification_chat_ids"] = notifications
+ self._save_json(SETTINGS_FILE, self.settings)
+
+ def get_admin_chat_id(self) -> int:
+ try:
+ return int(self.settings.get("admin_chat_id", 0))
+ except (TypeError, ValueError):
+ return 0
+
+ def set_admin_chat_id(self, chat_id: int, username: Optional[str] = None) -> None:
+ try:
+ normalized = int(chat_id)
+ except (TypeError, ValueError):
+ normalized = 0
+ self.settings["admin_chat_id"] = normalized
+ if username is not None:
+ self.settings["admin_username"] = username or ""
+ notifications = self.settings.get("notification_chat_ids")
+ if not isinstance(notifications, list):
+ notifications = []
+ if normalized and normalized not in notifications:
+ notifications.append(normalized)
+ self.settings["notification_chat_ids"] = notifications
+ self._save_json(SETTINGS_FILE, self.settings)
+
+ def get_admin_username(self) -> str:
+ return str(self.settings.get("admin_username", "") or "")
+
+ def get_notification_chat_ids(self) -> List[int]:
+ recipients: List[int] = []
+ owner_id = self.get_owner_chat_id()
+ if owner_id:
+ recipients.append(owner_id)
+ try:
+ admin_id = int(self.settings.get("admin_chat_id", 0))
+ except (TypeError, ValueError):
+ admin_id = 0
+ if admin_id and admin_id not in recipients:
+ recipients.append(admin_id)
+ extras = self.settings.get("notification_chat_ids", [])
+ if isinstance(extras, list):
+ for value in extras:
+ try:
+ candidate = int(value)
+ except (TypeError, ValueError):
+ continue
+ if candidate and candidate not in recipients:
+ recipients.append(candidate)
+ return recipients
+
+ def get_manager_contact(self) -> str:
+ return str(self.settings.get("manager_contact_url", MANAGER_CONTACT_LINK))
+
+ def add_referral(self, referrer_id: int, new_user_id: int) -> bool:
+ referrer_key = str(referrer_id)
+ referred_list = self.referrals.setdefault(referrer_key, [])
+ if new_user_id in referred_list:
+ return False
+ referred_list.append(new_user_id)
+ self._save_json(REFERRALS_FILE, self.referrals)
+ self.add_bonus(referrer_id, REFERRAL_BONUS_AMOUNT, f"Реферал {new_user_id}")
+ return True
+
+ def add_bonus(self, user_id: int, amount: int, reason: str) -> None:
+ if not amount:
+ return
+ user_key = str(user_id)
+ entry = self.bonuses.setdefault(user_key, {"balance": 0, "history": []})
+ entry["balance"] = int(entry.get("balance", 0)) + int(amount)
+ history = entry.setdefault("history", [])
+ history.append(
+ {
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "amount": int(amount),
+ "reason": reason,
+ }
+ )
+ self._save_json(BONUSES_FILE, self.bonuses)
+
+ def get_bonus_info(self, user_id: int) -> Dict[str, object]:
+ entry = self.bonuses.get(str(user_id), {})
+ return {
+ "balance": int(entry.get("balance", 0)),
+ "history": list(entry.get("history", [])),
+ }
+
+ def list_referral_stats(self) -> List[Dict[str, object]]:
+ stats: List[Dict[str, object]] = []
+ for user_id, referred in self.referrals.items():
+ info = self.get_bonus_info(int(user_id))
+ stats.append(
+ {
+ "user_id": int(user_id),
+ "count": len(referred),
+ "bonus": int(info.get("balance", 0)),
+ }
+ )
+ stats.sort(key=lambda item: (item["bonus"], item["count"]), reverse=True)
+ return stats
+
+ def log_action(self, user_id: int, username: Optional[str], action: str) -> None:
+ entry = {
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "action": action,
+ "username": username or "",
+ }
+ user_key = str(user_id)
+ self.user_logs.setdefault(user_key, []).append(entry)
+ self._save_json(USER_LOGS_FILE, self.user_logs)
+
+ def add_feedback(self, user_id: int, feedback: str) -> None:
+ user_key = str(user_id)
+ self.feedbacks.setdefault(user_key, []).append(feedback)
+ self._save_json(FEEDBACKS_FILE, self.feedbacks)
+
+ def next_order_id(self, user_id: int) -> int:
+ user_key = str(user_id)
+ orders = self.orders.get(user_key, [])
+ if not orders:
+ return 1
+ last_id = max(int(order.get("order_id", 0)) for order in orders)
+ return last_id + 1
+
+ def add_order(self, user_id: int, order: OrderRecord) -> OrderRecord:
+ user_key = str(user_id)
+ orders = self.orders.setdefault(user_key, [])
+ orders.append(asdict(order))
+ self._save_json(ORDERS_FILE, self.orders)
+ return order
+
+ def get_orders(self, user_id: int) -> List[Dict[str, object]]:
+ return self.orders.get(str(user_id), [])
+
+ def get_order(self, user_id: int, order_id: int) -> Optional[Dict[str, object]]:
+ orders = self.orders.get(str(user_id))
+ if not orders:
+ return None
+ for order in orders:
+ if int(order.get("order_id", 0)) == int(order_id):
+ return order
+ return None
+
+ def list_recent_orders(self, limit: int = 10) -> List[Dict[str, object]]:
+ entries: List[Dict[str, object]] = []
+ for user_id, orders in self.orders.items():
+ for order in orders:
+ combined = {"user_id": int(user_id)}
+ combined.update(order)
+ entries.append(combined)
+ entries.sort(key=lambda item: item.get("created_at", ""), reverse=True)
+ return entries[:limit]
+
+ def total_spent(self, user_id: int) -> int:
+ return int(sum(int(order.get("price", 0)) for order in self.get_orders(user_id)))
+
+ def get_referrals(self, user_id: int) -> List[int]:
+ return self.referrals.get(str(user_id), [])
+
+ def export_orders(self) -> Optional[Path]:
+ records: List[Dict[str, object]] = []
+ for user_id, orders in self.orders.items():
+ for order in orders:
+ records.append({"user_id": user_id, **order})
+ if not records:
+ return None
+ df = pd.DataFrame(records)
+ export_path = DATA_DIR / "orders_export.xlsx"
+ df.to_excel(export_path, index=False)
+ return export_path
+
+ def get_statistics(self) -> Dict[str, int]:
+ orders_flat = [order for orders in self.orders.values() for order in orders]
+ total_orders = len(orders_flat)
+ total_revenue = int(sum(int(order.get("price", 0)) for order in orders_flat))
+ active_orders = 0
+ for order in orders_flat:
+ status_key = str(order.get("status_key", "")).lower()
+ status_text = str(order.get("status", "")).lower()
+ if status_key in {"done"}:
+ continue
+ if any(word in status_text for word in ("готов", "заверш")):
+ continue
+ active_orders += 1
+ unique_users = len(self.orders)
+ total_referrals = sum(len(refs) for refs in self.referrals.values())
+ total_bonuses = int(
+ sum(int(info.get("balance", 0)) for info in self.bonuses.values())
+ )
+ return {
+ "orders": total_orders,
+ "revenue": total_revenue,
+ "active": active_orders,
+ "users": unique_users,
+ "referrals": total_referrals,
+ "bonuses": total_bonuses,
+ }
+
+ def update_order_status(
+ self, user_id: int, order_id: int, status_key: str, status_title: str
+ ) -> Optional[Dict[str, object]]:
+ orders = self.orders.get(str(user_id))
+ if not orders:
+ return None
+ for order in orders:
+ if int(order.get("order_id", 0)) == int(order_id):
+ order["status_key"] = status_key
+ order["status"] = status_title
+ self._save_json(ORDERS_FILE, self.orders)
+ return order
+ return None
+
+
+store = DataStore()
+
+
+STATE_NAVIGATION, STATE_ORDER_TOPIC, STATE_ORDER_PAGES, STATE_ORDER_REQUIREMENTS, STATE_FEEDBACK, STATE_ADMIN = range(6)
+
+
+def schedule_restart(delay: float = 1.5) -> None:
+ loop = asyncio.get_running_loop()
+
+ def _shutdown() -> None:
+ logger.warning("Stopping bot process by admin request")
+ os._exit(0)
+
+ loop.call_later(delay, _shutdown)
+
+
+def log_user_action(update: Update, action: str) -> None:
+ user = update.effective_user
+ if not user:
+ return
+ store.log_action(user.id, user.username, action)
+
+
+async def ensure_owner_access(
+ update: Update, context: ContextTypes.DEFAULT_TYPE
+) -> bool:
+ user = update.effective_user
+ owner_id = store.get_owner_chat_id()
+ if not user or user.id != owner_id:
+ if update.message:
+ await update.message.reply_text(
+ "Админ-панель доступна только владельцу бота."
+ )
+ elif update.callback_query:
+ await update.callback_query.answer("Нет доступа", show_alert=True)
+ return False
+ return True
+
+
+def calculate_price(
+ order_type: str,
+ days_left: int,
+ page_multiplier: float = 1.0,
+ upsells: Iterable[str] = (),
+) -> int:
+ price_info = store.prices.get(order_type) or DEFAULT_PRICES.get(order_type)
+ if not price_info:
+ logger.warning("Unknown order type for pricing: %s", order_type)
+ return 0
+ base_price = int(price_info.get("base", 0))
+ subtotal = int(base_price * page_multiplier)
+ mode = store.get_pricing_mode()
+ if days_left <= 1:
+ subtotal = int(subtotal * (1.8 if mode == "hard" else 1.65))
+ elif days_left <= 2:
+ subtotal = int(subtotal * (1.6 if mode == "hard" else 1.45))
+ elif days_left <= 3:
+ subtotal = int(subtotal * (1.35 if mode == "hard" else 1.25))
+ elif days_left <= 5:
+ subtotal = int(subtotal * (1.2 if mode == "hard" else 1.15))
+ elif days_left <= 7:
+ subtotal = int(subtotal * (1.15 if mode == "hard" else 1.1))
+ elif days_left <= 14 and mode == "hard":
+ subtotal = int(subtotal * 1.05)
+ total = subtotal
+ for upsell in upsells:
+ option = UPSELL_OPTIONS.get(upsell)
+ if option:
+ total += option["price"]
+ return total
+
+
+def build_main_menu_keyboard() -> InlineKeyboardMarkup:
+ contact_url = store.get_manager_contact()
+ keyboard = [
+ [InlineKeyboardButton("📝 Сделать заказ", callback_data="main:order")],
+ [
+ InlineKeyboardButton("💲 Прайс-лист", callback_data="main:prices"),
+ InlineKeyboardButton("🧮 Калькулятор", callback_data="main:calculator"),
+ ],
+ [
+ InlineKeyboardButton("👤 Профиль", callback_data="main:profile"),
+ InlineKeyboardButton("❓ FAQ", callback_data="main:faq"),
+ ],
+ [InlineKeyboardButton("📞 Администратор", url=contact_url)],
+ ]
+ return InlineKeyboardMarkup(keyboard)
+
+
+def order_type_name_from_key(type_key: Optional[str]) -> str:
+ if type_key and type_key in ORDER_TYPES:
+ return ORDER_TYPES[type_key]["name"]
+ return str(type_key or "Неизвестный тип")
+
+
+def order_type_name_from_record(order: Dict[str, object]) -> str:
+ type_key = order.get("type_key")
+ if type_key in ORDER_TYPES:
+ return ORDER_TYPES[type_key]["name"]
+ legacy_name = order.get("type")
+ if legacy_name:
+ return str(legacy_name)
+ return order_type_name_from_key(type_key if isinstance(type_key, str) else None)
+
+
+def format_order_summary(
+ draft: Dict[str, object], base_price: int, final_price: int, discount_value: int
+) -> str:
+ order_type = ORDER_TYPES.get(draft.get("type_key")) or {}
+ upsells = draft.get("upsells", [])
+ upsell_lines = []
+ for upsell in upsells:
+ info = UPSELL_OPTIONS.get(upsell)
+ if info:
+ upsell_lines.append(f"• {info['title']} (+{info['price']} ₽)")
+ upsell_text = "\n".join(upsell_lines) if upsell_lines else "—"
+ deadline_days = int(draft.get("deadline_days", 0))
+ deadline_date = (datetime.now() + timedelta(days=deadline_days)).strftime("%d.%m.%Y")
+ page_label = draft.get("page_label") or get_page_option(str(draft.get("page_key"))).get("label")
+ attachments = draft.get("attachments", [])
+ attachments_text = (
+ f"{len(attachments)} файл(ов)" if attachments else "можно прикрепить позже"
+ )
+ discount_line = (
+ f"Скидка новичка: −{discount_value} ₽\n" if discount_value else ""
+ )
+ return (
+ f"Проверим данные перед оформлением:\n\n"
+ f"Тип: {order_type.get('icon', '')} {order_type.get('name', 'Неизвестно')}\n"
+ f"Тема: {html.escape(str(draft.get('topic', 'не указана')))}\n"
+ f"Срок: {deadline_days} дн. (до {deadline_date})\n"
+ f"Объём: {page_label}\n"
+ f"Требования: {html.escape(str(draft.get('requirements', 'не указаны')))}\n"
+ f"Файлы: {attachments_text}\n"
+ f"Доп. услуги: {upsell_text}\n\n"
+ f"Базовая стоимость: {base_price} ₽\n"
+ f"{discount_line}Итого к оплате: {final_price} ₽"
+ )
+
+
+async def get_bot_username(context: ContextTypes.DEFAULT_TYPE) -> str:
+ username = context.application.bot_data.get("bot_username")
+ if username:
+ return username
+ me = await context.bot.get_me()
+ username = me.username or ""
+ context.application.bot_data["bot_username"] = username
+ return username
+
+
+async def show_main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE, text: Optional[str] = None) -> int:
+ markup = build_main_menu_keyboard()
+ message_text = text or (
+ "Выберите нужный раздел 👇\n"
+ "Срочный заказ за 24 часа? Просто прикрепите задание — мы возьмёмся сразу."
+ )
+ if update.callback_query:
+ query = update.callback_query
+ try:
+ await query.edit_message_text(message_text, reply_markup=markup, parse_mode=ParseMode.HTML)
+ except BadRequest as exc:
+ if "message is not modified" not in str(exc):
+ raise
+ elif update.message:
+ await update.message.reply_text(message_text, reply_markup=markup, parse_mode=ParseMode.HTML)
+ return STATE_NAVIGATION
+
+def get_order_draft(context: ContextTypes.DEFAULT_TYPE) -> Dict[str, object]:
+ draft = context.user_data.setdefault("order_draft", {})
+ draft.setdefault("upsells", set())
+ return draft
+
+
+def get_page_option(key: Optional[str]) -> Dict[str, object]:
+ if key and key in PAGE_OPTIONS:
+ return PAGE_OPTIONS[key]
+ return PAGE_OPTIONS["20"]
+
+
+def ensure_requirement_buckets(draft: Dict[str, object]) -> None:
+ draft.setdefault("requirements_texts", [])
+ draft.setdefault("attachments", [])
+
+
+async def show_order_types(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ log_user_action(update, "open_order_types")
+ keyboard = [
+ [
+ InlineKeyboardButton(
+ f"{info['icon']} {info['name']}", callback_data=f"order:type:{key}"
+ )
+ ]
+ for key, info in ORDER_TYPES.items()
+ ]
+ keyboard.append([InlineKeyboardButton("⬅️ Главное меню", callback_data="main:root")])
+ markup = InlineKeyboardMarkup(keyboard)
+ text = "Выберите тип работы. Можно оформить несколько заказов подряд — будет дополнительная скидка!"
+ query = update.callback_query
+ try:
+ await query.edit_message_text(text, reply_markup=markup)
+ except BadRequest as exc:
+ if "message is not modified" not in str(exc):
+ raise
+ return STATE_NAVIGATION
+
+
+async def show_order_details(update: Update, key: str) -> int:
+ query = update.callback_query
+ info = ORDER_TYPES.get(key)
+ if not info:
+ await query.edit_message_text("Неизвестный тип работы. Попробуйте снова.")
+ return STATE_NAVIGATION
+ examples = ", ".join(info.get("examples", []))
+ text = (
+ f"{info['icon']} {info['name']}\n\n"
+ f"{info['description']}\n\n"
+ f"Что включено: {info['details']}\n"
+ f"Примеры: {html.escape(examples)}"
+ )
+ keyboard = [
+ [InlineKeyboardButton("✅ Выбрать", callback_data=f"order:new:{key}")],
+ [InlineKeyboardButton("⬅️ Назад", callback_data="order:list")],
+ ]
+ await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode=ParseMode.HTML)
+ return STATE_NAVIGATION
+
+
+async def prompt_order_topic(update: Update, context: ContextTypes.DEFAULT_TYPE, key: str) -> int:
+ draft = get_order_draft(context)
+ draft.clear()
+ draft.update({"type_key": key, "upsells": set()})
+ query = update.callback_query
+ await query.edit_message_text(
+ "Напишите тему работы сообщением. Можно прикрепить требования позже. Для отмены /cancel",
+ parse_mode=ParseMode.HTML,
+ )
+ return STATE_ORDER_TOPIC
+
+
+async def receive_topic(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ draft = get_order_draft(context)
+ topic = update.message.text.strip()
+ if not topic:
+ await update.message.reply_text("Пожалуйста, отправьте тему текстом.")
+ return STATE_ORDER_TOPIC
+ draft["topic"] = topic
+ log_user_action(update, f"order_topic:{topic}")
+ return await prompt_deadline(update, context)
+
+
+async def prompt_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ text = (
+ "Выберите срок сдачи. Срочные заказы (до 24 часов) выполняем с приоритетом — "
+ "это дороже, но экономит вам время."
+ )
+ today = datetime.now()
+ keyboard: List[List[InlineKeyboardButton]] = []
+ for days, label in DEADLINE_CHOICES:
+ deadline = today + timedelta(days=days)
+ keyboard.append(
+ [
+ InlineKeyboardButton(
+ f"{label} · до {deadline:%d.%m}",
+ callback_data=f"order:deadline:{days}",
+ )
+ ]
+ )
+ keyboard.append([InlineKeyboardButton("⬅️ Отмена", callback_data="order:cancel")])
+ markup = InlineKeyboardMarkup(keyboard)
+ await update.message.reply_text(text, reply_markup=markup)
+ return STATE_NAVIGATION
+
+
+async def handle_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE, days: int) -> int:
+ draft = get_order_draft(context)
+ draft["deadline_days"] = days
+ return await prompt_page_selection(update, context)
+
+
+async def prompt_page_selection(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ draft = get_order_draft(context)
+ query = update.callback_query
+ text = (
+ "Сколько примерно страниц или слайдов нужно оформить? Чем больше объём, тем тщательнее подбор автора."
+ )
+ keyboard = [
+ [InlineKeyboardButton(option["label"], callback_data=f"order:pages:{key}")]
+ for key, option in PAGE_OPTIONS.items()
+ ]
+ keyboard.append([InlineKeyboardButton("⬅️ Отмена", callback_data="order:cancel")])
+ await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
+ return STATE_ORDER_PAGES
+
+
+async def prompt_requirements_input(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ draft = get_order_draft(context)
+ ensure_requirement_buckets(draft)
+ keyboard = InlineKeyboardMarkup(
+ [
+ [InlineKeyboardButton("Готово, дальше", callback_data="order:req_finish")],
+ [InlineKeyboardButton("⬅️ Отменить", callback_data="order:cancel")],
+ ]
+ )
+ await update.callback_query.edit_message_text(
+ (
+ "Отправьте требования сообщениями и прикрепите файлы (Word, PDF, фото, аудио — всё принимаем).\n"
+ "Для самостоятельных работ приложите задание, для курсовых/ВКР/магистерских — методические рекомендации.\n"
+ "Когда закончите, нажмите кнопку ниже или отправьте команду /done."
+ ),
+ reply_markup=keyboard,
+ )
+ return STATE_ORDER_REQUIREMENTS
+
+
+async def handle_page_selection(update: Update, context: ContextTypes.DEFAULT_TYPE, key: str) -> int:
+ draft = get_order_draft(context)
+ option = get_page_option(key)
+ draft["page_key"] = key
+ draft["page_label"] = option["label"]
+ draft["page_multiplier"] = float(option.get("multiplier", 1.0))
+ ensure_requirement_buckets(draft)
+ return await prompt_requirements_input(update, context)
+
+
+async def receive_requirements(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ draft = get_order_draft(context)
+ ensure_requirement_buckets(draft)
+ message_text = update.message.text.strip()
+ if not message_text:
+ await update.message.reply_text("Пожалуйста, опишите требования текстом или отправьте /skip, если их нет.")
+ return STATE_ORDER_REQUIREMENTS
+ draft.setdefault("requirements_texts", []).append(message_text)
+ draft["requirements"] = "\n\n".join(draft["requirements_texts"])
+ log_user_action(update, "order_requirements_note")
+ await update.message.reply_text(
+ "Приняли. Можно добавить ещё комментариев или прикрепить файлы. Когда всё готово — /done."
+ )
+ return STATE_ORDER_REQUIREMENTS
+
+
+async def skip_requirements(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ draft = get_order_draft(context)
+ ensure_requirement_buckets(draft)
+ draft["requirements_texts"] = []
+ draft["requirements"] = "Нет дополнительных требований"
+ return await finish_requirements(update, context)
+
+
+def finalize_requirement_text(draft: Dict[str, object]) -> str:
+ notes = [note.strip() for note in draft.get("requirements_texts", []) if note.strip()]
+ if notes:
+ combined = "\n\n".join(notes)
+ else:
+ combined = str(draft.get("requirements", "Нет дополнительных требований"))
+ draft["requirements"] = combined
+ return combined
+
+
+async def finish_requirements(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ draft = get_order_draft(context)
+ ensure_requirement_buckets(draft)
+ finalize_requirement_text(draft)
+ attachments = draft.get("attachments", [])
+ log_user_action(update, f"order_requirements_done:{len(draft['requirements_texts'])}:{len(attachments)}")
+ if update.message:
+ return await show_upsell_menu(update, context)
+ if update.callback_query:
+ await update.callback_query.answer("Требования сохранены", show_alert=False)
+ return await show_upsell_menu(update, context)
+
+
+async def handle_requirement_attachment(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ draft = get_order_draft(context)
+ ensure_requirement_buckets(draft)
+ message = update.message
+ attachment_entry: Dict[str, str]
+ label = "Файл"
+ attachment_type = "attachment"
+ if message.document:
+ doc = message.document
+ label = doc.file_name or (doc.mime_type or "Документ")
+ attachment_entry = {
+ "type": "document",
+ "file_id": doc.file_id,
+ "name": label,
+ }
+ elif message.photo:
+ photo = message.photo[-1]
+ label = message.caption or "Фото задания"
+ attachment_entry = {
+ "type": "photo",
+ "file_id": photo.file_id,
+ "name": label,
+ }
+ elif message.audio:
+ audio = message.audio
+ label = audio.title or audio.file_name or "Аудиофайл"
+ attachment_entry = {
+ "type": "audio",
+ "file_id": audio.file_id,
+ "name": label,
+ }
+ elif message.voice:
+ label = "Голосовое сообщение"
+ attachment_entry = {
+ "type": "voice",
+ "file_id": message.voice.file_id,
+ "name": label,
+ }
+ elif message.video:
+ video = message.video
+ label = video.file_name or "Видеофайл"
+ attachment_entry = {
+ "type": "video",
+ "file_id": video.file_id,
+ "name": label,
+ }
+ elif message.video_note:
+ label = "Круглое видео"
+ attachment_entry = {
+ "type": "video_note",
+ "file_id": message.video_note.file_id,
+ "name": label,
+ }
+ else:
+ label = "Файл"
+ attachment_entry = {"type": attachment_type, "file_id": "", "name": label}
+ attachments = draft.setdefault("attachments", [])
+ if attachment_entry.get("file_id"):
+ attachments.append(attachment_entry)
+ attachment_type = attachment_entry.get("type", attachment_type)
+ caption = message.caption
+ if caption:
+ draft.setdefault("requirements_texts", []).append(caption.strip())
+ finalize_requirement_text(draft)
+ log_user_action(update, f"order_attachment:{attachment_type}")
+ for admin_chat_id in store.get_notification_chat_ids():
+ try:
+ await message.forward(admin_chat_id)
+ except Exception as exc: # pragma: no cover - зависит от Telegram API
+ logger.warning("Не удалось переслать вложение админу %s: %s", admin_chat_id, exc)
+ await message.reply_text(
+ f"Добавили: {label}. Можно прикрепить ещё или нажать /done, когда всё готово.")
+ return STATE_ORDER_REQUIREMENTS
+
+
+def build_upsell_keyboard(selected: Iterable[str]) -> InlineKeyboardMarkup:
+ keyboard: List[List[InlineKeyboardButton]] = []
+ selected_set = set(selected)
+ for key, info in UPSELL_OPTIONS.items():
+ prefix = "✅" if key in selected_set else "➕"
+ keyboard.append(
+ [InlineKeyboardButton(f"{prefix} {info['title']} (+{info['price']} ₽)", callback_data=f"order:upsell:{key}")]
+ )
+ keyboard.append([InlineKeyboardButton("Продолжить", callback_data="order:summary")])
+ keyboard.append([InlineKeyboardButton("Отменить", callback_data="order:cancel")])
+ return InlineKeyboardMarkup(keyboard)
+
+
+async def show_upsell_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ draft = get_order_draft(context)
+ raw_selected = draft.get("upsells", set())
+ selected = raw_selected if isinstance(raw_selected, set) else set(raw_selected)
+ draft["upsells"] = selected
+ if update.message:
+ await update.message.reply_text(
+ "Добавьте дополнительные услуги: презентацию с речью или личного куратора с ежедневной связью."
+ " Можно сразу перейти к подтверждению.",
+ reply_markup=build_upsell_keyboard(selected),
+ )
+ else:
+ query = update.callback_query
+ await query.edit_message_text(
+ "Выберите дополнительные услуги или переходите к подтверждению заказа.",
+ reply_markup=build_upsell_keyboard(selected),
+ )
+ return STATE_NAVIGATION
+
+
+async def toggle_upsell(update: Update, context: ContextTypes.DEFAULT_TYPE, key: str) -> int:
+ draft = get_order_draft(context)
+ selected_raw = draft.get("upsells", set())
+ selected = selected_raw if isinstance(selected_raw, set) else set(selected_raw)
+ if key in selected:
+ selected.remove(key)
+ else:
+ selected.add(key)
+ draft["upsells"] = selected
+ query = update.callback_query
+ await query.edit_message_text(
+ "Отлично! Можно добавить еще или перейти к подтверждению.",
+ reply_markup=build_upsell_keyboard(selected),
+ )
+ return STATE_NAVIGATION
+
+
+async def show_order_summary_step(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ draft = get_order_draft(context)
+ user = update.effective_user
+ if "topic" not in draft or "deadline_days" not in draft:
+ return await show_main_menu(update, context, "Начните оформление заказа заново.")
+ upsells = list(draft.get("upsells", set()))
+ draft["upsells"] = upsells
+ page_multiplier = float(draft.get("page_multiplier", get_page_option(str(draft.get("page_key"))).get("multiplier", 1.0)))
+ base_price = calculate_price(
+ draft["type_key"],
+ int(draft["deadline_days"]),
+ page_multiplier,
+ upsells,
+ )
+ existing_orders = store.get_orders(user.id) if user else []
+ discount_value = 0
+ if user and not existing_orders:
+ discount_value = min(int(base_price * FIRST_ORDER_DISCOUNT_RATE), FIRST_ORDER_DISCOUNT_CAP)
+ final_price = max(base_price - discount_value, 0)
+ draft["base_price"] = base_price
+ draft["discount_value"] = discount_value
+ draft["price"] = final_price
+ text = format_order_summary(draft, base_price, final_price, discount_value)
+ keyboard = [
+ [InlineKeyboardButton("✅ Подтвердить заказ", callback_data="order:confirm")],
+ [InlineKeyboardButton("✏️ Изменить допы", callback_data="order:upsell")],
+ [InlineKeyboardButton("❌ Отменить", callback_data="order:cancel")],
+ ]
+ query = update.callback_query
+ await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
+ return STATE_NAVIGATION
+
+
+async def cancel_order(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ context.user_data.pop("order_draft", None)
+ if update.callback_query:
+ await update.callback_query.edit_message_text("Оформление отменено. Можно начать заново из меню.")
+ return await show_main_menu(update, context)
+ if update.message:
+ await update.message.reply_text("Оформление заказа отменено.")
+ return await show_main_menu(update, context)
+
+
+async def confirm_order(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ user = update.effective_user
+ if not user:
+ return STATE_NAVIGATION
+ draft = get_order_draft(context)
+ if not draft:
+ return await show_main_menu(update, context, "Корзина пуста. Попробуйте оформить заказ заново.")
+ order_id = store.next_order_id(user.id)
+ deadline_days = int(draft.get("deadline_days", 0))
+ deadline_date = (datetime.now() + timedelta(days=deadline_days)).strftime("%d.%m.%Y")
+ page_plan = draft.get("page_label") or get_page_option(str(draft.get("page_key"))).get("label")
+ attachments = list(draft.get("attachments", []))
+ base_price = int(draft.get("base_price", draft.get("price", 0)))
+ discount_value = int(draft.get("discount_value", 0))
+ final_price = int(draft.get("price", base_price))
+ order = OrderRecord(
+ order_id=order_id,
+ type_key=str(draft.get("type_key")),
+ topic=str(draft.get("topic", "")),
+ deadline_days=deadline_days,
+ deadline_date=deadline_date,
+ page_plan=str(page_plan),
+ requirements=str(draft.get("requirements", "")),
+ attachments=attachments,
+ upsells=list(draft.get("upsells", [])),
+ status_key="new",
+ base_price=base_price,
+ discount=discount_value,
+ price=final_price,
+ status=ORDER_STATUS_TITLES.get("new", "новый"),
+ created_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ )
+ store.add_order(user.id, order)
+ log_user_action(update, f"order_confirmed:{order.order_id}")
+ context.user_data.pop("order_draft", None)
+ loyalty_bonus = int(final_price * LOYALTY_REWARD_RATE)
+ if loyalty_bonus:
+ store.add_bonus(user.id, loyalty_bonus, f"Бонус за заказ #{order.order_id}")
+ bonus_info = store.get_bonus_info(user.id)
+ bonus_balance = int(bonus_info.get("balance", 0))
+ text = (
+ f"Спасибо! Заказ #{order.order_id} оформлен.\n"
+ f"Менеджер свяжется с вами в ближайшее время. Статус можно отслеживать в профиле.\n\n"
+ f"Текущий бонусный баланс: {bonus_balance} ₽."
+ )
+ if loyalty_bonus:
+ text += f"\nНачислили +{loyalty_bonus} ₽ за заказ — их можно тратить на следующие работы."
+ query = update.callback_query
+ await query.edit_message_text(text)
+ notification_ids = store.get_notification_chat_ids()
+ if notification_ids:
+ order_type = order_type_name_from_key(order.type_key)
+ upsell_titles = [UPSELL_OPTIONS.get(u, {}).get("title", u) for u in order.upsells]
+ admin_text = (
+ f"Новый заказ #{order.order_id}\n"
+ f"Пользователь: {user.id} ({user.username or user.full_name})\n"
+ f"Тип: {order_type}\n"
+ f"Тема: {order.topic}\n"
+ f"Срок: {order.deadline_days} дн. (до {order.deadline_date})\n"
+ f"Объём: {page_plan}\n"
+ f"Опции: {', '.join(upsell_titles) if upsell_titles else 'нет'}\n"
+ f"Базовая сумма: {base_price} ₽\n"
+ f"Скидка: {discount_value} ₽\n"
+ f"Итого: {final_price} ₽\n"
+ f"Файлы: {len(attachments)} (пересланы отдельными сообщениями)"
+ )
+ for admin_chat_id in notification_ids:
+ try:
+ await context.bot.send_message(admin_chat_id, admin_text)
+ except Exception as exc: # pragma: no cover - depends on Telegram API
+ logger.error("Failed to notify admin %s: %s", admin_chat_id, exc)
+ return await show_main_menu(update, context, "Хотите оформить еще одну работу?")
+
+async def show_price_list(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ log_user_action(update, "open_price_list")
+ rows = []
+ for key, info in ORDER_TYPES.items():
+ price_info = store.prices.get(key, DEFAULT_PRICES[key])
+ rows.append(f"{info['icon']} {info['name']} — от {price_info['base']} ₽")
+ text = (
+ "💰 Прайс-лист\n"
+ "6 лет опыта и 4000+ работ для {UNIVERSITIES_EXAMPLES}.\n"
+ "Срочные заказы (до 24 часов) выполняем с коэффициентом — это быстрее и выгоднее, чем переделывать в последний день.\n\n"
+ ).format(UNIVERSITIES_EXAMPLES=UNIVERSITIES_EXAMPLES) + "\n".join(rows)
+ keyboard = [
+ [
+ InlineKeyboardButton(f"Подробнее: {info['name']}", callback_data=f"prices:detail:{key}")
+ ]
+ for key, info in ORDER_TYPES.items()
+ ]
+ keyboard.append([InlineKeyboardButton("🧮 Калькулятор", callback_data="main:calculator")])
+ keyboard.append([InlineKeyboardButton("⬅️ Главное меню", callback_data="main:root")])
+ query = update.callback_query
+ await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode=ParseMode.HTML)
+ return STATE_NAVIGATION
+
+
+async def show_price_detail(update: Update, key: str) -> int:
+ info = ORDER_TYPES.get(key)
+ price_info = store.prices.get(key, DEFAULT_PRICES.get(key, {}))
+ if not info:
+ await update.callback_query.edit_message_text("Неизвестный тип работы.")
+ return STATE_NAVIGATION
+ text = (
+ f"{info['icon']} {info['name']}\n\n"
+ f"{info['description']}\n\n"
+ f"Диапазон цен: {price_info.get('min', price_info.get('base', 0))}–{price_info.get('max', price_info.get('base', 0))} ₽\n"
+ f"Что входит: {info['details']}\n"
+ f"Примеры: {html.escape(', '.join(info.get('examples', [])))}\n"
+ "Срочно? Сделаем за 24 часа с надбавкой и постоянной обратной связью.\n"
+ "Файлы: прикрепляйте методички, задания и данные прямо сюда — всё сразу уйдет в работу."
+ )
+ keyboard = [
+ [InlineKeyboardButton("📝 Оформить", callback_data=f"order:type:{key}")],
+ [InlineKeyboardButton("⬅️ Назад", callback_data="main:prices")],
+ ]
+ await update.callback_query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
+ return STATE_NAVIGATION
+
+
+async def show_calculator(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ log_user_action(update, "open_calculator")
+ keyboard = [
+ [InlineKeyboardButton(f"{info['icon']} {info['name']}", callback_data=f"calc:type:{key}")]
+ for key, info in ORDER_TYPES.items()
+ ]
+ keyboard.append([InlineKeyboardButton("⬅️ Главное меню", callback_data="main:root")])
+ await update.callback_query.edit_message_text(
+ "Выберите тип работы для расчета стоимости:",
+ reply_markup=InlineKeyboardMarkup(keyboard),
+ )
+ return STATE_NAVIGATION
+
+
+async def calculator_select_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE, key: str) -> int:
+ context.user_data.setdefault("calculator", {})["type"] = key
+ keyboard = []
+ for days, label in DEADLINE_CHOICES:
+ keyboard.append([InlineKeyboardButton(label, callback_data=f"calc:deadline:{days}")])
+ keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data="main:calculator")])
+ await update.callback_query.edit_message_text(
+ "Выберите срок выполнения:", reply_markup=InlineKeyboardMarkup(keyboard)
+ )
+ return STATE_NAVIGATION
+
+
+async def calculator_select_pages(update: Update, context: ContextTypes.DEFAULT_TYPE, days: int) -> int:
+ context.user_data.setdefault("calculator", {})["deadline"] = days
+ keyboard = [
+ [InlineKeyboardButton(option["label"], callback_data=f"calc:pages:{key}")]
+ for key, option in PAGE_OPTIONS.items()
+ ]
+ keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data="main:calculator")])
+ await update.callback_query.edit_message_text(
+ "Оцените объём работы:", reply_markup=InlineKeyboardMarkup(keyboard)
+ )
+ return STATE_NAVIGATION
+
+
+async def show_calculation_result(update: Update, context: ContextTypes.DEFAULT_TYPE, page_key: str) -> int:
+ data = context.user_data.get("calculator", {})
+ type_key = data.get("type")
+ days = int(data.get("deadline", 14))
+ if not type_key:
+ return await show_calculator(update, context)
+ page_option = get_page_option(page_key)
+ data["page_key"] = page_key
+ multiplier = float(page_option.get("multiplier", 1.0))
+ price = calculate_price(type_key, days, multiplier)
+ info = ORDER_TYPES.get(type_key, {})
+ user = update.effective_user
+ discount_value = 0
+ if user and not store.get_orders(user.id):
+ discount_value = min(int(price * FIRST_ORDER_DISCOUNT_RATE), FIRST_ORDER_DISCOUNT_CAP)
+ final_price = max(price - discount_value, 0)
+ text = (
+ f"Расчет для {info.get('icon', '')} {info.get('name', 'работы')}\n"
+ f"Срок: {days} дней\n"
+ f"Объём: {page_option['label']}\n\n"
+ f"Примерная стоимость: {price} ₽\n"
+ )
+ if discount_value:
+ text += f"Скидка новичка: −{discount_value} ₽\nИтого: {final_price} ₽\n"
+ text += "\nХотите оформить заказ прямо сейчас?"
+ keyboard = [
+ [InlineKeyboardButton("📝 Сделать заказ", callback_data=f"order:type:{type_key}")],
+ [InlineKeyboardButton("🔁 Пересчитать", callback_data="main:calculator")],
+ [InlineKeyboardButton("⬅️ Главное меню", callback_data="main:root")],
+ ]
+ await update.callback_query.edit_message_text(
+ text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard)
+ )
+ return STATE_NAVIGATION
+
+
+async def show_profile(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ user = update.effective_user
+ if not user:
+ return STATE_NAVIGATION
+ log_user_action(update, "open_profile")
+ orders = store.get_orders(user.id)
+ total_orders = len(orders)
+ total_spent = store.total_spent(user.id)
+ referrals = store.get_referrals(user.id)
+ bonus_info = store.get_bonus_info(user.id)
+ bonus_balance = int(bonus_info.get("balance", 0))
+ bot_username = await get_bot_username(context)
+ ref_link = f"https://t.me/{bot_username}?start={user.id}" if bot_username else "—"
+ last_order = orders[-1] if orders else None
+ discount_status = (
+ "Доступна скидка 7% на первый заказ (до 3000 ₽)"
+ if total_orders == 0
+ else "Скидка новичка уже использована — теперь копим бонусы"
+ )
+ profile_lines = [
+ f"👤 Профиль {html.escape(user.first_name or user.full_name or '')}",
+ "",
+ f"Статус: {'Новичок' if total_orders == 0 else 'Постоянный клиент'}",
+ f"Заказов: {total_orders} на {total_spent} ₽",
+ f"Бонусный счёт: {bonus_balance} ₽",
+ f"Рефералов: {len(referrals)} (по {REFERRAL_BONUS_AMOUNT} ₽ за каждого)",
+ discount_status,
+ ]
+ if last_order:
+ profile_lines.extend(
+ [
+ "",
+ f"Последний заказ #{last_order.get('order_id')}: {order_type_name_from_record(last_order)}",
+ f"Статус: {last_order.get('status', 'в работе')}",
+ f"Срок: {last_order.get('deadline_date', '—')}",
+ ]
+ )
+ profile_lines.extend(
+ [
+ "",
+ f"Реферальная ссылка: {ref_link}",
+ "Поделитесь ею — бонусы накапливаются автоматически.",
+ ]
+ )
+ text = "\n".join(profile_lines)
+ keyboard = [
+ [InlineKeyboardButton("📋 Мои заказы", callback_data="profile:orders")],
+ [InlineKeyboardButton("⭐ Оставить отзыв", callback_data="profile:feedback")],
+ [InlineKeyboardButton("⬅️ Главное меню", callback_data="main:root")],
+ ]
+ await update.callback_query.edit_message_text(
+ text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard)
+ )
+ return STATE_NAVIGATION
+
+
+async def show_user_orders(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ user = update.effective_user
+ if not user:
+ return STATE_NAVIGATION
+ orders = store.get_orders(user.id)
+ if not orders:
+ text = "Пока нет заказов. Самое время оформить первый!"
+ else:
+ lines = []
+ for order in orders[-10:]:
+ name = order_type_name_from_record(order)
+ attachments_count = len(order.get("attachments", []))
+ discount_amount = int(order.get("discount", 0))
+ lines.append(
+ f"#{order.get('order_id')} — {name}\n"
+ f"Тема: {order.get('topic')}\n"
+ f"Срок: {order.get('deadline_date')}\n"
+ f"Объём: {order.get('page_plan', '—')}\n"
+ f"Статус: {order.get('status', 'в работе')}\n"
+ f"Сумма: {order.get('price', 0)} ₽ (скидка {discount_amount} ₽)\n"
+ f"Файлов: {attachments_count}\n"
+ )
+ text = "\n".join(lines) + "\nЕсли появились новые файлы или вопросы — ответьте на это сообщение, и менеджер свяжется."
+ keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="main:profile")]]
+ await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
+ return STATE_NAVIGATION
+
+
+async def request_feedback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ await update.callback_query.edit_message_text(
+ "Поделитесь впечатлениями о нашей работе. Отзыв увидит администратор и подарит бонусы!"
+ )
+ return STATE_FEEDBACK
+
+
+async def receive_feedback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ feedback = update.message.text.strip()
+ user = update.effective_user
+ if not feedback:
+ await update.message.reply_text("Отзыв не может быть пустым. Попробуйте еще раз.")
+ return STATE_FEEDBACK
+ store.add_feedback(user.id, feedback)
+ log_user_action(update, "feedback_left")
+ await update.message.reply_text("Спасибо! Мы ценим обратную связь — менеджер ответит в ближайшее время.")
+ for admin_chat_id in store.get_notification_chat_ids():
+ try:
+ await context.bot.send_message(
+ admin_chat_id,
+ f"Новый отзыв от {user.id} ({user.username or user.full_name}):\n{feedback}",
+ )
+ except Exception as exc: # pragma: no cover
+ logger.error("Failed to send feedback to admin %s: %s", admin_chat_id, exc)
+ return await show_main_menu(update, context)
+
+
+async def show_faq(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ keyboard = [
+ [InlineKeyboardButton(item["question"], callback_data=f"faq:item:{idx}")]
+ for idx, item in enumerate(FAQ_ITEMS)
+ ]
+ keyboard.append([InlineKeyboardButton("⬅️ Главное меню", callback_data="main:root")])
+ await update.callback_query.edit_message_text(
+ "Частые вопросы. Выберите интересующий пункт:",
+ reply_markup=InlineKeyboardMarkup(keyboard),
+ )
+ return STATE_NAVIGATION
+
+
+async def show_faq_item(update: Update, idx: int) -> int:
+ if idx < 0 or idx >= len(FAQ_ITEMS):
+ await update.callback_query.edit_message_text("Вопрос не найден.")
+ return STATE_NAVIGATION
+ item = FAQ_ITEMS[idx]
+ keyboard = [
+ [InlineKeyboardButton("⬅️ Назад", callback_data="main:faq")],
+ [InlineKeyboardButton("⬅️ Главное меню", callback_data="main:root")],
+ ]
+ await update.callback_query.edit_message_text(
+ f"❓ {item['question']}\n\n{item['answer']}",
+ parse_mode=ParseMode.HTML,
+ reply_markup=InlineKeyboardMarkup(keyboard),
+ )
+ return STATE_NAVIGATION
+
+async def show_admin_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ keyboard = [
+ [InlineKeyboardButton("📊 Статистика", callback_data="admin:stats")],
+ [InlineKeyboardButton("📋 Последние заказы", callback_data="admin:orders")],
+ [InlineKeyboardButton("💰 Режим ценообразования", callback_data="admin:pricing")],
+ [InlineKeyboardButton("🎁 Бонусы и рефералы", callback_data="admin:bonuses")],
+ [InlineKeyboardButton("📤 Экспорт в Excel", callback_data="admin:export")],
+ [InlineKeyboardButton("♻️ Обновить статус", callback_data="admin:status_list")],
+ [InlineKeyboardButton("🗂 Последние действия", callback_data="admin:logs")],
+ [InlineKeyboardButton("🔄 Перезапустить бота", callback_data="admin:restart")],
+ [InlineKeyboardButton("⬅️ Главное меню", callback_data="main:root")],
+ ]
+ text = "🔐 Админ-панель. Выберите действие:"
+ if update.message:
+ await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
+ else:
+ await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
+ return STATE_NAVIGATION
+
+
+async def restart_bot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ user = update.effective_user
+ source = "callback" if update.callback_query else "command"
+ notify_text = "Перезапускаю бота. Он вернётся через несколько секунд."
+ if update.callback_query:
+ await update.callback_query.answer("Перезапускаю…")
+ await context.bot.send_message(chat_id=user.id, text=notify_text)
+ elif update.message:
+ await update.message.reply_text(notify_text)
+ logger.warning("Restart requested by admin %s via %s", user.id, source)
+ schedule_restart()
+ return STATE_NAVIGATION
+
+
+async def admin_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ log_user_action(update, "admin_command")
+ return await show_admin_menu(update, context)
+
+
+async def admin_show_stats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ stats = store.get_statistics()
+ text = (
+ "📊 Сводка по заказам\n\n"
+ f"Всего заказов: {stats['orders']}\n"
+ f"Активных заказов: {stats['active']}\n"
+ f"Выручка: {stats['revenue']} ₽\n"
+ f"Клиентов: {stats['users']}\n"
+ f"Рефералов: {stats['referrals']}\n"
+ f"Начислено бонусов: {stats['bonuses']} ₽"
+ )
+ await update.callback_query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data="admin:menu")]]))
+ return STATE_NAVIGATION
+
+
+async def admin_show_orders(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ records = [order for orders in store.orders.values() for order in orders]
+ records = sorted(records, key=lambda item: item.get("created_at", ""), reverse=True)[:10]
+ if not records:
+ text = "Заказов пока нет."
+ else:
+ lines = []
+ for record in records:
+ name = order_type_name_from_record(record)
+ base_amount = int(record.get("base_price", record.get("price", 0)))
+ discount_amount = int(record.get("discount", 0))
+ final_amount = int(record.get("price", 0))
+ attachments_count = len(record.get("attachments", []))
+ upsell_titles = [
+ UPSELL_OPTIONS.get(key, {}).get("title", key)
+ for key in record.get("upsells", [])
+ ]
+ lines.append(
+ f"#{record.get('order_id')} — {name}\n"
+ f"Тема: {record.get('topic')}\n"
+ f"Объём: {record.get('page_plan', '—')}\n"
+ f"Опции: {', '.join(upsell_titles) if upsell_titles else 'нет'}\n"
+ f"Статус: {record.get('status', 'в работе')}\n"
+ f"Сумма: {final_amount} ₽ (база {base_amount} ₽, скидка {discount_amount} ₽)\n"
+ f"Файлы: {attachments_count}\n"
+ )
+ text = "\n".join(lines)
+ await update.callback_query.edit_message_text(
+ text, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data="admin:menu")]])
+ )
+ return STATE_NAVIGATION
+
+
+async def admin_request_pricing_mode(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ context.user_data["admin_state"] = "pricing_mode"
+ await update.callback_query.edit_message_text(
+ f"Текущий режим: {store.get_pricing_mode()}. Введите hard или light:",
+ )
+ return STATE_ADMIN
+
+
+async def admin_export(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ path = store.export_orders()
+ if not path:
+ await update.callback_query.edit_message_text(
+ "Нет данных для экспорта.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data="admin:menu")]])
+ )
+ return STATE_NAVIGATION
+ await update.callback_query.edit_message_text("Отправляю файл с заказами…")
+ try:
+ owner_chat_id = store.get_owner_chat_id()
+ if owner_chat_id:
+ await context.bot.send_document(owner_chat_id, document=path.open("rb"))
+ finally:
+ path.unlink(missing_ok=True)
+ await update.callback_query.edit_message_text(
+ "Экспорт заказов успешно отправлен ✅",
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data="admin:menu")]]),
+ )
+ return STATE_NAVIGATION
+
+
+async def admin_show_bonuses(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ stats = store.list_referral_stats()
+ if not stats:
+ text = "Бонусов пока нет. Делитесь реферальной ссылкой из профиля клиентов."
+ else:
+ lines = [
+ f"{entry['user_id']}: приглашений {entry['count']}, бонусов {entry['bonus']} ₽"
+ for entry in stats[:10]
+ ]
+ text = "\n".join(lines)
+ text += f"\n\nБонус за реферала: {REFERRAL_BONUS_AMOUNT} ₽."
+ await update.callback_query.edit_message_text(
+ text,
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data="admin:menu")]]),
+ )
+ return STATE_NAVIGATION
+
+
+async def admin_show_logs(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ last_logs = []
+ for user_id, logs in store.user_logs.items():
+ if logs:
+ last_logs.append((user_id, logs[-1]))
+ if not last_logs:
+ text = "Логи пока пусты."
+ else:
+ lines = [
+ f"{user_id}: {entry['action']} ({entry['timestamp']})"
+ for user_id, entry in last_logs[-10:]
+ ]
+ text = "\n".join(lines)
+ await update.callback_query.edit_message_text(
+ text, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data="admin:menu")]])
+ )
+ return STATE_NAVIGATION
+
+
+async def admin_list_status_targets(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ records = store.list_recent_orders(limit=12)
+ if not records:
+ await update.callback_query.edit_message_text(
+ "Заказов пока нет.",
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data="admin:menu")]]),
+ )
+ return STATE_NAVIGATION
+ keyboard: List[List[InlineKeyboardButton]] = []
+ for record in records:
+ order_id = int(record.get("order_id", 0))
+ user_id = int(record.get("user_id", 0))
+ name = order_type_name_from_record(record)
+ status_key = str(record.get("status_key", "")) or "new"
+ status_label = record.get("status") or ORDER_STATUS_TITLES.get(status_key, "—")
+ label = f"#{order_id} — {name} ({status_label})"
+ if len(label) > 60:
+ label = label[:57] + "…"
+ callback = f"admin:status_select:{user_id}:{order_id}"
+ keyboard.append([InlineKeyboardButton(label, callback_data=callback)])
+ keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data="admin:menu")])
+ await update.callback_query.edit_message_text(
+ "Выберите заказ, чтобы обновить статус:",
+ reply_markup=InlineKeyboardMarkup(keyboard),
+ )
+ return STATE_NAVIGATION
+
+
+async def admin_select_status(
+ update: Update, context: ContextTypes.DEFAULT_TYPE, user_id: int, order_id: int
+) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ context.user_data["admin_status_target"] = {"user_id": user_id, "order_id": order_id}
+ record = store.get_order(user_id, order_id)
+ if record is None:
+ # Запросили несуществующий заказ, возвращаемся к списку.
+ context.user_data.pop("admin_status_target", None)
+ await update.callback_query.edit_message_text(
+ "Заказ не найден. Возможно, он был удален.",
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data="admin:status_list")]]),
+ )
+ return STATE_NAVIGATION
+ original_status_key = str(record.get("status_key", "new") or "new")
+ original_status_title = record.get("status") or ORDER_STATUS_TITLES.get(original_status_key, "—")
+ order_name = order_type_name_from_record(record)
+ keyboard = [
+ [InlineKeyboardButton(title, callback_data=f"admin:status_apply:{key}")]
+ for key, title in ORDER_STATUS_TITLES.items()
+ ]
+ keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data="admin:status_list")])
+ text = (
+ f"Заказ #{order_id} — {order_name}\n"
+ f"Текущий статус: {original_status_title}\n"
+ "Выберите новый статус:"
+ )
+ await update.callback_query.edit_message_text(
+ text, reply_markup=InlineKeyboardMarkup(keyboard)
+ )
+ return STATE_NAVIGATION
+
+
+async def admin_apply_status(
+ update: Update, context: ContextTypes.DEFAULT_TYPE, status_key: str
+) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ target = context.user_data.get("admin_status_target")
+ if not target:
+ return await admin_list_status_targets(update, context)
+ user_id = int(target.get("user_id", 0))
+ order_id = int(target.get("order_id", 0))
+ status_title = ORDER_STATUS_TITLES.get(status_key, status_key)
+ record = store.update_order_status(user_id, order_id, status_key, status_title)
+ if not record:
+ await update.callback_query.edit_message_text(
+ "Не удалось обновить статус (заказ не найден).",
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data="admin:status_list")]]),
+ )
+ return STATE_NAVIGATION
+ log_user_action(update, f"admin_status:{order_id}:{status_key}")
+ await update.callback_query.edit_message_text(
+ f"Статус заказа #{order_id} обновлен: {status_title}",
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ В меню", callback_data="admin:menu")]]),
+ )
+ try:
+ await context.bot.send_message(
+ user_id,
+ (
+ f"Статус вашего заказа #{order_id}: {status_title}.\n"
+ f"Если нужно обсудить детали — ответьте на это сообщение или пишите {MANAGER_CONTACT_LINK}."
+ ),
+ )
+ except Exception as exc: # pragma: no cover - зависит от Telegram API
+ logger.warning("Could not notify user %s about status change: %s", user_id, exc)
+ context.user_data.pop("admin_status_target", None)
+ return STATE_NAVIGATION
+
+
+async def handle_admin_text(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ if not await ensure_owner_access(update, context):
+ return STATE_NAVIGATION
+ state = context.user_data.pop("admin_state", None)
+ if state == "pricing_mode":
+ choice = update.message.text.strip().lower()
+ if choice not in {"hard", "light"}:
+ context.user_data["admin_state"] = "pricing_mode"
+ await update.message.reply_text("Пожалуйста, введите hard или light.")
+ return STATE_ADMIN
+ store.set_pricing_mode(choice)
+ await update.message.reply_text(f"Режим установлен: {choice}")
+ return await show_admin_menu(update, context)
+ await update.message.reply_text("Команда не распознана. Используйте кнопки в меню.")
+ return STATE_ADMIN
+
+async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ query = update.callback_query
+ await query.answer()
+ data = query.data
+ log_user_action(update, f"callback:{data}")
+ if data == "main:root":
+ return await show_main_menu(update, context)
+ if data == "main:order" or data == "order:list":
+ return await show_order_types(update, context)
+ if data.startswith("order:type:"):
+ key = data.split(":", maxsplit=2)[2]
+ return await show_order_details(update, key)
+ if data.startswith("order:new:"):
+ key = data.split(":", maxsplit=2)[2]
+ return await prompt_order_topic(update, context, key)
+ if data.startswith("order:deadline:"):
+ days = int(data.split(":", maxsplit=2)[2])
+ return await handle_deadline(update, context, days)
+ if data.startswith("order:pages:"):
+ key = data.split(":", maxsplit=2)[2]
+ return await handle_page_selection(update, context, key)
+ if data.startswith("order:upsell:"):
+ key = data.split(":", maxsplit=2)[2]
+ if key:
+ return await toggle_upsell(update, context, key)
+ if data == "order:upsell":
+ return await show_upsell_menu(update, context)
+ if data == "order:summary":
+ return await show_order_summary_step(update, context)
+ if data == "order:req_finish":
+ return await finish_requirements(update, context)
+ if data == "order:confirm":
+ return await confirm_order(update, context)
+ if data == "order:cancel":
+ return await cancel_order(update, context)
+ if data == "main:prices":
+ return await show_price_list(update, context)
+ if data.startswith("prices:detail:"):
+ key = data.split(":", maxsplit=2)[2]
+ return await show_price_detail(update, key)
+ if data == "main:calculator":
+ return await show_calculator(update, context)
+ if data.startswith("calc:type:"):
+ key = data.split(":", maxsplit=2)[2]
+ return await calculator_select_deadline(update, context, key)
+ if data.startswith("calc:deadline:"):
+ days = int(data.split(":", maxsplit=2)[2])
+ return await calculator_select_pages(update, context, days)
+ if data.startswith("calc:pages:"):
+ key = data.split(":", maxsplit=2)[2]
+ return await show_calculation_result(update, context, key)
+ if data == "main:profile" or data == "profile:back":
+ return await show_profile(update, context)
+ if data == "profile:orders":
+ return await show_user_orders(update, context)
+ if data == "profile:feedback":
+ return await request_feedback(update, context)
+ if data == "main:faq":
+ return await show_faq(update, context)
+ if data.startswith("faq:item:"):
+ idx = int(data.split(":", maxsplit=2)[2])
+ return await show_faq_item(update, idx)
+ if data == "admin:menu":
+ return await show_admin_menu(update, context)
+ if data == "admin:stats":
+ return await admin_show_stats(update, context)
+ if data == "admin:orders":
+ return await admin_show_orders(update, context)
+ if data == "admin:pricing":
+ return await admin_request_pricing_mode(update, context)
+ if data == "admin:export":
+ return await admin_export(update, context)
+ if data == "admin:bonuses":
+ return await admin_show_bonuses(update, context)
+ if data == "admin:logs":
+ return await admin_show_logs(update, context)
+ if data == "admin:restart":
+ return await restart_bot(update, context)
+ if data == "admin:status_list":
+ return await admin_list_status_targets(update, context)
+ if data.startswith("admin:status_select:"):
+ try:
+ _, _, user_str, order_str = data.split(":", maxsplit=3)
+ return await admin_select_status(update, context, int(user_str), int(order_str))
+ except (ValueError, IndexError):
+ await update.callback_query.answer("Не удалось распознать заказ", show_alert=True)
+ return STATE_NAVIGATION
+ if data.startswith("admin:status_apply:"):
+ status_key = data.split(":", maxsplit=2)[2]
+ return await admin_apply_status(update, context, status_key)
+ await query.edit_message_text("Команда не распознана. Возвращаюсь в меню.")
+ return await show_main_menu(update, context)
+
+async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ log_user_action(update, "start")
+ user = update.effective_user
+ context.user_data.pop("order_draft", None)
+ promoted_text = ""
+ owner_chat_id = store.get_owner_chat_id()
+ if user and owner_chat_id == 0:
+ store.set_owner_chat_id(user.id, user.username or user.full_name or "")
+ owner_chat_id = user.id
+ promoted_text = (
+ "\n\nВы назначены администратором бота. Используйте /admin для управления заказами."
+ )
+ if context.args:
+ payload = context.args[0]
+ if payload.isdigit():
+ referrer_id = int(payload)
+ if referrer_id != user.id and store.add_referral(referrer_id, user.id):
+ for admin_id in store.get_notification_chat_ids():
+ try:
+ await context.bot.send_message(
+ admin_id,
+ f"Новый реферал: {user.id} (пригласил {referrer_id})",
+ )
+ except Exception as exc: # pragma: no cover
+ logger.error(
+ "Failed to notify admin %s about referral: %s",
+ admin_id,
+ exc,
+ )
+ try:
+ bonus_info = store.get_bonus_info(referrer_id)
+ await context.bot.send_message(
+ referrer_id,
+ (
+ f"🔥 Ваша ссылка сработала! +{REFERRAL_BONUS_AMOUNT} ₽ на бонусный счёт.\n"
+ f"Баланс: {bonus_info.get('balance', 0)} ₽."
+ ),
+ )
+ except Exception as exc: # pragma: no cover
+ logger.warning("Не удалось уведомить реферера %s: %s", referrer_id, exc)
+ greeting = (
+ f"👋 Привет, {html.escape(user.first_name or user.full_name or 'друг')}!\n\n"
+ "Я помогу оформить любую учебную работу: от самостоятельной до магистерской."
+ )
+ if WELCOME_MESSAGE:
+ greeting += f"\n{WELCOME_MESSAGE}"
+ existing_orders = store.get_orders(user.id)
+ if not existing_orders:
+ greeting += "\n🎁 На первый заказ действует скидка 7% (до 3000 ₽)."
+ if promoted_text:
+ greeting += promoted_text
+ if update.message:
+ await update.message.reply_text(greeting)
+ return await show_main_menu(update, context, "Готовы сделать заказ?")
+
+
+async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ text = (
+ "Используйте /start для перехода к главному меню.\n"
+ "Кнопка '📝 Сделать заказ' запустит пошаговое оформление с возможностью прикреплять файлы.\n"
+ "Команды /done и /skip доступны на этапе требований.\n"
+ "Команда /cancel завершит текущий процесс."
+ )
+ await update.message.reply_text(text)
+
+
+async def cancel_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
+ return await cancel_order(update, context)
+
+
+async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
+ logger.exception("Unhandled error: %s", context.error)
+ for admin_chat_id in store.get_notification_chat_ids():
+ try:
+ await context.bot.send_message(admin_chat_id, f"Ошибка в боте: {context.error}")
+ except Exception: # pragma: no cover
+ pass
+
+
+def build_application():
+ application = ApplicationBuilder().token(TELEGRAM_BOT_TOKEN).build()
+ conv_handler = ConversationHandler(
+ entry_points=[CommandHandler("start", start)],
+ states={
+ STATE_NAVIGATION: [CallbackQueryHandler(handle_callback)],
+ STATE_ORDER_TOPIC: [
+ CallbackQueryHandler(handle_callback),
+ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_topic),
+ ],
+ STATE_ORDER_PAGES: [CallbackQueryHandler(handle_callback)],
+ STATE_ORDER_REQUIREMENTS: [
+ CallbackQueryHandler(handle_callback),
+ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_requirements),
+ MessageHandler(filters.ATTACHMENT, handle_requirement_attachment),
+ CommandHandler("skip", skip_requirements),
+ CommandHandler("done", finish_requirements),
+ ],
+ STATE_FEEDBACK: [
+ CallbackQueryHandler(handle_callback),
+ MessageHandler(filters.TEXT & ~filters.COMMAND, receive_feedback),
+ ],
+ STATE_ADMIN: [
+ CallbackQueryHandler(handle_callback),
+ MessageHandler(filters.TEXT & ~filters.COMMAND, handle_admin_text),
+ ],
+ },
+ fallbacks=[CommandHandler("cancel", cancel_command), CommandHandler("start", start)],
+ allow_reentry=True,
+ )
+ application.add_handler(conv_handler)
+ application.add_handler(CommandHandler("help", help_command))
+ application.add_handler(CommandHandler("admin", admin_command))
+ application.add_handler(CommandHandler("restart_bot", restart_bot))
+ application.add_error_handler(error_handler)
+ return application
+
+
+def main() -> None:
+ application = build_application()
+ application.run_polling(drop_pending_updates=True)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/bot1.py b/bot1.py
deleted file mode 100644
index 85c1a8a..0000000
--- a/bot1.py
+++ /dev/null
@@ -1,1908 +0,0 @@
-import os
-import sys
-import logging
-import json
-from datetime import datetime, timedelta
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import (
- ApplicationBuilder, CommandHandler, ContextTypes,
- CallbackQueryHandler, MessageHandler, filters, ConversationHandler
-)
-from telegram.constants import ParseMode
-from dotenv import load_dotenv
-import re
-
-# Загрузка переменных окружения
-load_dotenv()
-
-TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
-ADMIN_CHAT_ID = os.getenv('ADMIN_CHAT_ID')
-
-# Проверка обязательных переменных окружения
-if not TELEGRAM_BOT_TOKEN:
- print("ОШИБКА: Не найден TELEGRAM_BOT_TOKEN в переменных окружения!")
- print("Создайте файл .env и добавьте в него:")
- print("TELEGRAM_BOT_TOKEN=ваш_токен_бота")
- print("ADMIN_CHAT_ID=ваш_telegram_id")
- sys.exit(1)
-
-if not ADMIN_CHAT_ID:
- print("ОШИБКА: Не найден ADMIN_CHAT_ID в переменных окружения!")
- print("Добавьте в файл .env строку:")
- print("ADMIN_CHAT_ID=ваш_telegram_id")
- sys.exit(1)
-
-try:
- ADMIN_CHAT_ID = int(ADMIN_CHAT_ID)
-except ValueError:
- print(f"ОШИБКА: ADMIN_CHAT_ID должен быть числом, получено: {ADMIN_CHAT_ID}")
- sys.exit(1)
-
-# Настройка базовых директорий
-# Используем относительные пути от текущей директории
-SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
-BASE_DIR = os.path.join(SCRIPT_DIR, "gipsr_bot", "clients")
-DATA_DIR = os.path.join(SCRIPT_DIR, "gipsr_bot", "data")
-LOGS_DIR = os.path.join(SCRIPT_DIR, "gipsr_bot", "logs")
-ASSETS_DIR = os.path.join(SCRIPT_DIR, "gipsr_bot", "assets")
-
-# Настройка логирования
-logging.basicConfig(
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- level=logging.INFO
-)
-logger = logging.getLogger(__name__)
-
-# Создание необходимых директорий
-for directory in [BASE_DIR, os.path.join(BASE_DIR, 'feedbacks'), DATA_DIR, LOGS_DIR, ASSETS_DIR]:
- try:
- os.makedirs(directory, exist_ok=True)
- logger.info(f"Создана директория: {directory}")
- except Exception as e:
- logger.error(f"Ошибка при создании директории {directory}: {e}")
-
-# Логирование в файл
-file_handler = logging.FileHandler(os.path.join(LOGS_DIR, 'bot.log'))
-file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
-logger.addHandler(file_handler)
-
-# Файлы для хранения данных
-PRICES_FILE = os.path.join(DATA_DIR, 'prices.json')
-REFERRALS_FILE = os.path.join(DATA_DIR, 'referrals.json')
-ORDERS_FILE = os.path.join(DATA_DIR, 'orders.json')
-FEEDBACKS_FILE = os.path.join(DATA_DIR, 'feedbacks.json')
-
-# Типы заказов
-ORDER_TYPES = {
- 'self': {
- 'name': 'Самостоятельная работа',
- 'icon': '📝',
- 'description': 'Идеально для небольших заданий, эссе, контрольных работ. Выполняется быстро и качественно.',
- 'details': 'Самостоятельные работы включают небольшие эссе, задания по предметам, тесты и контрольные. Обычно объемом до 20 страниц.',
- 'examples': ['Эссе по философии', 'Контрольная по экономике', 'Реферат по истории']
- },
- 'course_theory': {
- 'name': 'Курсовая работа (теоретическая)',
- 'icon': '📘',
- 'description': 'Теоретическая курсовая работа с глубоким анализом литературы и источников.',
- 'details': 'Теоретическая курсовая включает анализ научной литературы, построение теоретических моделей без эмпирической части.',
- 'examples': ['Анализ теоретических подходов к социальной работе', 'Обзор методологий исследования', 'Теоретические основы психологии развития']
- },
- 'course_empirical': {
- 'name': 'Курсовая работа (теория + эмпирика)',
- 'icon': '📊',
- 'description': 'Курсовая с эмпирической частью, анализом данных и практическими рекомендациями.',
- 'details': 'Эмпирическая курсовая работа включает как теоретическую часть, так и практическое исследование с анализом полученных данных.',
- 'examples': ['Исследование социальной адаптации мигрантов', 'Анализ эффективности методов психотерапии', 'Опрос по уровню удовлетворенности клиентов']
- },
- 'vkr': {
- 'name': 'ВКР',
- 'icon': '🎓',
- 'description': 'Выпускная квалификационная работа для завершения обучения с полным циклом исследования.',
- 'details': 'ВКР - это итоговая научная работа для получения диплома бакалавра. Включает глубокий теоретический анализ и объемное эмпирическое исследование.',
- 'examples': ['Комплексное исследование социально-психологической адаптации', 'Проектирование системы социальной поддержки', 'Анализ эффективности социальных программ']
- },
- 'master': {
- 'name': 'Магистерская диссертация',
- 'icon': '🔍',
- 'description': 'Глубокое научное исследование для магистратуры с инновационным подходом.',
- 'details': 'Магистерская диссертация - это научное исследование высокого уровня, демонстрирующее способность к самостоятельной научной работе.',
- 'examples': ['Разработка методологии оценки эффективности социальных программ', 'Комплексное исследование психологических аспектов', 'Проектирование инновационных методов работы']
- }
-}
-
-# Режимы ценообразования
-PRICING_MODES = {
- 'hard': {
- 'name': 'Hard Mode',
- 'icon': '💲',
- 'description': '<7 дней: +30%, 8-14 дней: +15%, >14 дней: базовая цена.'
- },
- 'light': {
- 'name': 'Light Mode',
- 'icon': '💰',
- 'description': '<3 дней: +30%, >=7 дней: базовая цена.'
- }
-}
-
-# FAQ
-FAQ_ITEMS = [
- {
- 'question': 'Как сделать заказ?',
- 'answer': 'Для заказа работы выберите пункт "📝 Сделать заказ" в главном меню. Затем следуйте инструкциям: выберите тип работы, введите тему, укажите дедлайн и дополнительные требования. После подтверждения заказа с вами свяжется менеджер для уточнения деталей.'
- },
- {
- 'question': 'Как рассчитывается стоимость?',
- 'answer': 'Стоимость зависит от трех основных факторов: тип работы, срочность выполнения и сложность темы. Базовые цены указаны в прайс-листе. При срочном выполнении (менее 7 дней) применяется наценка 15-30%. Для получения точной стоимости воспользуйтесь калькулятором цен в разделе "Прайс-лист".'
- },
- {
- 'question': 'Как работает реферальная программа?',
- 'answer': 'В разделе "Мой профиль" вы найдете вашу персональную реферальную ссылку. Поделитесь ею с друзьями, и когда они закажут работу по вашей ссылке, вы получите бонус в размере 5% от стоимости их заказа. Бонусы можно использовать для оплаты своих заказов.'
- },
- {
- 'question': 'Какие гарантии качества работы?',
- 'answer': 'Мы гарантируем высокое качество всех работ. Каждая работа проходит проверку на плагиат и соответствие требованиям. После выполнения работы предоставляется бесплатное внесение правок в течение 14 дней. В случае необходимости мы обеспечиваем полную поддержку до успешной защиты работы.'
- },
- {
- 'question': 'Могу ли я получить скидку?',
- 'answer': 'Да, у нас действует система скидок: 5% при заказе от 10 000 руб., 10% при заказе от 20 000 руб. Постоянные клиенты получают персональные скидки до 15%. Также действуют сезонные акции и специальные предложения, о которых вы можете узнать у менеджера.'
- },
- {
- 'question': 'Как отслеживать процесс выполнения заказа?',
- 'answer': 'В разделе "Мой профиль" вы можете видеть статус всех ваших заказов. Также наш менеджер будет регулярно информировать вас о ходе выполнения работы. Вы всегда можете запросить промежуточные результаты для оценки работы.'
- }
-]
-
-# Глобальные переменные
-current_pricing_mode = 'light'
-user_orders = {}
-user_ids = set()
-user_feedbacks = {}
-
-# Состояния для ConversationHandler
-(
- START,
- SELECT_MAIN_MENU,
- SELECT_ORDER_TYPE,
- VIEW_ORDER_DETAILS,
- INPUT_TOPIC,
- INPUT_DEADLINE,
- INPUT_REQUIREMENTS,
- CALCULATE_PRICE,
- CONFIRM_ORDER,
- ADMIN_MENU,
- PROFILE_MENU,
- SHOW_PRICE_LIST,
- PRICE_CALCULATOR,
- SHOW_FAQ,
- FAQ_DETAILS,
- SHOW_ORDERS,
- LEAVE_FEEDBACK,
- INPUT_FEEDBACK
-) = range(18)
-
-# Функции для работы с ценами
-def load_prices():
- try:
- if os.path.exists(PRICES_FILE):
- with open(PRICES_FILE, 'r', encoding='utf-8') as f:
- return json.load(f)
- default_prices = {
- 'self': {'base': 1500, 'min': 1000, 'max': 3000},
- 'course_theory': {'base': 7000, 'min': 5000, 'max': 10000},
- 'course_empirical': {'base': 11000, 'min': 8000, 'max': 15000},
- 'vkr': {'base': 32000, 'min': 25000, 'max': 45000},
- 'master': {'base': 42000, 'min': 35000, 'max': 60000}
- }
- save_prices(default_prices)
- return default_prices
- except Exception as e:
- logger.error(f"Ошибка при загрузке цен: {e}")
- return {
- 'self': {'base': 1500, 'min': 1000, 'max': 3000},
- 'course_theory': {'base': 7000, 'min': 5000, 'max': 10000},
- 'course_empirical': {'base': 11000, 'min': 8000, 'max': 15000},
- 'vkr': {'base': 32000, 'min': 25000, 'max': 45000},
- 'master': {'base': 42000, 'min': 35000, 'max': 60000}
- }
-
-def save_prices(prices):
- try:
- with open(PRICES_FILE, 'w', encoding='utf-8') as f:
- json.dump(prices, f, ensure_ascii=False, indent=4)
- except Exception as e:
- logger.error(f"Ошибка при сохранении цен: {e}")
-
-PRICES = load_prices()
-
-# Функции для работы с рефералами
-def load_referrals():
- try:
- if os.path.exists(REFERRALS_FILE):
- with open(REFERRALS_FILE, 'r', encoding='utf-8') as f:
- return json.load(f)
- return {}
- except Exception as e:
- logger.error(f"Ошибка при загрузке рефералов: {e}")
- return {}
-
-def save_referrals(data):
- try:
- with open(REFERRALS_FILE, 'w', encoding='utf-8') as f:
- json.dump(data, f, ensure_ascii=False, indent=4)
- except Exception as e:
- logger.error(f"Ошибка при сохранении рефералов: {e}")
-
-referrals = load_referrals()
-
-# Функции для работы с заказами
-def load_orders():
- try:
- if os.path.exists(ORDERS_FILE):
- with open(ORDERS_FILE, 'r', encoding='utf-8') as f:
- return json.load(f)
- return {}
- except Exception as e:
- logger.error(f"Ошибка при загрузке заказов: {e}")
- return {}
-
-def save_orders(data):
- try:
- with open(ORDERS_FILE, 'w', encoding='utf-8') as f:
- json.dump(data, f, ensure_ascii=False, indent=4)
- except Exception as e:
- logger.error(f"Ошибка при сохранении заказов: {e}")
-
-# Функции для работы с отзывами
-def load_feedbacks():
- try:
- if os.path.exists(FEEDBACKS_FILE):
- with open(FEEDBACKS_FILE, 'r', encoding='utf-8') as f:
- return json.load(f)
- return {}
- except Exception as e:
- logger.error(f"Ошибка при загрузке отзывов: {e}")
- return {}
-
-def save_feedbacks(data):
- try:
- with open(FEEDBACKS_FILE, 'w', encoding='utf-8') as f:
- json.dump(data, f, ensure_ascii=False, indent=4)
- except Exception as e:
- logger.error(f"Ошибка при сохранении отзывов: {e}")
-
-user_feedbacks = load_feedbacks()
-
-# Расчет цены
-def calculate_price(order_type_key, days_left, complexity_factor=1.0):
- try:
- prices = PRICES.get(order_type_key, {'base': 0})
- base_price = prices.get('base', 0)
- base_price = int(base_price * complexity_factor)
-
- if current_pricing_mode == 'hard':
- if days_left <= 7:
- return int(base_price * 1.3)
- elif 8 <= days_left <= 14:
- return int(base_price * 1.15)
- else:
- return base_price
- else:
- if days_left <= 3:
- return int(base_price * 1.3)
- elif 4 <= days_left <= 6:
- return int(base_price * 1.15)
- else:
- return base_price
- except Exception as e:
- logger.error(f"Ошибка при расчете цены: {e}")
- return PRICES.get(order_type_key, {'base': 0}).get('base', 0)
-
-# Стилизованное сообщение
-def generate_styled_message(title, content, footer=None):
- message = f"*{title}*\n\n{content}"
- if footer:
- message += f"\n\n_{footer}_"
- return message
-
-# Обработчик ошибок
-async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- logger.error(f"Ошибка при обработке обновления: {context.error}")
- try:
- error_message = f"⚠️ *Ошибка в боте*\n\nError: {context.error}\n"
- if update and update.effective_user:
- error_message += f"User: @{update.effective_user.username} (ID: {update.effective_user.id})\n"
- if update and update.effective_message:
- error_message += f"Message: {update.effective_message.text[:50]}...\n"
-
- await context.bot.send_message(
- chat_id=ADMIN_CHAT_ID,
- text=error_message,
- parse_mode=ParseMode.MARKDOWN
- )
- except Exception as e:
- logger.error(f"Не удалось отправить сообщение об ошибке админу: {e}")
-
- try:
- if update and update.effective_chat:
- await context.bot.send_message(
- chat_id=update.effective_chat.id,
- text="Извините, произошла ошибка. Попробуйте снова или используйте /start."
- )
- except:
- pass
-
-# Команда /start
-async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
- user = update.effective_user
- user_ids.add(user.id)
-
- # Отправляем уведомление админу о новом пользователе
- try:
- await context.bot.send_message(
- chat_id=ADMIN_CHAT_ID,
- text=f"👤 *Новый пользователь в боте*\n\n"
- f"Имя: {user.first_name} {user.last_name or ''}\n"
- f"Username: @{user.username or 'отсутствует'}\n"
- f"ID: `{user.id}`\n"
- f"Время: {datetime.now().strftime('%d.%m.%Y %H:%M')}",
- parse_mode=ParseMode.MARKDOWN
- )
- except Exception as e:
- logger.error(f"Ошибка уведомления админа о новом пользователе: {e}")
-
- text = update.message.text
- args = text.split()
-
- try:
- bot = await context.bot.get_me()
- bot_username = bot.username
- except Exception as e:
- logger.error(f"Ошибка получения данных бота: {e}")
- bot_username = "Kladovaya_GIPSR_bot"
-
- if len(args) > 1 and args[1].isdigit():
- referrer_id = int(args[1])
- if referrer_id != user.id:
- referrals.setdefault(str(referrer_id), [])
- if user.id not in referrals[str(referrer_id)]:
- referrals[str(referrer_id)].append(user.id)
- save_referrals(referrals)
- try:
- await context.bot.send_message(
- chat_id=referrer_id,
- text=f"🎉 Ваш реферал {user.first_name} (@{user.username or 'без имени'}) присоединился!"
- )
- except Exception as e:
- logger.error(f"Ошибка отправки уведомления рефереру: {e}")
-
- ref_link = f"https://t.me/{bot_username}?start={user.id}"
- context.user_data['ref_link'] = ref_link
-
- welcome_message = (
- f"🎓 *Добро пожаловать в Кладовую ГИПСР, {user.first_name}!*\n\n"
- f"✨ *Что мы предлагаем:*\n"
- f"• Качественные академические работы любой сложности\n"
- f"• Гарантия уникальности от 75%\n"
- f"• Бесплатные правки в течение 14 дней\n"
- f"• Поддержка до успешной защиты\n"
- f"• Конфиденциальность и безопасность\n\n"
- f"💡 *Как сделать заказ:*\n"
- f"1️⃣ Нажмите «Сделать заказ»\n"
- f"2️⃣ Выберите тип работы\n"
- f"3️⃣ Укажите тему и срок\n"
- f"4️⃣ Получите точную стоимость\n"
- f"5️⃣ Менеджер свяжется с вами\n\n"
- f"🎁 *Бонусы:*\n"
- f"• Скидка 10% на первый заказ\n"
- f"• Реферальная программа - 5% от заказов друзей\n"
- f"• Накопительные скидки для постоянных клиентов\n\n"
- f"📱 *Выберите действие:*"
- )
- return await main_menu(update, context, welcome_message)
-
-# Команда /help
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
- text = (
- "📋 *Основные команды:*\n\n"
- "/start - Главное меню\n"
- "/help - Список команд\n"
- "/order - Новый заказ\n"
- "/profile - Личный кабинет\n"
- "/price - Прайс-лист\n"
- "/faq - Часто задаваемые вопросы\n"
- "/admin - Панель администратора\n\n"
- "Свяжитесь с менеджером через главное меню при необходимости."
- )
- await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
-
-# Команда /price
-async def price_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
- user_id = str(update.effective_user.id)
- orders_count = len(user_orders.get(user_id, []))
- discount = 0
- if orders_count >= 5:
- discount = 15
- elif orders_count >= 3:
- discount = 10
- elif orders_count >= 1:
- discount = 5
-
- text = "🎓 *ПРАЙС-ЛИСТ КЛАДОВОЙ ГИПСР*\n\n"
-
- if discount > 0:
- text += f"🎉 *Ваша персональная скидка: {discount}%*\n\n"
-
- text += "📍 *Актуальные цены на 2024 год:*\n\n"
-
- for key, val in PRICES.items():
- order_type = ORDER_TYPES.get(key, {})
- base_price = val.get('base', 0)
- if discount > 0:
- discounted_price = int(base_price * (1 - discount/100))
- text += f"{order_type.get('icon', '')} *{order_type.get('name', key)}*\n"
- text += f" ├ ~{base_price:,}~ *{discounted_price:,} руб.*\n"
- text += f" └ 🎯 Срок: от 3 дней\n\n"
- else:
- text += f"{order_type.get('icon', '')} *{order_type.get('name', key)}*\n"
- text += f" ├ 💰 *{base_price:,} руб.*\n"
- text += f" └ 🎯 Срок: от 3 дней\n\n"
-
- text += "🎁 *Специальные предложения:*\n"
- text += "• Скидка 10% на первый заказ\n"
- text += "• Приведи друга - получи 500₽ бонус\n"
- text += "• Заказ от 2 работ = скидка 15%\n"
-
- keyboard = [
- [InlineKeyboardButton("🧮 Рассчитать точную цену", callback_data='price_calculator')],
- [InlineKeyboardButton("🎯 Быстрый заказ", callback_data='make_order')],
- [InlineKeyboardButton("🏠 Главное меню", callback_data='back_to_main')]
- ]
- await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return SHOW_PRICE_LIST
-
-# Команда /faq
-async def faq_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
- text = "❓ *Часто задаваемые вопросы*\n\nВыберите вопрос:\n"
- keyboard = [[InlineKeyboardButton(f"{idx+1}. {item['question']}", callback_data=f'faq_{idx}')] for idx, item in enumerate(FAQ_ITEMS)]
- keyboard.append([InlineKeyboardButton("🏠 Главное меню", callback_data='back_to_main')])
- await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return SHOW_FAQ
-
-# Команда /order
-async def order_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
- return await select_order_type(update, context)
-
-# Команда /profile
-async def profile_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
- user = update.effective_user
- orders = user_orders.get(str(user.id), []) or load_orders().get(str(user.id), [])
-
- try:
- bot = await context.bot.get_me()
- bot_username = bot.username
- except Exception as e:
- logger.error(f"Ошибка получения данных бота: {e}")
- bot_username = "Kladovaya_GIPSR_bot"
-
- ref_link = f"https://t.me/{bot_username}?start={user.id}"
- context.user_data['ref_link'] = ref_link
-
- ref_count = len(referrals.get(str(user.id), []))
- bonus = sum(int(order.get('price', 0) * 0.05) for ref_id in referrals.get(str(user.id), []) for order in user_orders.get(str(ref_id), []))
-
- text = (
- f"👤 *Личный кабинет*\n\n"
- f"*Имя:* {user.first_name}\n"
- f"*Username:* @{user.username if user.username else 'отсутствует'}\n"
- f"*ID:* `{user.id}`\n\n"
- f"*Реферальная программа:*\n"
- f"- Приглашено: {ref_count}\n"
- f"- Бонус: {bonus} руб.\n"
- f"- Ссылка: `{ref_link}`\n\n"
- )
-
- if orders:
- text += "*Ваши заказы:*\n"
- recent_orders = sorted(orders, key=lambda x: x.get('date', ''), reverse=True)[:3]
- for o in recent_orders:
- text += f"- Заказ #{o.get('order_id', 'N/A')}: {o.get('type')} | Статус: {o.get('status')}\n"
- if len(orders) > 3:
- text += f"\n_...и еще {len(orders) - 3} заказов_\n"
- else:
- text += "У вас пока нет заказов."
-
- keyboard = [
- [InlineKeyboardButton("📋 Все заказы", callback_data='show_all_orders')],
- [InlineKeyboardButton("📝 Новый заказ", callback_data='make_order')],
- [InlineKeyboardButton("✍️ Оставить отзыв", callback_data='leave_feedback')],
- [InlineKeyboardButton("🏠 Главное меню", callback_data='back_to_main')]
- ]
-
- await update.message.reply_text(
- text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return PROFILE_MENU
-
-# Главное меню
-async def main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE, custom_message=None):
- try:
- user = update.effective_user
- keyboard = [
- [InlineKeyboardButton("📝 Сделать заказ", callback_data='make_order')],
- [InlineKeyboardButton("💲 Прайс-лист", callback_data='price_list'),
- InlineKeyboardButton("👤 Мой профиль", callback_data='profile')],
- [InlineKeyboardButton("❓ FAQ", callback_data='faq')],
- [InlineKeyboardButton("📞 Администратор", url='https://t.me/Thisissaymoon')]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
- text = custom_message or f"👋 *Привет, {user.first_name}!*\n\nВыберите раздел:"
-
- if update.callback_query:
- await update.callback_query.message.edit_text(text, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN)
- else:
- await update.message.reply_text(text, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN)
- return SELECT_MAIN_MENU
- except Exception as e:
- logger.error(f"Ошибка в main_menu: {e}")
- return SELECT_MAIN_MENU
-
-# Обработчик главного меню
-async def main_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- choice = query.data
-
- if choice == 'make_order':
- return await select_order_type(update, context)
- elif choice == 'price_list':
- return await show_price_list(update, context)
- elif choice == 'profile':
- return await show_profile(update, context)
- elif choice == 'faq':
- return await show_faq(update, context)
- elif choice == 'back_to_main':
- return await main_menu(update, context)
-
- await query.message.reply_text("Неизвестный выбор.")
- return SELECT_MAIN_MENU
-
-# Прайс-лист
-async def show_price_list(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
-
- # Подсчет скидки для постоянного клиента
- user_id = str(update.effective_user.id)
- orders_count = len(user_orders.get(user_id, []))
- discount = 0
- if orders_count >= 5:
- discount = 15
- elif orders_count >= 3:
- discount = 10
- elif orders_count >= 1:
- discount = 5
-
- text = "🎓 *ПРАЙС-ЛИСТ КЛАДОВОЙ ГИПСР*\n\n"
-
- if discount > 0:
- text += f"🎉 *Ваша персональная скидка: {discount}%*\n\n"
-
- text += "📍 *Актуальные цены на 2024 год:*\n\n"
-
- for key, val in PRICES.items():
- order_type = ORDER_TYPES.get(key, {})
- base_price = val.get('base', 0)
- if discount > 0:
- discounted_price = int(base_price * (1 - discount/100))
- text += f"{order_type.get('icon', '')} *{order_type.get('name', key)}*\n"
- text += f" ├ ~{base_price:,}~ *{discounted_price:,} руб.*\n"
- text += f" └ 🎯 Срок: от 3 дней\n\n"
- else:
- text += f"{order_type.get('icon', '')} *{order_type.get('name', key)}*\n"
- text += f" ├ 💰 *{base_price:,} руб.*\n"
- text += f" └ 🎯 Срок: от 3 дней\n\n"
-
- text += "🎁 *Специальные предложения:*\n"
- text += "• Скидка 10% на первый заказ\n"
- text += "• Приведи друга - получи 500₽ бонус\n"
- text += "• Заказ от 2 работ = скидка 15%\n\n"
- text += "🔥 *Почему выбирают нас:*\n"
- text += "• 100% гарантия сдачи\n"
- text += "• Бесплатная доработка\n"
- text += "• Антиплагиат от 75%\n"
-
- keyboard = [
- [InlineKeyboardButton("🧮 Рассчитать точную цену", callback_data='price_calculator')],
- [InlineKeyboardButton("🎯 Быстрый заказ", callback_data='make_order')],
- [InlineKeyboardButton("🏠 Главное меню", callback_data='back_to_main')]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return SHOW_PRICE_LIST
-
-# Калькулятор стоимости
-async def price_calculator(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
-
- # Отправляем уведомление админу
- user = update.effective_user
- try:
- await context.bot.send_message(
- chat_id=ADMIN_CHAT_ID,
- text=f"🧮 *Пользователь открыл калькулятор*\n\n"
- f"Имя: {user.first_name}\n"
- f"Username: @{user.username or 'отсутствует'}\n"
- f"ID: `{user.id}`\n"
- f"Время: {datetime.now().strftime('%H:%M')}",
- parse_mode=ParseMode.MARKDOWN
- )
- except:
- pass
-
- text = (
- "🎆 *ИНТЕРАКТИВНЫЙ КАЛЬКУЛЯТОР ЦЕН*\n\n"
- "🎁 *СПЕЦИАЛЬНОЕ ПРЕДЛОЖЕНИЕ!*\n"
- "При заказе сегодня - скидка 10%!\n\n"
- "👇 *Шаг 1: Выберите тип работы*\n\n"
- "Какую работу вы хотите заказать?"
- )
-
- keyboard = []
- for key, val in ORDER_TYPES.items():
- price = PRICES[key]['base']
- # Показываем цену прямо в кнопке
- button_text = f"{val['icon']} {val['name']} | {price:,}₽"
- keyboard.append([InlineKeyboardButton(button_text, callback_data=f'calc_{key}')])
-
- keyboard.append([InlineKeyboardButton("🎁 Пакетное предложение (2+ работы)", callback_data='calc_package')])
- keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data='back_to_price')])
-
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return PRICE_CALCULATOR
-
-# Расчет цены в калькуляторе
-async def calculate_price_in_calculator(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
-
- if query.data == 'calc_package':
- # Пакетное предложение
- text = (
- "🎁 *ПАКЕТНОЕ ПРЕДЛОЖЕНИЕ*\n\n"
- "🔥 *Закажите 2+ работ и получите:*\n\n"
- "✅ Скидка 15% на все работы\n"
- "✅ Приоритетное выполнение\n"
- "✅ Один менеджер на весь заказ\n\n"
- "💰 *Примеры экономии:*\n\n"
- "📚 Курсовая + Самостоятельная:\n"
- "~12,500₽~ → *10,625₽* (экономия 1,875₽)\n\n"
- "🎓 Курсовая + ВКР:\n"
- "~43,000₽~ → *36,550₽* (экономия 6,450₽)\n\n"
- "🎆 Полный комплект (все 5 типов):\n"
- "~93,500₽~ → *79,475₽* (экономия 14,025₽!)\n\n"
- "📞 *Оформить пакет?*"
- )
- keyboard = [
- [InlineKeyboardButton("🚀 Оформить пакет", callback_data='make_order')],
- [InlineKeyboardButton("🔄 К выбору работ", callback_data='price_calculator')],
- [InlineKeyboardButton("⬅️ Назад", callback_data='back_to_price')]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return PRICE_CALCULATOR
-
- order_type_key = query.data.replace('calc_', '')
- if order_type_key not in ORDER_TYPES:
- await query.message.reply_text("Неизвестный тип работы.")
- return PRICE_CALCULATOR
-
- order_type_info = ORDER_TYPES[order_type_key]
- price_info = PRICES[order_type_key]
- context.user_data['calc_type'] = order_type_key
-
- text = (
- f"🎆 *КАЛЬКУЛЯТОР ЦЕН*\n\n"
- f"{order_type_info['icon']} *{order_type_info['name']}*\n\n"
- f"📍 Базовая цена: *{price_info['base']:,} руб.*\n\n"
- f"🕐 *Шаг 2: Выберите срок выполнения*\n\n"
- f"Чем больше времени - тем ниже цена:"
- )
-
- # Создаем кнопки с ценами
- keyboard = []
- deadlines = [(3, '🔴 Срочно (3 дня)'),
- (7, '🟡 Быстро (7 дней)'),
- (14, '🟢 Оптимально (14 дней)'),
- (30, '🔵 Комфортно (30 дней)')]
-
- for days, label in deadlines:
- price = calculate_price(order_type_key, days)
- if days <= 7:
- price_text = f"{label} | {price:,}₽ (+{int((price/price_info['base']-1)*100)}%)"
- else:
- price_text = f"{label} | {price:,}₽"
- keyboard.append([InlineKeyboardButton(price_text, callback_data=f'deadline_{order_type_key}_{days}')])
-
- keyboard.append([InlineKeyboardButton("🔄 Другая работа", callback_data='price_calculator')])
- keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data='back_to_price')])
-
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return PRICE_CALCULATOR
-
-# Обработчик выбора срока в калькуляторе
-async def handle_deadline_selection(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
-
- parts = query.data.split('_')
- order_type_key = parts[1]
- days = int(parts[2])
-
- order_type_info = ORDER_TYPES[order_type_key]
- base_price = PRICES[order_type_key]['base']
- final_price = calculate_price(order_type_key, days)
-
- # Проверяем скидки пользователя
- user_id = str(update.effective_user.id)
- orders_count = len(user_orders.get(user_id, []))
- loyalty_discount = 0
- if orders_count >= 5:
- loyalty_discount = 15
- elif orders_count >= 3:
- loyalty_discount = 10
- elif orders_count >= 1:
- loyalty_discount = 5
-
- # Скидка для первого заказа
- if orders_count == 0:
- loyalty_discount = 10
- discount_text = "Скидка новому клиенту"
- else:
- discount_text = f"Ваша постоянная скидка"
-
- final_price_with_discount = int(final_price * (1 - loyalty_discount/100))
-
- text = (
- f"🎯 *ИТОГОВЫЙ РАСЧЕТ*\n\n"
- f"{order_type_info['icon']} *{order_type_info['name']}*\n"
- f"📅 Срок: {days} дней\n\n"
- f"💰 *Стоимость:*\n"
- f"Базовая цена: {base_price:,} руб.\n"
- )
-
- if days <= 7:
- urgency_percent = int((final_price/base_price - 1) * 100)
- text += f"Срочность (+{urgency_percent}%): {final_price - base_price:,} руб.\n"
-
- text += f"─────────────────\n"
- text += f"Итого: {final_price:,} руб.\n\n"
-
- if loyalty_discount > 0:
- text += f"🎁 *{discount_text}: {loyalty_discount}%*\n"
- text += f"🔥 *ФИНАЛЬНАЯ ЦЕНА: {final_price_with_discount:,} руб.*\n"
- text += f"💵 Вы экономите: {final_price - final_price_with_discount:,} руб.\n\n"
-
- text += (
- "✅ *Что входит в стоимость:*\n"
- "• Полное выполнение работы\n"
- "• Антиплагиат от 75%\n"
- "• Оформление по ГОСТ\n"
- "• Бесплатные правки 14 дней\n"
- "• Поддержка до защиты\n\n"
- "🚀 *Готовы заказать?*"
- )
-
- keyboard = [
- [InlineKeyboardButton("✅ ЗАКАЗАТЬ СЕЙЧАС", callback_data=f'quick_order_{order_type_key}_{days}')],
- [InlineKeyboardButton("💬 Обсудить с менеджером", url='https://t.me/Thisissaymoon')],
- [InlineKeyboardButton("🔄 Изменить параметры", callback_data='price_calculator')],
- [InlineKeyboardButton("🏠 Главное меню", callback_data='back_to_main')]
- ]
-
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
-
- # Отправляем уведомление админу
- try:
- await context.bot.send_message(
- chat_id=ADMIN_CHAT_ID,
- text=f"💸 *Пользователь рассчитал цену*\n\n"
- f"Пользователь: {update.effective_user.first_name}\n"
- f"Username: @{update.effective_user.username or 'отсутствует'}\n"
- f"Работа: {order_type_info['name']}\n"
- f"Срок: {days} дней\n"
- f"Цена: {final_price_with_discount:,} руб.\n"
- f"Время: {datetime.now().strftime('%H:%M')}",
- parse_mode=ParseMode.MARKDOWN
- )
- except:
- pass
-
- return PRICE_CALCULATOR
-
-# Возврат к прайс-листу
-async def back_to_price(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- return await show_price_list(update, context)
-
-# FAQ
-async def show_faq(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- text = "❓ *Часто задаваемые вопросы*\n\nВыберите вопрос:\n"
- keyboard = [[InlineKeyboardButton(f"{idx+1}. {item['question']}", callback_data=f'faq_{idx}')] for idx, item in enumerate(FAQ_ITEMS)]
- keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data='back_to_main')])
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return SHOW_FAQ
-
-# Детали FAQ
-async def show_faq_details(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- faq_idx = int(query.data.replace('faq_', ''))
- if faq_idx < 0 or faq_idx >= len(FAQ_ITEMS):
- await query.message.reply_text("Неизвестный вопрос.")
- return SHOW_FAQ
-
- faq_item = FAQ_ITEMS[faq_idx]
- text = f"❓ *{faq_item['question']}*\n\n{faq_item['answer']}"
- keyboard = [
- [InlineKeyboardButton("⬅️ Назад к FAQ", callback_data='back_to_faq')],
- [InlineKeyboardButton("🏠 Главное меню", callback_data='back_to_main')]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return FAQ_DETAILS
-
-# Возврат к FAQ
-async def back_to_faq(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- return await show_faq(update, context)
-
-# Выбор типа заказа
-async def select_order_type(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query or update
- if query.callback_query:
- await query.answer()
-
- keyboard = [[InlineKeyboardButton(f"{val['icon']} {val['name']}", callback_data=f'type_{key}')] for key, val in ORDER_TYPES.items()]
- keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data='back_to_main')])
- text = "📋 *Выберите тип работы:*\n\nНажмите для подробностей и стоимости:"
-
- if query.callback_query:
- await query.message.edit_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode=ParseMode.MARKDOWN)
- else:
- await query.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode=ParseMode.MARKDOWN)
- return SELECT_ORDER_TYPE
-
-# Обработчик выбора типа заказа
-async def select_order_type_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- callback_data = query.data
-
- if callback_data == 'back_to_main':
- return await main_menu(update, context)
- if callback_data.startswith('type_') or callback_data.startswith('order_'):
- order_type_key = callback_data.replace('type_', '').replace('order_', '')
- return await view_order_details(update, context, order_type_key)
- await query.message.reply_text("Неизвестный тип заказа.")
- return SELECT_ORDER_TYPE
-
-# Детали заказа
-async def view_order_details(update: Update, context: ContextTypes.DEFAULT_TYPE, order_type_key):
- query = update.callback_query
- if order_type_key not in ORDER_TYPES:
- await query.message.reply_text("Неизвестный тип заказа.")
- return SELECT_ORDER_TYPE
-
- order_type_info = ORDER_TYPES[order_type_key]
- price_info = PRICES[order_type_key]
- context.user_data['order_type_key'] = order_type_key
- context.user_data['order_type'] = order_type_info['name']
-
- text = f"*{order_type_info['icon']} {order_type_info['name']}*\n\n{order_type_info['details']}\n\n"
- text += "*Примеры тем:*\n" + "\n".join(f"• {example}" for example in order_type_info['examples'])
- text += f"\n\n*Стоимость:* от {price_info['base']} руб.\n*Срок:* от 3 дней\n\nХотите заказать?"
-
- keyboard = [
- [InlineKeyboardButton("✅ Заказать", callback_data='continue_order')],
- [InlineKeyboardButton("⬅️ Другой тип", callback_data='back_to_order_type')],
- [InlineKeyboardButton("🏠 Главное меню", callback_data='back_to_main')]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return VIEW_ORDER_DETAILS
-
-# Обработчик деталей заказа
-async def order_details_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- choice = query.data
-
- if choice == 'back_to_order_type':
- return await select_order_type(update, context)
- elif choice == 'back_to_main':
- return await main_menu(update, context)
- elif choice == 'continue_order':
- keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data='back_to_order_details')]]
- await query.message.edit_text(
- f"Вы выбрали: *{context.user_data['order_type']}*\n\nВведите тему работы:",
- parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return INPUT_TOPIC
- await query.message.reply_text("Неизвестный выбор.")
- return VIEW_ORDER_DETAILS
-
-# Назад к деталям заказа
-async def back_to_order_details(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- return await view_order_details(update, context, context.user_data['order_type_key'])
-
-# Ввод темы
-async def input_topic(update: Update, context: ContextTypes.DEFAULT_TYPE):
- if update.callback_query:
- query = update.callback_query
- await query.answer()
- if query.data == 'back_to_order_details':
- return await view_order_details(update, context, context.user_data['order_type_key'])
- return
-
- context.user_data['topic'] = update.message.text
- keyboard = [
- [InlineKeyboardButton("3 дня", callback_data='3'), InlineKeyboardButton("7 дней", callback_data='7'), InlineKeyboardButton("14 дней", callback_data='14')],
- [InlineKeyboardButton("21 день", callback_data='21'), InlineKeyboardButton("30 дней", callback_data='30'), InlineKeyboardButton("Другой", callback_data='custom')],
- [InlineKeyboardButton("⬅️ Назад", callback_data='back_to_topic')]
- ]
- await update.message.reply_text(
- "📅 *Выберите срок выполнения:*\n\nЧем больше времени, тем ниже стоимость.",
- parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return INPUT_DEADLINE
-
-# Ввод дедлайна
-async def input_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
-
- if query.data == 'back_to_topic':
- keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data='back_to_order_details')]]
- await query.message.edit_text(
- f"Вы выбрали: *{context.user_data['order_type']}*\n\nВведите тему работы:",
- parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return INPUT_TOPIC
-
- if query.data == 'custom':
- await query.message.edit_text(
- "Введите количество дней (число):",
- reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data='back_to_deadline_select')]])
- )
- return INPUT_DEADLINE
-
- if query.data == 'back_to_deadline_select':
- keyboard = [
- [InlineKeyboardButton("3 дня", callback_data='3'), InlineKeyboardButton("7 дней", callback_data='7'), InlineKeyboardButton("14 дней", callback_data='14')],
- [InlineKeyboardButton("21 день", callback_data='21'), InlineKeyboardButton("30 дней", callback_data='30'), InlineKeyboardButton("Другой", callback_data='custom')],
- [InlineKeyboardButton("⬅️ Назад", callback_data='back_to_topic')]
- ]
- await query.message.edit_text(
- "📅 *Выберите срок выполнения:*\n\nЧем больше времени, тем ниже стоимость.",
- parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return INPUT_DEADLINE
-
- try:
- days = int(query.data)
- deadline_date = datetime.now() + timedelta(days=days)
- context.user_data['deadline'] = deadline_date
- context.user_data['days_left'] = days
- keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data='back_to_deadline')]]
- await query.message.edit_text(
- f"📝 *Требования:*\n\nОпишите требования (объем, структура, источники и т.д.) или напишите 'Нет требований'.",
- parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return INPUT_REQUIREMENTS
- except ValueError:
- await query.message.reply_text("Введите число дней.")
- return INPUT_DEADLINE
-
-# Ввод произвольного дедлайна
-async def input_custom_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE):
- try:
- days = int(update.message.text)
- if days < 1:
- await update.message.reply_text(
- "Введите положительное число дней.",
- reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data='back_to_deadline_select')]])
- )
- return INPUT_DEADLINE
- deadline_date = datetime.now() + timedelta(days=days)
- context.user_data['deadline'] = deadline_date
- context.user_data['days_left'] = days
- keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data='back_to_deadline')]]
- await update.message.reply_text(
- f"📝 *Требования:*\n\nОпишите требования или напишите 'Нет требований'.",
- parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return INPUT_REQUIREMENTS
- except ValueError:
- await update.message.reply_text(
- "Введите число дней.",
- reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Назад", callback_data='back_to_deadline_select')]])
- )
- return INPUT_DEADLINE
-
-# Назад к дедлайну
-async def back_to_deadline(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- keyboard = [
- [InlineKeyboardButton("3 дня", callback_data='3'), InlineKeyboardButton("7 дней", callback_data='7'), InlineKeyboardButton("14 дней", callback_data='14')],
- [InlineKeyboardButton("21 день", callback_data='21'), InlineKeyboardButton("30 дней", callback_data='30'), InlineKeyboardButton("Другой", callback_data='custom')],
- [InlineKeyboardButton("⬅️ Назад", callback_data='back_to_topic')]
- ]
- await query.message.edit_text(
- "📅 *Выберите срок выполнения:*\n\nЧем больше времени, тем ниже стоимость.",
- parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return INPUT_DEADLINE
-
-# Ввод требований
-async def input_requirements(update: Update, context: ContextTypes.DEFAULT_TYPE):
- if update.callback_query:
- query = update.callback_query
- await query.answer()
- if query.data == 'back_to_deadline':
- return await back_to_deadline(update, context)
- return
-
- context.user_data['requirements'] = update.message.text
- return await calculate_price_step(update, context)
-
-# Расчет цены
-async def calculate_price_step(update, context: ContextTypes.DEFAULT_TYPE):
- data = context.user_data
- order_type_key = data.get('order_type_key')
- days_left = data.get('days_left', 7)
- topic = data.get('topic', '')
- requirements = data.get('requirements', '')
- complexity_factor = 1.0 + 0.05 * (len(topic) > 50) + 0.1 * any(term in topic.lower() or term in requirements.lower() for term in ['анализ', 'исследование', 'сравнительный', 'методология', 'эмпирический'])
-
- price = calculate_price(order_type_key, days_left, complexity_factor)
- data['price'] = price
- data['complexity_factor'] = complexity_factor
- deadline = data.get('deadline')
- deadline_str = deadline.strftime('%d.%m.%Y') if deadline else "Не указан"
-
- text = (
- f"📋 *Ваш заказ:*\n\n"
- f"*Тип работы:* {data.get('order_type')}\n"
- f"*Тема:* {data.get('topic')}\n"
- f"*Срок:* {deadline_str} ({days_left} дней)\n"
- f"*Требования:*\n{data.get('requirements', 'Не указаны')}\n\n"
- f"*Стоимость:* {price} руб.\n\n"
- f"Подтвердите заказ:"
- )
-
- keyboard = [
- [InlineKeyboardButton("✅ Подтвердить", callback_data='confirm_order')],
- [InlineKeyboardButton("🔄 Изменить", callback_data='change_order_data')],
- [InlineKeyboardButton("❌ Отменить", callback_data='cancel_order')]
- ]
- if update.callback_query:
- await update.callback_query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- else:
- await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return CALCULATE_PRICE
-
-# Изменение данных заказа
-async def change_order_data(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- keyboard = [
- [InlineKeyboardButton("📝 Тип работы", callback_data='change_type')],
- [InlineKeyboardButton("📋 Тема", callback_data='change_topic')],
- [InlineKeyboardButton("📅 Срок", callback_data='change_deadline')],
- [InlineKeyboardButton("📌 Требования", callback_data='change_requirements')],
- [InlineKeyboardButton("⬅️ Назад", callback_data='back_to_price_calc')]
- ]
- await query.message.edit_text(
- "🔄 *Изменение заказа*\n\nВыберите, что изменить:",
- parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return CALCULATE_PRICE
-
-# Обработка изменения данных
-async def handle_change_data(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- choice = query.data
-
- if choice == 'change_type':
- return await select_order_type(update, context)
- elif choice == 'change_topic':
- keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data='back_to_change_menu')]]
- await query.message.edit_text("Введите новую тему:", parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return INPUT_TOPIC
- elif choice == 'change_deadline':
- keyboard = [
- [InlineKeyboardButton("3 дня", callback_data='3'), InlineKeyboardButton("7 дней", callback_data='7'), InlineKeyboardButton("14 дней", callback_data='14')],
- [InlineKeyboardButton("21 день", callback_data='21'), InlineKeyboardButton("30 дней", callback_data='30'), InlineKeyboardButton("Другой", callback_data='custom')],
- [InlineKeyboardButton("⬅️ Назад", callback_data='back_to_change_menu')]
- ]
- await query.message.edit_text("📅 *Выберите новый срок:*", parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return INPUT_DEADLINE
- elif choice == 'change_requirements':
- keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data='back_to_change_menu')]]
- await query.message.edit_text("📝 *Введите новые требования:*", parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return INPUT_REQUIREMENTS
- elif choice == 'back_to_price_calc':
- return await calculate_price_step(update, context)
- elif choice == 'back_to_change_menu':
- return await change_order_data(update, context)
- return CALCULATE_PRICE
-
-# Назад к расчету цены
-async def back_to_price_calc(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- return await calculate_price_step(update, context)
-
-# Подтверждение заказа
-async def confirm_order(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- data = context.user_data
- user = update.effective_user
- client_name = user.username or f"user_{user.id}"
- order_type = data.get('order_type', 'Неизвестный тип')
-
- client_dir = os.path.join(BASE_DIR, client_name)
- os.makedirs(client_dir, exist_ok=True)
- orders_list = user_orders.get(str(user.id), [])
- order_id = len(orders_list) + 1
- order_path = os.path.join(client_dir, f"order_{order_id}.txt")
-
- order_data = {
- 'order_id': order_id,
- 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'type': order_type,
- 'topic': data.get('topic'),
- 'deadline': data.get('deadline').strftime('%d.%m.%Y') if data.get('deadline') else "Не указан",
- 'days_left': data.get('days_left'),
- 'requirements': data.get('requirements', 'Не указаны'),
- 'price': data.get('price'),
- 'status': 'Новый заказ',
- 'user_id': user.id,
- 'user_name': user.first_name,
- 'user_username': user.username
- }
-
- user_orders.setdefault(str(user.id), []).append(order_data)
- all_orders = load_orders()
- all_orders.setdefault(str(user.id), []).append(order_data)
- save_orders(all_orders)
-
- with open(order_path, 'w', encoding='utf-8') as f:
- f.write(f"Пользователь: {user.first_name} (@{user.username})\n")
- f.write(f"ID: {user.id}\n")
- f.write(f"Тип работы: {order_type}\n")
- f.write(f"Тема: {data.get('topic')}\n")
- f.write(f"Сроки: {order_data['deadline']} ({data.get('days_left')} дней)\n")
- f.write(f"Требования: {data.get('requirements', 'Не указаны')}\n")
- f.write(f"Стоимость: {data.get('price')} руб.\n")
- f.write(f"Статус: {order_data['status']}\n")
-
- # Сохраняем данные заказа в JSON вместо Excel (не требует pandas)
- orders_json_path = os.path.join(DATA_DIR, 'all_orders.json')
- try:
- if os.path.exists(orders_json_path):
- with open(orders_json_path, 'r', encoding='utf-8') as f:
- all_orders_list = json.load(f)
- else:
- all_orders_list = []
-
- order_data_json = {
- 'Дата': order_data['date'],
- 'Пользователь': f"{user.first_name} (@{user.username})",
- 'ID': user.id,
- 'Тип работы': order_type,
- 'Тема': data.get('topic'),
- 'Сроки': order_data['deadline'],
- 'Дней осталось': data.get('days_left'),
- 'Требования': data.get('requirements', 'Не указаны'),
- 'Стоимость': data.get('price'),
- 'Статус': 'Новый заказ'
- }
-
- all_orders_list.append(order_data_json)
-
- with open(orders_json_path, 'w', encoding='utf-8') as f:
- json.dump(all_orders_list, f, ensure_ascii=False, indent=4)
- except Exception as e:
- logger.error(f"Ошибка при сохранении заказа: {e}")
-
- try:
- admin_message = (
- f"🆕 *Новый заказ #{order_id}*\n\n"
- f"*От:* @{user.username} ({user.first_name})\n"
- f"*Тип:* {order_type}\n"
- f"*Тема:* {data.get('topic')}\n"
- f"*Дедлайн:* {order_data['deadline']} ({data.get('days_left')} дней)\n"
- f"*Требования:* {data.get('requirements', 'Не указаны')}\n"
- f"*Цена:* {data.get('price')} руб."
- )
- keyboard = [
- [InlineKeyboardButton("✅ Принять", callback_data=f'admin_accept_{user.id}_{order_id}')],
- [InlineKeyboardButton("❌ Отклонить", callback_data=f'admin_reject_{user.id}_{order_id}')],
- [InlineKeyboardButton("💲 Изменить цену", callback_data=f'admin_change_price_{user.id}_{order_id}')]
- ]
- await context.bot.send_message(
- chat_id=ADMIN_CHAT_ID,
- text=admin_message,
- parse_mode=ParseMode.MARKDOWN,
- reply_markup=InlineKeyboardMarkup(keyboard)
- )
- except Exception as e:
- logger.error(f"Ошибка отправки уведомления админу: {e}")
-
- success_message = (
- f"✅ *Заказ оформлен!*\n\n"
- f"Номер: #{order_id}\n"
- f"Тип: {order_type}\n"
- f"Срок: {order_data['deadline']}\n"
- f"Стоимость: {data.get('price')} руб.\n\n"
- f"Менеджер свяжется с вами для деталей и оплаты."
- )
- keyboard = [
- [InlineKeyboardButton("👤 Профиль", callback_data="profile")],
- [InlineKeyboardButton("🏠 Главное меню", callback_data="back_to_main")]
- ]
- await query.message.edit_text(success_message, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
-
- for key in list(context.user_data.keys()):
- if key != 'ref_link':
- context.user_data.pop(key, None)
- return CONFIRM_ORDER
-
-# Отмена заказа
-async def cancel_order(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- for key in list(context.user_data.keys()):
- if key != 'ref_link':
- context.user_data.pop(key, None)
- await query.message.edit_text(
- "❌ *Заказ отменен*\n\nОформите новый заказ в любое время.",
- parse_mode=ParseMode.MARKDOWN,
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("📝 Новый заказ", callback_data="make_order")],
- [InlineKeyboardButton("🏠 Главное меню", callback_data="back_to_main")]
- ])
- )
- return SELECT_MAIN_MENU
-
-# Профиль
-async def show_profile(update: Update, context: ContextTypes.DEFAULT_TYPE):
- user = update.effective_user
- orders = user_orders.get(str(user.id), []) or load_orders().get(str(user.id), [])
-
- try:
- bot = await context.bot.get_me()
- bot_username = bot.username
- except Exception as e:
- logger.error(f"Ошибка получения данных бота: {e}")
- bot_username = "Kladovaya_GIPSR_bot"
-
- ref_link = f"https://t.me/{bot_username}?start={user.id}"
- context.user_data['ref_link'] = ref_link
- ref_count = len(referrals.get(str(user.id), []))
- bonus = sum(int(order.get('price', 0) * 0.05) for ref_id in referrals.get(str(user.id), []) for order in user_orders.get(str(ref_id), []))
-
- text = (
- f"👤 *Личный кабинет*\n\n"
- f"*Имя:* {user.first_name}\n"
- f"*Username:* @{user.username if user.username else 'отсутствует'}\n"
- f"*ID:* `{user.id}`\n\n"
- f"*Реферальная программа:*\n"
- f"- Приглашено: {ref_count}\n"
- f"- Бонус: {bonus} руб.\n"
- f"- Ссылка: `{ref_link}`\n\n"
- )
-
- if orders:
- text += "*Ваши заказы:*\n"
- recent_orders = sorted(orders, key=lambda x: x.get('date', ''), reverse=True)[:3]
- for o in recent_orders:
- text += f"- Заказ #{o.get('order_id', 'N/A')}: {o.get('type')} | Статус: {o.get('status')}\n"
- if len(orders) > 3:
- text += f"\n_...и еще {len(orders) - 3} заказов_\n"
- else:
- text += "У вас пока нет заказов."
-
- keyboard = [
- [InlineKeyboardButton("📋 Все заказы", callback_data='show_all_orders')],
- [InlineKeyboardButton("📝 Новый заказ", callback_data='make_order')],
- [InlineKeyboardButton("✍️ Оставить отзыв", callback_data='leave_feedback')],
- [InlineKeyboardButton("🏠 Главное меню", callback_data='back_to_main')]
- ]
-
- if update.callback_query:
- await update.callback_query.answer()
- await update.callback_query.message.edit_text(
- text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- else:
- await context.bot.send_message(
- chat_id=user.id, text=text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return PROFILE_MENU
-
-# Все заказы
-async def show_all_orders(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- user = update.effective_user
- orders = user_orders.get(str(user.id), []) or load_orders().get(str(user.id), [])
-
- if not orders:
- text = "У вас пока нет заказов."
- keyboard = [[InlineKeyboardButton("⬅️ Назад к профилю", callback_data='back_to_profile')]]
- await query.message.edit_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
- return SHOW_ORDERS
-
- orders = sorted(orders, key=lambda x: x.get('date', ''), reverse=True)
- text = "📋 *Все ваши заказы:*\n\n"
- for i, order in enumerate(orders):
- text += f"*Заказ #{order.get('order_id', 'N/A')}* ({order.get('date', '')[:10]})\n"
- text += f"Тип: {order.get('type', 'Неизвестный')}\n"
- text += f"Тема: {order.get('topic', 'Не указана')}\n"
- text += f"Статус: {order.get('status', 'Неизвестен')}\n"
- text += f"Цена: {order.get('price', 'Не указана')} руб.\n"
- if i < len(orders) - 1:
- text += "\n----------------------------\n\n"
-
- keyboard = [
- [InlineKeyboardButton("📝 Новый заказ", callback_data='make_order')],
- [InlineKeyboardButton("⬅️ Назад к профилю", callback_data='back_to_profile')]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return SHOW_ORDERS
-
-# Назад к профилю
-async def back_to_profile(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- return await show_profile(update, context)
-
-# Оставить отзыв
-async def leave_feedback(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- text = (
- "✍️ *Оставить отзыв*\n\n"
- "Напишите ваш отзыв о работе с нами в одном сообщении:"
- )
- keyboard = [[InlineKeyboardButton("⬅️ Назад к профилю", callback_data='back_to_profile')]]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return LEAVE_FEEDBACK
-
-# Принять отзыв
-async def input_feedback(update: Update, context: ContextTypes.DEFAULT_TYPE):
- if update.callback_query:
- query = update.callback_query
- await query.answer()
- if query.data == 'back_to_profile':
- return await show_profile(update, context)
- return
-
- user = update.effective_user
- feedback_text = update.message.text
- feedback_data = {
- 'user_id': user.id,
- 'username': user.username,
- 'name': user.first_name,
- 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'text': feedback_text
- }
-
- feedbacks = load_feedbacks()
- feedbacks.setdefault(str(user.id), []).append(feedback_data)
- save_feedbacks(feedbacks)
-
- client_name = user.username or f"user_{user.id}"
- feedback_dir = os.path.join(BASE_DIR, 'feedbacks')
- os.makedirs(feedback_dir, exist_ok=True)
- feedback_path = os.path.join(feedback_dir, f"{client_name}.txt")
- with open(feedback_path, 'a', encoding='utf-8') as f:
- f.write(f"--- Отзыв от {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ---\n{feedback_text}\n\n")
-
- try:
- await context.bot.send_message(
- chat_id=ADMIN_CHAT_ID,
- text=f"📣 *Новый отзыв*\n\nОт: @{user.username} ({user.first_name})\n\n{feedback_text}",
- parse_mode=ParseMode.MARKDOWN
- )
- except Exception as e:
- logger.error(f"Ошибка отправки уведомления админу об отзыве: {e}")
-
- text = "🙏 *Спасибо за отзыв!*\n\nМы ценим ваше мнение."
- keyboard = [
- [InlineKeyboardButton("👤 Профиль", callback_data='back_to_profile')],
- [InlineKeyboardButton("🏠 Главное меню", callback_data='back_to_main')]
- ]
- await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
- return INPUT_FEEDBACK
-
-# Команда /admin
-async def admin_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
- if update.effective_user.id != ADMIN_CHAT_ID:
- await update.message.reply_text("У вас нет доступа к панели администратора.")
- return
-
- # Подсчет статистики
- all_orders = load_orders()
- total_orders = sum(len(orders) for orders in all_orders.values())
- total_users = len(all_orders)
- new_orders = sum(1 for orders in all_orders.values() for order in orders if order.get('status') == 'Новый заказ')
-
- text = (
- "🎆 *ПРОДВИНУТАЯ АДМИН-ПАНЕЛЬ*\n\n"
- f"📊 *Общая статистика:*\n"
- f"• Заказов: {total_orders}\n"
- f"• Клиентов: {total_users}\n"
- f"• Новые: {new_orders}\n\n"
- "🎛️ *Управление ботом:*"
- )
-
- keyboard = [
- [InlineKeyboardButton("📋 Заказы", callback_data='admin_orders'),
- InlineKeyboardButton("👥 Клиенты", callback_data='admin_users')],
- [InlineKeyboardButton("💰 Цены", callback_data='admin_prices'),
- InlineKeyboardButton("📊 Статистика", callback_data='admin_stats')],
- [InlineKeyboardButton("📢 Рассылка", callback_data='admin_broadcast'),
- InlineKeyboardButton("💻 Логи", callback_data='admin_logs')],
- [InlineKeyboardButton("🎁 Акции и скидки", callback_data='admin_promos')],
- [InlineKeyboardButton("⚙️ Настройки", callback_data='admin_settings')],
- [InlineKeyboardButton("❌ Выйти", callback_data='back_to_main_admin')]
- ]
-
- await update.message.reply_text(
- text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return ADMIN_MENU
-
-# Обработчик админ-меню
-async def admin_menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- if update.effective_user.id != ADMIN_CHAT_ID:
- await query.message.edit_text("У вас нет доступа.")
- return await main_menu(update, context)
-
- choice = query.data
-
- if choice == 'admin_orders':
- # Показать заказы
- all_orders = load_orders()
- text = "📋 *УПРАВЛЕНИЕ ЗАКАЗАМИ*\n\n"
-
- # Подсчет статусов
- statuses = {}
- for orders in all_orders.values():
- for order in orders:
- status = order.get('status', 'Неизвестен')
- statuses[status] = statuses.get(status, 0) + 1
-
- text += "📊 *По статусам:*\n"
- for status, count in statuses.items():
- text += f"• {status}: {count}\n"
-
- text += "\n🆕 *Последние 5 заказов:*\n\n"
-
- # Показать последние заказы
- recent_orders = []
- for uid, orders in all_orders.items():
- for order in orders:
- order['user_id'] = uid
- recent_orders.append(order)
-
- recent_orders = sorted(recent_orders, key=lambda x: x.get('date', ''), reverse=True)[:5]
-
- for order in recent_orders:
- text += f"🔸 #{order.get('order_id', 'N/A')} | {order.get('user_name', 'Unknown')}\n"
- text += f" {order.get('type', 'N/A')} | {order.get('status', 'N/A')}\n\n"
-
- keyboard = [
- [InlineKeyboardButton("🆕 Новые заказы", callback_data="admin_new_orders")],
- [InlineKeyboardButton("✅ Принятые", callback_data="admin_accepted_orders")],
- [InlineKeyboardButton("📤 Экспорт Excel", callback_data="admin_export_orders")],
- [InlineKeyboardButton("⬅️ Назад", callback_data="admin_menu")]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
-
- elif choice == 'admin_users':
- # Управление пользователями
- all_users = user_ids.union({int(uid) for uid in load_orders().keys() if uid.isdigit()})
- text = "👥 *УПРАВЛЕНИЕ КЛИЕНТАМИ*\n\n"
- text += f"📊 *Всего пользователей:* {len(all_users)}\n\n"
-
- # Топ клиентов
- top_clients = []
- for uid in all_users:
- orders = load_orders().get(str(uid), [])
- if orders:
- total = sum(order.get('price', 0) for order in orders)
- top_clients.append({'id': uid, 'orders': len(orders), 'total': total, 'name': orders[0].get('user_name', 'Unknown')})
-
- top_clients = sorted(top_clients, key=lambda x: x['total'], reverse=True)[:5]
-
- text += "🏆 *Топ-5 клиентов:*\n"
- for idx, client in enumerate(top_clients, 1):
- text += f"{idx}. {client['name']} - {client['total']:,}₽ ({client['orders']} зак.)\n"
-
- keyboard = [
- [InlineKeyboardButton("🔍 Найти клиента", callback_data="admin_find_user")],
- [InlineKeyboardButton("📨 Отправить сообщение", callback_data="admin_message_user")],
- [InlineKeyboardButton("🚫 Черный список", callback_data="admin_blacklist")],
- [InlineKeyboardButton("⬅️ Назад", callback_data="admin_menu")]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
-
- elif choice == 'admin_prices':
- # Управление ценами
- text = "💰 *УПРАВЛЕНИЕ ЦЕНАМИ*\n\n"
- text += "📍 *Текущие цены:*\n\n"
-
- for key, val in PRICES.items():
- order_type = ORDER_TYPES.get(key, {})
- text += f"{order_type.get('icon', '')} {order_type.get('name', key)}: {val.get('base', 0):,}₽\n"
-
- text += f"\n🎆 *Режим:* {PRICING_MODES.get(current_pricing_mode, {}).get('name', '')}\n"
-
- keyboard = [
- [InlineKeyboardButton("✏️ Изменить цены", callback_data="admin_edit_prices")],
- [InlineKeyboardButton("🔄 Сменить режим", callback_data="admin_change_pricing_mode")],
- [InlineKeyboardButton("🎯 Скидки", callback_data="admin_discounts")],
- [InlineKeyboardButton("⬅️ Назад", callback_data="admin_menu")]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
-
- elif choice == 'admin_stats':
- # Детальная статистика
- all_orders = load_orders()
- total_orders = sum(len(orders) for orders in all_orders.values())
- total_users = len(all_orders)
- order_types = {}
- total_revenue = 0
- today_revenue = 0
- today = datetime.now().strftime('%Y-%m-%d')
-
- for uid, orders in all_orders.items():
- for order in orders:
- order_types[order.get('type', 'Неизвестный')] = order_types.get(order.get('type', 'Неизвестный'), 0) + 1
- price = int(order.get('price', 0))
- total_revenue += price
- if order.get('date', '').startswith(today):
- today_revenue += price
-
- text = "📊 *ДЕТАЛЬНАЯ СТАТИСТИКА*\n\n"
- text += f"📈 *Общие показатели:*\n"
- text += f"• Всего клиентов: {total_users}\n"
- text += f"• Всего заказов: {total_orders}\n"
- text += f"• Общая выручка: {total_revenue:,} руб.\n"
- text += f"• Средний чек: {int(total_revenue/total_orders) if total_orders else 0:,} руб.\n\n"
-
- text += f"💰 *Выручка сегодня:* {today_revenue:,} руб.\n\n"
-
- text += "*📝 По типам работ:*\n"
- for t, c in sorted(order_types.items(), key=lambda x: x[1], reverse=True):
- text += f"• {t}: {c} ({c/total_orders*100:.1f}%)\n"
-
- keyboard = [
- [InlineKeyboardButton("📤 Экспорт в Excel", callback_data="admin_export_stats")],
- [InlineKeyboardButton("📊 Графики", callback_data="admin_charts")],
- [InlineKeyboardButton("⬅️ Назад", callback_data="admin_menu")]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
-
- elif choice == 'admin_broadcast':
- # Рассылка сообщений
- text = (
- "📢 *РАССЫЛКА СООБЩЕНИЙ*\n\n"
- "Выберите тип рассылки:\n\n"
- "• *Всем пользователям* - отправить всем клиентам\n"
- "• *Активным* - только тем, кто заказывал\n"
- "• *Новым* - кто еще не заказывал\n"
- )
- keyboard = [
- [InlineKeyboardButton("📨 Всем пользователям", callback_data="broadcast_all")],
- [InlineKeyboardButton("✅ Активным клиентам", callback_data="broadcast_active")],
- [InlineKeyboardButton("🆕 Новым пользователям", callback_data="broadcast_new")],
- [InlineKeyboardButton("⬅️ Назад", callback_data="admin_menu")]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
-
- elif choice == 'admin_settings':
- # Настройки бота
- text = (
- "⚙️ *НАСТРОЙКИ БОТА*\n\n"
- "🔧 *Доступные настройки:*\n\n"
- "• Режим цен\n"
- "• Антиплагиат минимум\n"
- "• Срок бесплатных правок\n"
- "• Реферальный процент\n"
- "• Автоответы\n"
- )
- keyboard = [
- [InlineKeyboardButton("💲 Режим цен", callback_data="settings_pricing")],
- [InlineKeyboardButton("📝 Тексты и сообщения", callback_data="settings_messages")],
- [InlineKeyboardButton("🎁 Бонусы и скидки", callback_data="settings_bonuses")],
- [InlineKeyboardButton("🤖 Автоматизация", callback_data="settings_automation")],
- [InlineKeyboardButton("⬅️ Назад", callback_data="admin_menu")]
- ]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
-
- elif choice == 'admin_change_pricing_mode':
- global current_pricing_mode
- current_pricing_mode = 'hard' if current_pricing_mode == 'light' else 'light'
- mode_info = PRICING_MODES[current_pricing_mode]
- text = f"🔄 *Режим цен изменен*\n\n{mode_info['name']}: {mode_info['icon']} {mode_info['description']}"
- keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="admin_prices")]]
- await query.message.edit_text(text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard))
-
- elif choice == 'back_to_main_admin':
- return await main_menu(update, context)
-
- elif choice == 'admin_menu':
- # Возврат к главному меню админа
- all_orders = load_orders()
- total_orders = sum(len(orders) for orders in all_orders.values())
- total_users = len(all_orders)
- new_orders = sum(1 for orders in all_orders.values() for order in orders if order.get('status') == 'Новый заказ')
-
- text = (
- "🎆 *ПРОДВИНУТАЯ АДМИН-ПАНЕЛЬ*\n\n"
- f"📊 *Общая статистика:*\n"
- f"• Заказов: {total_orders}\n"
- f"• Клиентов: {total_users}\n"
- f"• Новые: {new_orders}\n\n"
- "🎛️ *Управление ботом:*"
- )
-
- keyboard = [
- [InlineKeyboardButton("📋 Заказы", callback_data='admin_orders'),
- InlineKeyboardButton("👥 Клиенты", callback_data='admin_users')],
- [InlineKeyboardButton("💰 Цены", callback_data='admin_prices'),
- InlineKeyboardButton("📊 Статистика", callback_data='admin_stats')],
- [InlineKeyboardButton("📢 Рассылка", callback_data='admin_broadcast'),
- InlineKeyboardButton("💻 Логи", callback_data='admin_logs')],
- [InlineKeyboardButton("🎁 Акции и скидки", callback_data='admin_promos')],
- [InlineKeyboardButton("⚙️ Настройки", callback_data='admin_settings')],
- [InlineKeyboardButton("❌ Выйти", callback_data='back_to_main_admin')]
- ]
-
- await query.message.edit_text(
- text, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup(keyboard)
- )
- return ADMIN_MENU
-
-# Действия администратора
-async def admin_order_action(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- if update.effective_user.id != ADMIN_CHAT_ID:
- await query.message.edit_text("У вас нет доступа.")
- return ADMIN_MENU
-
- action_data = query.data.split('_')
- action, user_id, order_id = action_data[1], action_data[2], int(action_data[3])
- all_orders = load_orders()
-
- if user_id in all_orders:
- for order in all_orders[user_id]:
- if order.get('order_id') == order_id:
- if action == 'accept':
- order['status'] = 'Принят'
- try:
- await context.bot.send_message(
- chat_id=int(user_id),
- text=f"✅ *Заказ #{order_id} принят!*\n\nТип: {order.get('type')}\nТема: {order.get('topic')}\nСтатус: Принят\n\nМенеджер свяжется с вами.",
- parse_mode=ParseMode.MARKDOWN
- )
- except Exception as e:
- logger.error(f"Ошибка уведомления пользователя: {e}")
- await query.message.edit_text(f"✅ Заказ #{order_id} принят.")
- elif action == 'reject':
- order['status'] = 'Отклонен'
- try:
- await context.bot.send_message(
- chat_id=int(user_id),
- text=f"❌ *Заказ #{order_id} отклонен*\n\nТип: {order.get('type')}\nТема: {order.get('topic')}\nСтатус: Отклонен\n\nМенеджер свяжется с вами.",
- parse_mode=ParseMode.MARKDOWN
- )
- except Exception as e:
- logger.error(f"Ошибка уведомления пользователя: {e}")
- await query.message.edit_text(f"❌ Заказ #{order_id} отклонен.")
- elif action == 'change_price':
- context.user_data['admin_edit_order'] = {'user_id': user_id, 'order_id': order_id, 'current_price': order.get('price')}
- await query.message.edit_text(f"Текущая цена заказа #{order_id}: {order.get('price')} руб.\n\nВведите новую цену:")
- return ADMIN_MENU
- break
- save_orders(all_orders)
- user_orders[user_id] = all_orders[user_id]
- else:
- await query.message.edit_text(f"Ошибка: Заказ #{order_id} не найден.")
-
- keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="admin_menu")]]
- await query.message.reply_text("Выберите действие:", reply_markup=InlineKeyboardMarkup(keyboard))
- return ADMIN_MENU
-
-# Изменение цены администратором
-async def admin_change_price(update: Update, context: ContextTypes.DEFAULT_TYPE):
- edit_data = context.user_data.get('admin_edit_order', {})
- if not edit_data:
- await update.message.reply_text("Ошибка: Данные о заказе не найдены.")
- return ADMIN_MENU
-
- try:
- new_price = int(update.message.text)
- if new_price <= 0:
- await update.message.reply_text("Цена должна быть положительной.")
- return ADMIN_MENU
-
- user_id, order_id = edit_data['user_id'], edit_data['order_id']
- all_orders = load_orders()
- if user_id in all_orders:
- for order in all_orders[user_id]:
- if order.get('order_id') == order_id:
- old_price = order.get('price')
- order['price'] = new_price
- try:
- await context.bot.send_message(
- chat_id=int(user_id),
- text=f"💲 *Цена заказа #{order_id} изменена*\n\nТип: {order.get('type')}\nТема: {order.get('topic')}\nСтарая цена: {old_price} руб.\nНовая цена: {new_price} руб.\n\nМенеджер свяжется с вами.",
- parse_mode=ParseMode.MARKDOWN
- )
- except Exception as e:
- logger.error(f"Ошибка уведомления пользователя: {e}")
- save_orders(all_orders)
- user_orders[user_id] = all_orders[user_id]
- await update.message.reply_text(f"✅ Цена заказа #{order_id} изменена с {old_price} на {new_price} руб.")
- del context.user_data['admin_edit_order']
- break
- else:
- await update.message.reply_text(f"Ошибка: Заказ #{order_id} не найден.")
- except ValueError:
- await update.message.reply_text("Введите корректное число.")
-
- keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="admin_menu")]]
- await update.message.reply_text("Выберите действие:", reply_markup=InlineKeyboardMarkup(keyboard))
- return ADMIN_MENU
-
-# Запуск бота
-def main():
- logger.info("="*50)
- logger.info("Бот запускается...")
- logger.info(f"Bot token: {TELEGRAM_BOT_TOKEN[:10]}...{TELEGRAM_BOT_TOKEN[-5:]}")
- logger.info(f"Admin ID: {ADMIN_CHAT_ID}")
- logger.info(f"Platform: {platform.system()}")
- logger.info(f"BASE_DIR: {BASE_DIR}")
- logger.info(f"DATA_DIR: {DATA_DIR}")
- logger.info(f"Python version: {sys.version}")
- logger.info(f"Directories exist: BASE={os.path.exists(BASE_DIR)}, DATA={os.path.exists(DATA_DIR)}")
- logger.info("="*50)
-
- try:
- application = ApplicationBuilder().token(TELEGRAM_BOT_TOKEN).build()
- conv_handler = ConversationHandler(
- entry_points=[
- CommandHandler('start', start),
- CommandHandler('order', order_command),
- CommandHandler('profile', profile_command),
- CommandHandler('admin', admin_start)
- ],
- states={
- SELECT_MAIN_MENU: [
- CallbackQueryHandler(main_menu_handler),
- CallbackQueryHandler(main_menu, pattern='^back_to_main$')
- ],
- SELECT_ORDER_TYPE: [CallbackQueryHandler(select_order_type_callback)],
- VIEW_ORDER_DETAILS: [CallbackQueryHandler(order_details_handler)],
- INPUT_TOPIC: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, input_topic),
- CallbackQueryHandler(back_to_order_details, pattern='^back_to_order_details$')
- ],
- INPUT_DEADLINE: [
- CallbackQueryHandler(input_deadline),
- MessageHandler(filters.TEXT & ~filters.COMMAND, input_custom_deadline)
- ],
- INPUT_REQUIREMENTS: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, input_requirements),
- CallbackQueryHandler(back_to_deadline, pattern='^back_to_deadline$')
- ],
- CALCULATE_PRICE: [
- CallbackQueryHandler(confirm_order, pattern='^confirm_order$'),
- CallbackQueryHandler(cancel_order, pattern='^cancel_order$'),
- CallbackQueryHandler(change_order_data, pattern='^change_order_data$'),
- CallbackQueryHandler(handle_change_data, pattern='^change_'),
- CallbackQueryHandler(back_to_price_calc, pattern='^back_to_price_calc$')
- ],
- CONFIRM_ORDER: [
- CallbackQueryHandler(main_menu_handler, pattern='^back_to_main$'),
- CallbackQueryHandler(show_profile, pattern='^profile$')
- ],
- PROFILE_MENU: [
- CallbackQueryHandler(show_all_orders, pattern='^show_all_orders$'),
- CallbackQueryHandler(leave_feedback, pattern='^leave_feedback$'),
- CallbackQueryHandler(main_menu_handler, pattern='^make_order$'),
- CallbackQueryHandler(main_menu, pattern='^back_to_main$')
- ],
- SHOW_PRICE_LIST: [
- CallbackQueryHandler(price_calculator, pattern='^price_calculator$'),
- CallbackQueryHandler(main_menu, pattern='^back_to_main$')
- ],
- PRICE_CALCULATOR: [
- CallbackQueryHandler(calculate_price_in_calculator, pattern='^calc_'),
- CallbackQueryHandler(handle_deadline_selection, pattern='^deadline_'),
- CallbackQueryHandler(select_order_type_callback, pattern='^order_'),
- CallbackQueryHandler(back_to_price, pattern='^back_to_price$')
- ],
- SHOW_FAQ: [
- CallbackQueryHandler(show_faq_details, pattern='^faq_'),
- CallbackQueryHandler(main_menu, pattern='^back_to_main$')
- ],
- FAQ_DETAILS: [
- CallbackQueryHandler(back_to_faq, pattern='^back_to_faq$'),
- CallbackQueryHandler(main_menu, pattern='^back_to_main$')
- ],
- SHOW_ORDERS: [
- CallbackQueryHandler(back_to_profile, pattern='^back_to_profile$'),
- CallbackQueryHandler(main_menu_handler, pattern='^make_order$')
- ],
- LEAVE_FEEDBACK: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, input_feedback),
- CallbackQueryHandler(back_to_profile, pattern='^back_to_profile$')
- ],
- INPUT_FEEDBACK: [
- CallbackQueryHandler(back_to_profile, pattern='^back_to_profile$'),
- CallbackQueryHandler(main_menu, pattern='^back_to_main$')
- ],
- ADMIN_MENU: [
- CallbackQueryHandler(admin_menu_handler, pattern='^admin_'),
- CallbackQueryHandler(admin_order_action, pattern='^admin_(accept|reject|change_price)_'),
- MessageHandler(filters.TEXT & ~filters.COMMAND, admin_change_price),
- CallbackQueryHandler(main_menu, pattern='^back_to_main_admin$')
- ]
- },
- fallbacks=[
- CommandHandler('start', start),
- CommandHandler('order', order_command),
- CommandHandler('profile', profile_command),
- CommandHandler('admin', admin_start),
- CommandHandler('help', help_command)
- ],
- name="main_conversation",
- persistent=False
- )
- application.add_handler(conv_handler)
- # Добавляем обработчики команд вне ConversationHandler
- application.add_handler(CommandHandler('help', help_command))
- application.add_handler(CommandHandler('order', order_command))
- application.add_handler(CommandHandler('profile', profile_command))
- application.add_handler(CommandHandler('price', price_command))
- application.add_handler(CommandHandler('faq', faq_command))
- application.add_handler(CommandHandler('admin', admin_start))
-
- # Добавляем обработчик ошибок
- application.add_error_handler(error_handler)
-
- logger.info("Все обработчики зарегистрированы")
- logger.info("Бот настроен, начинаем polling...")
- application.run_polling(drop_pending_updates=True)
- except Exception as e:
- logger.error(f"Критическая ошибка при запуске: {e}", exc_info=True)
-
-if __name__ == '__main__':
- main()
\ No newline at end of file
diff --git a/cloud/cloud-init.yaml b/cloud/cloud-init.yaml
new file mode 100644
index 0000000..e970147
--- /dev/null
+++ b/cloud/cloud-init.yaml
@@ -0,0 +1,48 @@
+#cloud-config
+package_update: true
+package_upgrade: true
+packages:
+ - python3
+ - python3-pip
+ - python3-venv
+ - systemd
+ - supervisor
+write_files:
+ - path: /etc/systemd/system/gipsrbot-autodeploy.service
+ owner: root:root
+ permissions: '0644'
+ content: |
+ [Unit]
+ Description=GIPSR Bot auto deployment
+ After=network-online.target
+ Wants=network-online.target
+
+ [Service]
+ Type=oneshot
+ WorkingDirectory=/root/gipsr_bot
+ ExecStart=/bin/bash /root/gipsr_bot/scripts/autodeploy.sh
+ StandardOutput=journal
+ StandardError=journal
+
+ [Install]
+ WantedBy=multi-user.target
+ - path: /etc/systemd/system/gipsrbot-autodeploy.path
+ owner: root:root
+ permissions: '0644'
+ content: |
+ [Unit]
+ Description=Trigger GIPSR Bot deployment when files arrive
+
+ [Path]
+ PathExists=/root/gipsr_bot/scripts/autodeploy.sh
+ PathChanged=/root/gipsr_bot/.env
+ PathChanged=/root/gipsr_bot/bot.py
+ PathChanged=/root/gipsr_bot/requirements.txt
+ Unit=gipsrbot-autodeploy.service
+
+ [Install]
+ WantedBy=multi-user.target
+runcmd:
+ - [mkdir, -p, /root/gipsr_bot]
+ - [systemctl, daemon-reload]
+ - [systemctl, enable, '--now', 'gipsrbot-autodeploy.path']
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..b91ecbc
--- /dev/null
+++ b/config.py
@@ -0,0 +1,48 @@
+"""Static configuration for the GIPSR Telegram bot."""
+
+# === Telegram credentials ===
+# Пользователь просил полностью готовый конфиг, поэтому токен уже прописан.
+BOT_TOKEN: str = "7387413773:AAFgKfuf-REN5cn1ZXmCQg"
+
+# === Владельцы и администраторы ===
+# Все права доступны владельцу (Семён), Анна получает только уведомления.
+OWNER_CHAT_ID: int = 872379852
+OWNER_USERNAME: str = "Thisissaymoon"
+
+SECONDARY_ADMINS = [
+ {
+ "chat_id": 2019512207,
+ "username": "Nnnnnnnnn234567",
+ "name": "Анна",
+ }
+]
+
+if SECONDARY_ADMINS:
+ DEFAULT_ADMIN_CHAT_ID: int = int(SECONDARY_ADMINS[0]["chat_id"])
+ DEFAULT_ADMIN_USERNAME: str = str(
+ SECONDARY_ADMINS[0].get("username", "")
+ )
+else:
+ DEFAULT_ADMIN_CHAT_ID = 0
+ DEFAULT_ADMIN_USERNAME = ""
+
+# === Базовые настройки проекта ===
+PROJECT_NAME: str = "gipsr_bot"
+MANAGER_CONTACT_URL: str = "https://t.me/Thisissaymoon"
+DEFAULT_PRICING_MODE: str = "light"
+
+# Предопределенные статусы заказа для админ-панели.
+ORDER_STATUS_TITLES = {
+ "new": "🟡 Новый заказ",
+ "in_progress": "🔧 В работе",
+ "waiting_payment": "💳 Ожидает оплаты",
+ "revision": "🔁 На доработке",
+ "done": "✅ Готово к выдаче",
+}
+
+# Текст приветствия, дополняющий стандартный /start.
+WELCOME_MESSAGE: str = (
+ "6 лет помогаем студентам психологии, социальной работы, конфликтологии и логопедии. "
+ "Более 4000 проектов для МГУ, СПбГУ, ВШЭ, РАНХиГС и других ведущих вузов. "
+ "Срочные заказы выполняем за 24 часа с усиленной поддержкой и возможностью прикреплять любые файлы."
+)
diff --git a/deploy/supervisor.conf b/deploy/supervisor.conf
new file mode 100644
index 0000000..6113056
--- /dev/null
+++ b/deploy/supervisor.conf
@@ -0,0 +1,12 @@
+[program:gipsr_bot]
+directory=/root/gipsr_bot
+command=/root/gipsr_bot/scripts/run.sh
+autostart=true
+autorestart=true
+startsecs=5
+stopwaitsecs=10
+stdout_logfile=/root/gipsr_bot/logs/supervisor.out.log
+stderr_logfile=/root/gipsr_bot/logs/supervisor.err.log
+stopsignal=TERM
+environment=PYTHONUNBUFFERED="1"
+
diff --git a/orders.json b/orders.json
deleted file mode 100644
index 606da33..0000000
--- a/orders.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "872379852": [
- {
- "order_id": 1,
- "date": "2025-08-28 20:55:05",
- "type": "Самостоятельная работа",
- "topic": "Бра",
- "deadline": "31.08.2025",
- "days_left": 3,
- "requirements": "нет",
- "price": 1950,
- "status": "Новый заказ",
- "user_id": 872379852,
- "user_name": "Семён",
- "user_username": "Thisissaymoon"
- }
- ]
-}
\ No newline at end of file
diff --git a/orders.xlsx b/orders.xlsx
deleted file mode 100644
index c177227..0000000
Binary files a/orders.xlsx and /dev/null differ
diff --git a/prices.json b/prices.json
deleted file mode 100644
index e6f1d55..0000000
--- a/prices.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "self": {
- "base": 1500
- },
- "course_theory": {
- "base": 7000
- },
- "course_empirical": {
- "base": 11000
- },
- "vkr": {
- "base": 32000
- },
- "master": {
- "base": 42000
- }
-}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 6f138ad..74e55a7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-python-dotenv==1.0.1
-python-telegram-bot==21.4
-pandas==2.2.3
-openpyxl==3.1.5
\ No newline at end of file
+python-dotenv==1.0.1
+python-telegram-bot==21.4
+pandas==2.2.3
+openpyxl==3.1.5
diff --git a/scripts/autodeploy.sh b/scripts/autodeploy.sh
new file mode 100755
index 0000000..b97f01c
--- /dev/null
+++ b/scripts/autodeploy.sh
@@ -0,0 +1,108 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
+ echo "[gipsr_bot] Скрипт autodeploy нужно запускать от имени root." >&2
+ exit 1
+fi
+
+APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+LOG_DIR="$APP_DIR/logs"
+LOG_FILE="$LOG_DIR/autodeploy.log"
+ENV_FILE="$APP_DIR/.env"
+
+mkdir -p "$LOG_DIR"
+
+timestamp() {
+ date +"%Y-%m-%d %H:%M:%S"
+}
+
+log() {
+ local message="$1"
+ printf '[%s] %s\n' "$(timestamp)" "$message" | tee -a "$LOG_FILE"
+}
+
+log "Запуск автоматической установки GIPSR Bot..."
+
+wait_for_path() {
+ local target="$1"
+ local description="${2:-$1}"
+ if [[ -e "$target" ]]; then
+ log "Найдено: $description"
+ return 0
+ fi
+ log "Ждём, когда появится $description..."
+ while [[ ! -e "$target" ]]; do
+ sleep 3
+ done
+ log "Найдено: $description"
+}
+
+log "Проверяем, что репозиторий загружен в $APP_DIR"
+wait_for_path "$APP_DIR/bot.py" "bot.py"
+wait_for_path "$APP_DIR/requirements.txt" "requirements.txt"
+wait_for_path "$APP_DIR/scripts/install.sh" "scripts/install.sh"
+wait_for_path "$ENV_FILE" ".env"
+
+chmod +x "$APP_DIR/scripts/install.sh" "$APP_DIR/scripts/run.sh" 2>/dev/null || true
+
+if command -v apt-get >/dev/null 2>&1; then
+ export DEBIAN_FRONTEND=noninteractive
+ log "Обновляем список пакетов..."
+ apt-get update -y >>"$LOG_FILE" 2>&1
+ log "Устанавливаем python3-venv, python3-pip и systemd-tools..."
+ apt-get install -y python3 python3-venv python3-pip systemd >>"$LOG_FILE" 2>&1
+else
+ log "apt-get не найден. Предполагаем, что зависимости уже установлены."
+fi
+
+if [[ ! -s "$ENV_FILE" ]]; then
+ log "Файл .env пустой. Заполните его перед установкой."
+ exit 1
+fi
+
+if ! grep -Eq '^TELEGRAM_BOT_TOKEN=[^\s]+$' "$ENV_FILE"; then
+ log "В .env отсутствует TELEGRAM_BOT_TOKEN. Проверьте файл и перезапустите установку."
+ exit 1
+fi
+
+if grep -Eq 'PASTE_TELEGRAM_BOT_TOKEN_HERE|REPLACE_ME|0000000000' "$ENV_FILE"; then
+ log "В .env все еще стоит шаблон TELEGRAM_BOT_TOKEN. Замените его на реальный токен бота."
+ exit 1
+fi
+
+log "Создаем виртуальное окружение и ставим зависимости..."
+bash "$APP_DIR/scripts/install.sh" >>"$LOG_FILE" 2>&1
+
+SERVICE_PATH="/etc/systemd/system/gipsrbot.service"
+log "Создаем systemd unit $SERVICE_PATH"
+cat <"$SERVICE_PATH"
+[Unit]
+Description=GIPSR Telegram bot
+After=network.target
+
+[Service]
+Type=simple
+WorkingDirectory=$APP_DIR
+Environment=PYTHONUNBUFFERED=1
+ExecStart=$APP_DIR/venv/bin/python $APP_DIR/bot.py
+Restart=always
+RestartSec=5
+
+[Install]
+WantedBy=multi-user.target
+SYSTEMD
+
+log "Перезапускаем systemd и включаем службу..."
+systemctl daemon-reload
+systemctl enable gipsrbot.service >>"$LOG_FILE" 2>&1
+systemctl restart gipsrbot.service
+
+log "Текущий статус службы:"
+systemctl status gipsrbot.service --no-pager >>"$LOG_FILE" 2>&1 || true
+systemctl status gipsrbot.service --no-pager || true
+
+log "Последние строки журнала сервиса:"
+journalctl -u gipsrbot.service -n 20 --no-pager || true
+
+log "Готово! Бот запущен и будет стартовать автоматически при перезагрузке."
diff --git a/scripts/install.sh b/scripts/install.sh
new file mode 100755
index 0000000..c663dd1
--- /dev/null
+++ b/scripts/install.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+VENV_DIR="$APP_DIR/venv"
+
+if [[ ! -d "$VENV_DIR" ]]; then
+ python3 -m venv "$VENV_DIR"
+fi
+
+source "$VENV_DIR/bin/activate"
+python -m pip install --upgrade pip
+python -m pip install -r "$APP_DIR/requirements.txt"
+
+echo "[gipsr_bot] Зависимости установлены в $VENV_DIR"
+
diff --git a/scripts/run.sh b/scripts/run.sh
new file mode 100755
index 0000000..b856ede
--- /dev/null
+++ b/scripts/run.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+VENV_DIR="$APP_DIR/venv"
+LOG_DIR="$APP_DIR/logs"
+
+mkdir -p "$LOG_DIR"
+source "$VENV_DIR/bin/activate"
+exec python "$APP_DIR/bot.py"
+
diff --git a/user_logs.json b/user_logs.json
deleted file mode 100644
index b683457..0000000
--- a/user_logs.json
+++ /dev/null
@@ -1,1059 +0,0 @@
-{
- "872379852": [
- {
- "timestamp": "2025-07-30 09:13:54",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:13:54",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:18:54",
- "action": "Админ-панель",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:19:11",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:19:13",
- "action": "Выбор в меню: price_calculator",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:19:13",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:19:33",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:19:38",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:19:48",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:19:51",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:19:57",
- "action": "Выбор в меню: profile",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:19:57",
- "action": "Профиль",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:20:02",
- "action": "Профиль",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:20:05",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:20:07",
- "action": "Выбор в меню: make_order",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:20:07",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:20:10",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:20:13",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:20:14",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:20:15",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:04",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:04",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:06",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:07",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:10",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:37",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:37",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:39",
- "action": "Выбор в меню: price_calculator",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:39",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:44",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:46",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:47",
- "action": "Выбор в меню: make_order",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:47",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:48",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:50",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:52",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:54",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:55",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:56",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:56",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:21:57",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:00",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:00",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:03",
- "action": "Выбор в меню: faq",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:03",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:10",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:12",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:15",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:18",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:22",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:26",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:28",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:32",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:36",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:37",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:39",
- "action": "Выбор в меню: price_list",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:22:39",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:23:06",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:23:16",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:23:17",
- "action": "Выбор в меню: make_order",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:23:17",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:23:18",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:23:19",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:23:19",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:23:22",
- "action": "Выбор в меню: price_list",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:23:22",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:24:42",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:25:57",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:26:17",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:26:19",
- "action": "Выбор в меню: make_order",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:26:19",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:26:20",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:26:39",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:26:39",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:26:41",
- "action": "Выбор в меню: price_list",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:26:41",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:27:08",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:28:03",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:28:05",
- "action": "Выбор в меню: price_calculator",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:28:05",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:28:14",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:28:15",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:28:16",
- "action": "Выбор в меню: price_calculator",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:28:16",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:29:17",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:29:22",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:30:38",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:30:40",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:30:42",
- "action": "Выбор в меню: profile",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:30:42",
- "action": "Профиль",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:30:47",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:30:48",
- "action": "Выбор в меню: faq",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:30:48",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:30:50",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:43:35",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:43:35",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:00",
- "action": "Админ-панель",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:12",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:14",
- "action": "Выбор в меню: price_calculator",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:14",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:37",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:38",
- "action": "Выбор в меню: make_order",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:39",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:40",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:41",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:43",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:48",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:49",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:49",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:49",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:49",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:54",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:55",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:55",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:55",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:57",
- "action": "Выбор в меню: faq",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:44:57",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:45:01",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:45:02",
- "action": "Выбор в меню: price_calculator",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:45:02",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:45:08",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:45:12",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:45:52",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:45:52",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:45:53",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:01",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:01",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:02",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:33",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:33",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:35",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:38",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:42",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:42",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:44",
- "action": "Выбор в меню: price_calculator",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:44",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:45",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:46",
- "action": "Выбор в меню: price_list",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:46:46",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:47:32",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:34",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:36",
- "action": "Выбор в меню: faq",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:36",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:37",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:38",
- "action": "Выбор в меню: price_calculator",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:38",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:39",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:40",
- "action": "Выбор в меню: price_list",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:40",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:41",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:43",
- "action": "Выбор в меню: make_order",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:43",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:44",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:46",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:47",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:52",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:52",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:53",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:54",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:48:56",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:50:10",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:50:11",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:50:51",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:50:52",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:50:53",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:50:54",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:50:54",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:50:56",
- "action": "Выбор в меню: faq",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:50:56",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:51:00",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:51:01",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:51:04",
- "action": "Выбор в меню: profile",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:51:04",
- "action": "Профиль",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:51:06",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:51:06",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 09:59:57",
- "action": "Профиль",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:00",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:00",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:02",
- "action": "Профиль",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:07",
- "action": "Профиль",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:09",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:11",
- "action": "Выбор в меню: price_list",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:12",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:14",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:16",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:18",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:19",
- "action": "Выбор в меню: price_calculator",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:19",
- "action": "Калькулятор",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:25",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:31",
- "action": "Выбор в меню: my_orders",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:31",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:34",
- "action": "Выбор в меню: price_list",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:34",
- "action": "Прайс-лист",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:36",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:37",
- "action": "Выбор в меню: faq",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:37",
- "action": "FAQ",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 10:00:39",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 19:30:15",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 19:30:15",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 19:30:18",
- "action": "Выбор в меню: make_order",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 19:30:18",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 19:30:20",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-07-30 19:30:30",
- "action": "Выбор типа заказа",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-08-28 04:47:55",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-08-28 04:47:55",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-08-28 05:00:20",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-08-28 05:00:21",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-08-28 05:00:25",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-08-28 05:00:25",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-08-28 05:03:22",
- "action": "/start",
- "username": "Thisissaymoon"
- },
- {
- "timestamp": "2025-08-28 05:03:22",
- "action": "Главное меню",
- "username": "Thisissaymoon"
- }
- ]
-}
\ No newline at end of file