savagedb_bot/main_old.py

3614 lines
180 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import signal
import sys
from os import getenv
import logging
from datetime import datetime
import platform
import os
from logging.handlers import TimedRotatingFileHandler
from aiogram import Bot, Dispatcher
from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, LabeledPrice, PreCheckoutQuery, InputMediaPhoto, FSInputFile
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
from db import OracleDatabase
from middlewares.db import DbSessionMiddleware
def setup_logging():
"""
Настройка системы логирования с ротацией файлов и выводом в консоль
"""
# Создаем каталог logs если он не существует
if is_windows():
logs_dir = "logs"
else:
logs_dir = "/logs/"
if not os.path.exists(logs_dir):
os.makedirs(logs_dir)
print(f"Created logs directory: {logs_dir}")
# Определяем уровень логирования
if getenv("DEBUG", '0') == '1':
log_level = logging.INFO
else:
log_level = logging.WARNING
# Временно включаем детальное логирование для отладки
log_level = logging.INFO
# Создаем основной логгер
logger = logging.getLogger()
logger.setLevel(log_level)
# Очищаем существующие обработчики
logger.handlers.clear()
# Формат логов
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Настройка файлового логирования с ротацией
file_handler = TimedRotatingFileHandler(
filename=os.path.join(logs_dir, "salvagedb_bot.log"),
when='midnight', # Ротация в полночь
interval=1, # Каждый день
backupCount=30, # Храним 30 дней
encoding='utf-8',
utc=False # Используем локальное время
)
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
file_handler.suffix = "%Y-%m-%d" # Формат суффикса для файлов
# Настройка консольного логирования
console_handler = logging.StreamHandler()
console_handler.setLevel(log_level)
console_handler.setFormatter(formatter)
# Добавляем обработчики к логгеру
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# Логируем успешную настройку
logging.info("=== LOGGING SYSTEM INITIALIZED ===")
logging.info(f"Log level: {logging.getLevelName(log_level)}")
logging.info(f"Logs directory: {os.path.abspath(logs_dir)}")
logging.info(f"Log rotation: daily, keeping 30 days")
# Проверяем запуск в Docker
if os.path.exists('/.dockerenv'):
logging.info("Running inside Docker container")
logging.info(f"Container timezone: {getenv('TZ', 'UTC')}")
logging.info(f"Container user: {getenv('USER', 'unknown')}")
else:
logging.info("Running on host system")
logging.info("=== LOGGING SETUP COMPLETE ===")
def get_us_state_name(state_code: str) -> str:
"""
Конвертирует двухбуквенный код штата США в полное название
"""
states = {
'AL': 'Alabama', 'AK': 'Alaska', 'AZ': 'Arizona', 'AR': 'Arkansas', 'CA': 'California',
'CO': 'Colorado', 'CT': 'Connecticut', 'DE': 'Delaware', 'FL': 'Florida', 'GA': 'Georgia',
'HI': 'Hawaii', 'ID': 'Idaho', 'IL': 'Illinois', 'IN': 'Indiana', 'IA': 'Iowa',
'KS': 'Kansas', 'KY': 'Kentucky', 'LA': 'Louisiana', 'ME': 'Maine', 'MD': 'Maryland',
'MA': 'Massachusetts', 'MI': 'Michigan', 'MN': 'Minnesota', 'MS': 'Mississippi', 'MO': 'Missouri',
'MT': 'Montana', 'NE': 'Nebraska', 'NV': 'Nevada', 'NH': 'New Hampshire', 'NJ': 'New Jersey',
'NM': 'New Mexico', 'NY': 'New York', 'NC': 'North Carolina', 'ND': 'North Dakota', 'OH': 'Ohio',
'OK': 'Oklahoma', 'OR': 'Oregon', 'PA': 'Pennsylvania', 'RI': 'Rhode Island', 'SC': 'South Carolina',
'SD': 'South Dakota', 'TN': 'Tennessee', 'TX': 'Texas', 'UT': 'Utah', 'VT': 'Vermont',
'VA': 'Virginia', 'WA': 'Washington', 'WV': 'West Virginia', 'WI': 'Wisconsin', 'WY': 'Wyoming',
'DC': 'District of Columbia'
}
return states.get(state_code.upper(), state_code)
def format_sale_date(date_str: str) -> str:
"""
Форматирует дату продажи из MM/YYYY в красивый формат
"""
if not date_str or date_str == 'None' or '/' not in date_str:
return "Unknown"
try:
month, year = date_str.split('/')
months = {
'1': 'January', '2': 'February', '3': 'March', '4': 'April',
'5': 'May', '6': 'June', '7': 'July', '8': 'August',
'9': 'September', '10': 'October', '11': 'November', '12': 'December'
}
month_name = months.get(month.lstrip('0'), month)
return f"{month_name} {year}"
except:
return date_str
def parse_location(location_str: str) -> str:
r"""
Парсит и форматирует локацию из формата ST/TOWN или ST\TOWN
"""
if not location_str or location_str == 'None':
return "Unknown Location"
try:
# Проверяем оба варианта разделителей: / и \
if '/' in location_str:
state_code, city = location_str.split('/', 1)
state_name = get_us_state_name(state_code.strip())
city_formatted = city.strip().title()
return f"{city_formatted}, {state_name}"
elif '\\' in location_str:
state_code, city = location_str.split('\\', 1)
state_name = get_us_state_name(state_code.strip())
city_formatted = city.strip().title()
return f"{city_formatted}, {state_name}"
else:
return location_str
except:
return location_str
def escape_markdown(text: str) -> str:
"""
Экранирует специальные символы Markdown для безопасной отправки в Telegram
"""
if not text:
return ""
# Символы которые нужно экранировать для Markdown
escape_chars = ['*', '_', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
escaped_text = str(text)
for char in escape_chars:
escaped_text = escaped_text.replace(char, f'\\{char}')
return escaped_text
def get_operating_system() -> str:
"""
Определяет операционную систему на которой запущен код
Returns:
str: 'Windows', 'Linux', 'macOS' или 'Unknown'
"""
system = platform.system().lower()
if system == 'windows':
return 'Windows'
elif system == 'linux':
return 'Linux'
elif system == 'darwin':
return 'macOS'
else:
return f'Unknown ({system})'
def log_system_info():
"""
Логирует информацию о системе при запуске
"""
os_name = get_operating_system()
python_version = platform.python_version()
architecture = platform.machine()
logging.info(f"=== SYSTEM INFORMATION ===")
logging.info(f"Operating System: {os_name}")
logging.info(f"Platform: {platform.platform()}")
logging.info(f"Python Version: {python_version}")
logging.info(f"Architecture: {architecture}")
logging.info(f"Node Name: {platform.node()}")
logging.info(f"os.name: {os.name}")
logging.info(f"=== END SYSTEM INFO ===")
def is_windows() -> bool:
"""Проверяет, запущен ли код на Windows"""
return get_operating_system() == 'Windows'
def is_linux() -> bool:
"""Проверяет, запущен ли код на Linux"""
return get_operating_system() == 'Linux'
def is_macos() -> bool:
"""Проверяет, запущен ли код на macOS"""
return get_operating_system() == 'macOS'
def convert_photo_path(db_path: str) -> str:
"""
Конвертирует путь к фотографии в зависимости от операционной системы
Args:
db_path: путь из базы данных в Linux формате (с /)
Returns:
str: полный путь к файлу с учетом OS и базового пути
"""
if not db_path:
return ""
# Убираем лишние пробелы из пути
db_path = db_path.strip()
# Базовый путь из константы
base_path = image_path
if is_windows():
# Конвертируем Linux пути в Windows формат
windows_path = db_path.replace('/', '\\')
full_path = f"{base_path}\\{windows_path}"
logging.info(f"Converted path for Windows: {db_path} -> {full_path}")
return full_path
else:
# Для Linux/macOS оставляем как есть
full_path = f"{base_path}/{db_path}"
logging.info(f"Path for Linux/macOS: {db_path} -> {full_path}")
return full_path
def prepare_photo_paths(db_paths: list) -> list:
"""
Подготавливает список полных путей к фотографиям
Args:
db_paths: список путей из базы данных
Returns:
list: список полных путей к файлам
"""
if not db_paths:
return []
full_paths = []
for db_path in db_paths:
full_path = convert_photo_path(db_path)
if full_path:
full_paths.append(full_path)
logging.info(f"Prepared {len(full_paths)} photo paths from {len(db_paths)} database paths")
return full_paths
async def send_vehicle_photos(message: Message, vin: str, photo_paths: list, make: str, model: str, year: str):
"""
Отправляет фотографии автомобиля пользователю
Args:
message: сообщение пользователя
vin: VIN автомобиля
photo_paths: список полных путей к фотографиям
make, model, year: информация об автомобиле
"""
if not photo_paths:
await message.answer("❌ No photos found to send.")
return
try:
# Дебаг информация о текущем пользователе
import pwd
import grp
current_user = pwd.getpwuid(os.getuid())
current_groups = [grp.getgrgid(gid).gr_name for gid in os.getgroups()]
logging.info(f"DEBUG: Running as user: {current_user.pw_name}({os.getuid()}), groups: {current_groups}")
# Telegram позволяет максимум 10 фотографий в media group
photos_per_group = 10
total_photos = len(photo_paths)
logging.info(f"Attempting to send {total_photos} photos for VIN: {vin}")
# Разбиваем фотографии на группы по 10
photo_groups = [photo_paths[i:i + photos_per_group] for i in range(0, len(photo_paths), photos_per_group)]
total_groups = len(photo_groups)
logging.info(f"Split {total_photos} photos into {total_groups} groups")
sent_count = 0
for group_num, photo_group in enumerate(photo_groups, 1):
try:
logging.info(f"Processing group {group_num}/{total_groups} with {len(photo_group)} photos")
# Создаем media group для текущей группы
media_group = []
for i, photo_path in enumerate(photo_group):
try:
# Проверяем существование файла
# Дебаг информация
import stat
import pwd
import grp
try:
stat_info = os.stat(photo_path)
file_owner = pwd.getpwuid(stat_info.st_uid).pw_name
file_group = grp.getgrgid(stat_info.st_gid).gr_name
file_perms = oct(stat_info.st_mode)[-3:]
logging.info(f"DEBUG: File {photo_path} - owner: {file_owner}({stat_info.st_uid}), group: {file_group}({stat_info.st_gid}), perms: {file_perms}")
except Exception as debug_e:
logging.warning(f"DEBUG: Cannot get file info for {photo_path}: {debug_e}")
if os.path.exists(photo_path):
# Создаем InputMediaPhoto
if i == 0 and group_num == 1:
# Первая фотография первой группы с полным описанием
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
caption = f"📸 Vehicle damage photos\n📋 VIN: {vin}\n📊 Total photos: {total_photos}\n🗂️ Group {group_num}/{total_groups}"
else:
caption = f"📸 {year} {make} {model}\n📋 VIN: {vin}\n📊 Total photos: {total_photos}\n🗂️ Group {group_num}/{total_groups}"
elif i == 0:
# Первая фотография других групп с номером группы
caption = f"🗂️ Group {group_num}/{total_groups}"
else:
# Остальные фотографии без описания
caption = None
if caption:
media_group.append(InputMediaPhoto(
media=FSInputFile(photo_path),
caption=caption
))
else:
media_group.append(InputMediaPhoto(
media=FSInputFile(photo_path)
))
sent_count += 1
logging.info(f"Added photo {sent_count}/{total_photos}: {photo_path}")
else:
logging.warning(f"Photo file not found: {photo_path}")
except Exception as photo_error:
logging.error(f"Error processing photo {sent_count + 1} ({photo_path}): {photo_error}")
continue
if media_group:
# Отправляем media group
await message.answer_media_group(media_group)
logging.info(f"Successfully sent group {group_num}/{total_groups} with {len(media_group)} photos")
# Небольшая пауза между группами для избежания rate limiting
if group_num < total_groups:
await asyncio.sleep(0.5)
else:
logging.warning(f"No valid photos in group {group_num}")
except Exception as group_error:
logging.error(f"Error sending photo group {group_num}: {group_error}")
await message.answer(f"❌ Error sending photo group {group_num}. Continuing with remaining photos...")
continue
if sent_count > 0:
# Отправляем итоговое сообщение
await message.answer(
f"✅ **Photos sent successfully!**\n"
f"📊 **{sent_count} of {total_photos} photos** delivered\n"
f"🗂️ **{total_groups} photo groups** sent"
)
logging.info(f"Successfully sent {sent_count}/{total_photos} photos for VIN: {vin}")
else:
# Если ни одна фотография не найдена
await message.answer(
"❌ **Error:** Photo files not found on server.\n"
"Please contact support with your transaction details."
)
logging.error(f"No valid photo files found for VIN: {vin}")
except Exception as e:
logging.error(f"Error sending photos for VIN {vin}: {e}")
await message.answer(
"❌ **Error sending photos.** Please contact support with your transaction details.\n"
f"Error details: {str(e)}"
)
# Настройка системы логирования
setup_logging()
# Логируем информацию о системе
log_system_info()
TOKEN = getenv("BOT_TOKEN")
BOTNAME = getenv("BOT_NAME")
DECODE_PRICE = getenv("DECODE_PRICE",1)
CHECK_PRICE = getenv("CHECK_PRICE",10)
IMG_PRICE = getenv("IMG_PRICE",100)
ADMIN_USER_ID = int(getenv("ADMIN_USER_ID", "0")) # ID администратора из переменных окружения
if is_windows():
image_path = "D:\\SALVAGEDB\\salvagedb_bot\\images"
else:
image_path = "/images"
oracle_db = OracleDatabase(
user= getenv("DB_USER"),
password= getenv("DB_PASSWORD"),
dsn= getenv("DB_DSN")
)
dp = Dispatcher()
class VinStates(StatesGroup):
waiting_for_vin = State()
waiting_for_check_vin = State()
waiting_for_photo_vin = State()
# Command handler
@dp.message(Command("start"))
async def command_start_handler(message: Message, db: OracleDatabase = None) -> None:
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при каждом взаимодействии
await database.save_user(message.from_user, "start_command")
welcome_text = (
"Welcome to SalvagedbBot — your trusted assistant for vehicle history checks via VIN!\n\n"
"🔍 What You Can Discover:\n\n"
"• Salvage or junk status\n"
"• Damage from hail, flood, or fire\n"
"• Mileage discrepancies or odometer rollback\n"
"• Gray market vehicle status\n\n"
"We don't claim that a vehicle has a salvage title, but we provide information indicating possible past damages, helping you make informed decisions."
)
builder = InlineKeyboardBuilder()
builder.button(text="Decode VIN", callback_data="decode_vin")
builder.button(text="Check VIN", callback_data="check_vin")
builder.button(text="Car photo", callback_data="search_car_photo")
builder.adjust(3)
builder.button(text="Help", callback_data="help")
builder.button(text="Prices", callback_data="prices")
builder.button(text="Go Salvagedb.com", url="https://salvagedb.com")
# Добавляем кнопку администратора только для админа
if message.from_user.id == ADMIN_USER_ID:
builder.button(text="📊 Admin Stats", callback_data="admin_stats")
builder.adjust(3, 3, 1)
else:
builder.adjust(3, 2)
await message.answer(welcome_text, reply_markup=builder.as_markup())
@dp.callback_query(lambda c: c.data == "decode_vin")
async def decode_vin_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при нажатии кнопки
await database.save_user(callback.from_user, "decode_vin_button")
await callback.message.answer("Please enter the vehicle VIN.")
await state.set_state(VinStates.waiting_for_vin)
await callback.answer()
@dp.callback_query(lambda c: c.data == "check_vin")
async def check_vin_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при нажатии кнопки
await database.save_user(callback.from_user, "check_vin_button")
await callback.message.answer("Please enter the vehicle VIN to check salvage records.")
await state.set_state(VinStates.waiting_for_check_vin)
await callback.answer()
@dp.callback_query(lambda c: c.data == "search_car_photo")
async def search_car_photo_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при нажатии кнопки
await database.save_user(callback.from_user, "search_car_photo_button")
await callback.message.answer("Please enter the vehicle VIN to search for damage photos.")
await state.set_state(VinStates.waiting_for_photo_vin)
await callback.answer()
@dp.callback_query(lambda c: c.data == "main_menu")
async def main_menu_callback(callback: CallbackQuery, state: FSMContext, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при возврате в главное меню
await database.save_user(callback.from_user, "main_menu_button")
await state.clear()
# Создаем новое сообщение с правильным пользователем для проверки admin кнопки
welcome_text = (
f"Welcome to {BOTNAME}!\n\n"
"🔍 Enter a VIN and discover valuable vehicle information:\n\n"
"• **Decode VIN** - Get detailed specifications and history\n"
"• **Check VIN** - Find salvage records and damage reports\n"
"• **Car photo** - Access damage photos from auctions\n\n"
"We don't claim that a vehicle has a salvage title, but we provide information indicating possible past damages, helping you make informed decisions."
)
builder = InlineKeyboardBuilder()
builder.button(text="Decode VIN", callback_data="decode_vin")
builder.button(text="Check VIN", callback_data="check_vin")
builder.button(text="Car photo", callback_data="search_car_photo")
builder.adjust(3)
builder.button(text="Help", callback_data="help")
builder.button(text="Prices", callback_data="prices")
builder.button(text="Go Salvagedb.com", url="https://salvagedb.com")
# Добавляем кнопку администратора только для админа
if callback.from_user.id == ADMIN_USER_ID:
builder.button(text="📊 Admin Stats", callback_data="admin_stats")
builder.adjust(3, 3, 1)
else:
builder.adjust(3, 2)
await callback.message.answer(welcome_text, reply_markup=builder.as_markup())
await callback.answer()
@dp.callback_query(lambda c: c.data == "help")
async def help_callback(callback: CallbackQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при просмотре справки
await database.save_user(callback.from_user, "help_button")
help_text = (
" **Help - Our Services**\n\n"
"🔍 **1. Decode VIN**\n"
f"• Free basic vehicle information (make, model, year)\n"
f"• Detailed specifications for {DECODE_PRICE} ⭐ (engine, transmission, safety features, etc.)\n"
f"• Comprehensive technical data from NHTSA database\n\n"
"🚨 **2. Check VIN**\n"
f"• Free salvage records count\n"
f"• Detailed damage history for {CHECK_PRICE} ⭐ (auction data, damage types, repair costs)\n"
f"• Sale dates and locations from insurance auctions\n\n"
"📸 **3. Car Photos**\n"
f"• Check availability of damage photos\n"
f"• Access to actual auction photos for {IMG_PRICE}\n"
f"• High-quality images showing vehicle condition and damage\n\n"
"💡 **How to use:**\n"
"• Enter any 17-character VIN number\n"
"• VIN must contain only letters and numbers (no I, O, Q)\n"
"• All payments are made with Telegram Stars ⭐\n\n"
"⚠️ **Important:** Our reports show historical data for informed decision-making. "
"Always consult automotive experts for professional vehicle evaluation."
)
builder = InlineKeyboardBuilder()
builder.button(text="🔍 Try Decode VIN", callback_data="decode_vin")
builder.button(text="🚨 Try Check VIN", callback_data="check_vin")
builder.button(text="📸 Try Search Photos", callback_data="search_car_photo")
builder.adjust(3)
builder.button(text="🏠 Back to Main Menu", callback_data="main_menu")
builder.adjust(3, 1)
await callback.message.answer(help_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
@dp.callback_query(lambda c: c.data == "prices")
async def prices_callback(callback: CallbackQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при просмотре цен
await database.save_user(callback.from_user, "prices_button")
prices_text = (
"💰 **Our Service Prices**\n\n"
"🔍 **VIN Decoding Service:**\n"
f"• Basic info (make, model, year): **FREE** 🆓\n"
f"• Detailed specifications: **{DECODE_PRICE} ⭐**\n"
f" └ Engine details, transmission, safety features\n"
f" └ Dimensions, construction, brake system\n"
f" └ Lighting, additional features, NCSA data\n\n"
"🚨 **Salvage Check Service:**\n"
f"• Records count check: **FREE** 🆓\n"
f"• Detailed damage history: **{CHECK_PRICE} ⭐**\n"
f" └ Primary and secondary damage types\n"
f" └ Sale dates and auction locations\n"
f" └ Odometer readings and repair costs\n"
f" └ Engine/drive status information\n\n"
"📸 **Vehicle Photos Service:**\n"
f"• Photo availability check: **FREE** 🆓\n"
f"• Access to damage photos: **{IMG_PRICE} ⭐**\n"
f" └ High-quality auction images\n"
f" └ Multiple angles of vehicle damage\n"
f" └ Before and after condition photos\n\n"
"⭐ **Payment Information:**\n"
"• All payments made with **Telegram Stars**\n"
"• Instant delivery after successful payment\n"
"• Automatic refund if no data found\n"
"• Admin users get automatic refunds\n\n"
"💡 **Money-back guarantee:** If we can't provide the requested data, "
"your payment will be automatically refunded!"
)
builder = InlineKeyboardBuilder()
builder.button(text=f"🔍 Decode for {DECODE_PRICE}", callback_data="decode_vin")
builder.button(text=f"🚨 Check for {CHECK_PRICE}", callback_data="check_vin")
builder.button(text=f"📸 Photos for {IMG_PRICE}", callback_data="search_car_photo")
builder.adjust(3)
builder.button(text=" Help", callback_data="help")
builder.button(text="🏠 Back to Main Menu", callback_data="main_menu")
builder.adjust(2, 1)
await callback.message.answer(prices_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
@dp.callback_query(lambda c: c.data == "admin_stats")
async def admin_stats_callback(callback: CallbackQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Проверяем, является ли пользователь администратором
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied. This command is for administrators only.", show_alert=True)
return
# Сохраняем данные пользователя при нажатии кнопки админки
await database.save_user(callback.from_user, "admin_stats_button")
# Формируем меню админ панели с категориями отчетов
admin_menu_text = """🔧 **Admin Dashboard**
Выберите категорию отчетов для анализа:
📊 **Доступные отчеты:**
• Пользовательская аналитика
• Финансовая аналитика
• Техническая аналитика
• Операционные отчеты
• Бизнес-аналитика
💡 Выберите интересующую категорию ниже:"""
# Создаем кнопки для категорий отчетов
builder = InlineKeyboardBuilder()
builder.button(text="👥 Пользователи", callback_data="admin_users")
builder.button(text="💰 Финансы", callback_data="admin_finance")
builder.adjust(2)
builder.button(text="⚡ Операционная", callback_data="admin_operations")
builder.button(text="📈 Бизнес-аналитика", callback_data="admin_business")
builder.adjust(2)
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(1)
await callback.message.answer(admin_menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
# ==============================================
# ADMIN REPORTS CALLBACKS
# ==============================================
@dp.callback_query(lambda c: c.data == "admin_users")
async def admin_users_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Пользовательская аналитика"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
menu_text = """👥 **Пользовательская аналитика**
Выберите тип отчета:"""
builder = InlineKeyboardBuilder()
builder.button(text="📊 Общая статистика", callback_data="admin_users_general")
builder.button(text="📈 Рост пользователей", callback_data="admin_users_growth")
builder.adjust(2)
builder.button(text="👑 Premium анализ", callback_data="admin_users_premium")
builder.button(text="🌍 География", callback_data="admin_users_geo")
builder.adjust(2)
builder.button(text="⚡ Активность", callback_data="admin_users_activity")
builder.button(text="📋 Источники", callback_data="admin_users_sources")
builder.adjust(2)
builder.button(text="🔙 Назад", callback_data="admin_stats")
builder.adjust(1)
await callback.message.answer(menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
@dp.callback_query(lambda c: c.data == "admin_finance")
async def admin_finance_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Финансовая аналитика"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
menu_text = """💰 **Финансовая аналитика**
Выберите тип отчета:"""
builder = InlineKeyboardBuilder()
builder.button(text="💵 Доходы", callback_data="admin_finance_revenue")
builder.button(text="📊 По услугам", callback_data="admin_finance_services")
builder.adjust(2)
builder.button(text="🔄 Конверсия", callback_data="admin_finance_conversion")
builder.button(text="↩️ Возвраты", callback_data="admin_finance_refunds")
builder.adjust(2)
builder.button(text="💳 Транзакции", callback_data="admin_finance_transactions")
builder.button(text="🎯 Эффективность", callback_data="admin_finance_efficiency")
builder.adjust(2)
builder.button(text="🔙 Назад", callback_data="admin_stats")
builder.adjust(1)
await callback.message.answer(menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
@dp.callback_query(lambda c: c.data == "admin_operations")
async def admin_operations_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Операционные отчеты"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
menu_text = """⚡ **Операционные отчеты**
Выберите тип отчета:"""
builder = InlineKeyboardBuilder()
builder.button(text="⏱️ Производительность", callback_data="admin_ops_performance")
builder.button(text="🚨 Ошибки системы", callback_data="admin_ops_errors")
builder.adjust(2)
builder.button(text="📈 Нагрузка", callback_data="admin_ops_load")
builder.button(text="🏪 Аукционы", callback_data="admin_ops_auctions")
builder.adjust(2)
builder.button(text="🔍 Проблемные VIN", callback_data="admin_ops_problem_vins")
builder.button(text="👀 Мониторинг", callback_data="admin_ops_monitoring")
builder.adjust(2)
builder.button(text="🔙 Назад", callback_data="admin_stats")
builder.adjust(1)
await callback.message.answer(menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
@dp.callback_query(lambda c: c.data == "admin_business")
async def admin_business_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Бизнес-аналитика"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
menu_text = """📈 **Бизнес-аналитика**
Выберите тип отчета:"""
builder = InlineKeyboardBuilder()
builder.button(text="📊 Тренды", callback_data="admin_biz_trends")
builder.button(text="🔮 Прогнозы", callback_data="admin_biz_forecasts")
builder.adjust(2)
builder.button(text="🌍 Регионы роста", callback_data="admin_biz_regions")
builder.button(text="💎 Монетизация", callback_data="admin_biz_monetization")
builder.adjust(2)
builder.button(text="🎯 Оптимизация", callback_data="admin_biz_optimization")
builder.button(text="💡 Рекомендации", callback_data="admin_biz_recommendations")
builder.adjust(2)
builder.button(text="🔙 Назад", callback_data="admin_stats")
builder.adjust(1)
await callback.message.answer(menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
# ==============================================
# BUSINESS ANALYTICS REPORT HANDLERS
# ==============================================
@dp.callback_query(lambda c: c.data == "admin_biz_trends")
async def admin_biz_trends_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ трендов бизнеса"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_biz_trends_stats()
# Тренд роста пользователей
user_growth_text = ""
if stats['user_growth_trend']:
user_growth_text = "\n📈 <b>Рост пользователей:</b>\n"
for month, new_users, premium in stats['user_growth_trend'][-6:]: # последние 6 месяцев
user_growth_text += f"{month}: {new_users:,} новых ({premium} premium)\n"
# Тренд выручки
revenue_trend_text = ""
if stats['revenue_trend']:
revenue_trend_text = "\n💰 <b>Тренд выручки:</b>\n"
for month, transactions, revenue, paying_users in stats['revenue_trend'][-6:]:
revenue_trend_text += f"{month}: {revenue or 0:,.0f} ⭐️ ({transactions} тр-ций, {paying_users} польз.)\n"
# Тренд услуг
services_trend_text = ""
if stats['services_trend']:
services_trend_text = "\n🛠 <b>Популярность услуг (за 6 мес.):</b>\n"
service_totals = {}
for service, month, count in stats['services_trend']:
if service not in service_totals:
service_totals[service] = 0
service_totals[service] += count
for service, total in sorted(service_totals.items(), key=lambda x: x[1], reverse=True)[:5]:
# Экранируем название сервиса
safe_service = escape_markdown(str(service)) if service else "Unknown"
services_trend_text += f"{safe_service}: {total:,} использований\n"
# Конверсия по месяцам
conversion_text = ""
if stats['conversion_trend']:
conversion_text = "\n🎯 <b>Конверсия по месяцам:</b>\n"
for month, total_users, converted in stats['conversion_trend'][-6:]:
conversion_rate = round(converted/total_users*100, 1) if total_users > 0 else 0
conversion_text += f"{month}: {conversion_rate}% ({converted}/{total_users})\n"
report = f"""📊 <b>Анализ трендов бизнеса</b>
{user_growth_text}
{revenue_trend_text}
{services_trend_text}
{conversion_text}
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_business")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating trends report: {e}")
import traceback
logging.error(f"Full traceback: {traceback.format_exc()}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_biz_forecasts")
async def admin_biz_forecasts_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Прогнозы развития бизнеса"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_biz_forecasts_stats()
# Анализ роста за последние 3 месяца
growth_analysis = ""
if stats['recent_growth']:
growth_analysis = "\n📈 <b>Последние 3 месяца:</b>\n"
total_users = sum([users for month, users, revenue in stats['recent_growth']])
avg_growth = total_users / len(stats['recent_growth']) if stats['recent_growth'] else 0
for month, users, revenue in stats['recent_growth']:
growth_analysis += f"{month}: {users:,} новых (+{revenue or 0:,.0f} ⭐️)\n"
growth_analysis += f"\n📊 <b>Прогноз на следующий месяц:</b> ~{avg_growth:,.0f} новых пользователей"
# Сезонность по дням недели
seasonality_text = ""
if stats['seasonality']:
seasonality_text = "\n📅 <b>Сезонность (дни недели):</b>\n"
days = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']
for day_num, transactions, avg_amount in sorted(stats['seasonality']):
day_name = days[int(day_num) - 2] if int(day_num) <= 7 else f"День {day_num}"
seasonality_text += f"{day_name}: {transactions} тр-ций, {avg_amount or 0:.1f} ⭐️ средняя сумма\n"
# Потенциал по регионам
regional_potential = ""
if stats['regional_potential']:
regional_potential = "\n🌍 <b>Потенциал роста по регионам:</b>\n"
for lang, users, paying, avg_rev in stats['regional_potential'][:5]:
conversion = round(paying/users*100, 1) if users > 0 else 0
flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪"}.get(lang, "🌐")
regional_potential += f"{flag} {lang}: {users:,} польз., конверсия {conversion}%, LTV {avg_rev or 0:.1f} ⭐️\n"
report = f"""🔮 <b>Прогнозы развития бизнеса</b>
{growth_analysis}
{seasonality_text}
{regional_potential}
💡 <b>Рекомендации:</b>
• Сосредоточьтесь на днях с высокой активностью
• Развивайте регионы с низкой конверсией
• Планируйте маркетинг на основе сезонности
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_business")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating forecasts report: {e}")
import traceback
logging.error(f"Full traceback: {traceback.format_exc()}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_biz_regions")
async def admin_biz_regions_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ регионов роста"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_biz_regions_stats()
# Топ регионы по росту
growth_regions = ""
if stats['regions_growth']:
growth_regions = "\n🚀 <b>Топ регионы по росту:</b>\n"
for lang, new_month, prev_month, total, avg_rev, paying in stats['regions_growth'][:5]:
growth_rate = round(((new_month - prev_month) / prev_month * 100), 1) if prev_month > 0 else 0
flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪", "fr": "🇫🇷"}.get(lang, "🌐")
growth_regions += f"{flag} {lang}: +{new_month} за месяц ({growth_rate:+.1f}%), всего {total:,}\n"
# Конверсия по регионам
conversion_regions = ""
if stats['regional_conversion']:
conversion_regions = "\n💰 <b>Конверсия по регионам:</b>\n"
for lang, total_users, paying_users, transactions, revenue in stats['regional_conversion'][:5]:
conversion = round(paying_users/total_users*100, 1) if total_users > 0 else 0
arpu = round(revenue/total_users, 1) if total_users > 0 and revenue else 0
flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪", "fr": "🇫🇷"}.get(lang, "🌐")
conversion_regions += f"{flag} {lang}: {conversion}% конверсия, {arpu} ⭐️ ARPU\n"
# Premium распределение
premium_regions = ""
if stats['premium_distribution']:
premium_regions = "\n👑 <b>Premium по регионам:</b>\n"
for lang, total, premium, prem_avg, reg_avg in stats['premium_distribution'][:5]:
premium_rate = round(premium/total*100, 1) if total > 0 else 0
flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪", "fr": "🇫🇷"}.get(lang, "🌐")
premium_regions += f"{flag} {lang}: {premium_rate}% premium ({premium}/{total})\n"
report = f"""🌍 <b>Анализ регионов роста</b>
{growth_regions}
{conversion_regions}
{premium_regions}
💡 <b>Выводы:</b>
• Сосредоточьтесь на регионах с высоким ростом
• Изучите успешные стратегии топ-регионов
• Адаптируйте контент под местные особенности
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_business")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating regions report: {e}")
import traceback
logging.error(f"Full traceback: {traceback.format_exc()}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_biz_monetization")
async def admin_biz_monetization_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ монетизации"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_biz_monetization_stats()
# Воронка монетизации
funnel_text = ""
if stats['monetization_funnel']:
total, engaged, paying, repeat, high_value = stats['monetization_funnel']
engagement_rate = round(engaged/total*100, 1) if total > 0 else 0
conversion_rate = round(paying/total*100, 1) if total > 0 else 0
repeat_rate = round(repeat/paying*100, 1) if paying > 0 else 0
high_value_rate = round(high_value/total*100, 1) if total > 0 else 0
funnel_text = f"""🎯 <b>Воронка монетизации:</b>
Всего пользователей: <b>{total:,}</b>
• Вовлеченные (&gt;1 взаимодействия): <b>{engaged:,}</b> ({engagement_rate}%)
• Платящие: <b>{paying:,}</b> ({conversion_rate}%)
• Повторные покупки: <b>{repeat:,}</b> ({repeat_rate}%)
• Высокая ценность (&gt;50⭐): <b>{high_value:,}</b> ({high_value_rate}%)"""
# LTV анализ
ltv_text = ""
if stats['ltv_analysis']:
ltv_text = "\n💎 <b>Анализ LTV по сегментам:</b>\n"
for segment, count, avg_ltv, total_rev, avg_trans in stats['ltv_analysis']:
percentage = round(count/sum([c[1] for c in stats['ltv_analysis']])*100, 1)
ltv_text += f"{segment}: {count:,} ({percentage}%), LTV {avg_ltv or 0:.1f} ⭐️\n"
# Прибыльность услуг
profitability_text = ""
if stats['service_profitability']:
profitability_text = "\n🛠 <b>Прибыльность услуг (90 дней):</b>\n"
for service, count, revenue, avg_price, users, successful in stats['service_profitability']:
success_rate = round(successful/count*100, 1) if count > 0 else 0
profitability_text += f"{service}: {revenue or 0:,.0f} ⭐️ ({success_rate}% успех)\n"
# Время до покупки
time_to_purchase_text = ""
if stats['time_to_purchase']:
avg_days, purchases = stats['time_to_purchase']
time_to_purchase_text = f"\n⏱ <b>Время до первой покупки:</b> {avg_days or 0} дней в среднем ({purchases} покупок)"
report = f"""💎 <b>Анализ монетизации</b>
{funnel_text}
{ltv_text}
{profitability_text}
{time_to_purchase_text}
💡 <b>Рекомендации:</b>
• Улучшите вовлечение новых пользователей
• Сосредоточьтесь на повторных покупках
• Оптимизируйте наименее прибыльные услуги
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_business")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating monetization report: {e}")
import traceback
logging.error(f"Full traceback: {traceback.format_exc()}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_biz_optimization")
async def admin_biz_optimization_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ возможностей оптимизации"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_biz_optimization_stats()
# Анализ оттока
churn_text = ""
if stats['churn_analysis']:
churn_text = "\n📉 <b>Анализ оттока пользователей:</b>\n"
total_analyzed = sum([count for status, count, avg_rev, paying in stats['churn_analysis']])
for status, count, avg_rev, paying in stats['churn_analysis']:
percentage = round(count/total_analyzed*100, 1) if total_analyzed > 0 else 0
churn_text += f"{status}: {count:,} ({percentage}%), LTV {avg_rev or 0:.1f} ⭐️\n"
# Неэффективные запросы
inefficient_text = ""
if stats['inefficient_requests']:
inefficient_text = "\n⚠️ <b>Неэффективные запросы (30 дней):</b>\n"
for service, total, no_data, errors, refunds in stats['inefficient_requests']:
no_data_rate = round(no_data/total*100, 1) if total > 0 else 0
error_rate = round(errors/total*100, 1) if total > 0 else 0
refund_rate = round(refunds/total*100, 1) if total > 0 else 0
inefficient_text += f"{service}: {no_data_rate}% без данных, {error_rate}% ошибок, {refund_rate}% возвратов\n"
# Высокопотенциальные пользователи
potential_text = ""
if stats['high_potential']:
engaged_non, potential_repeat, underperform_prem, valuable_dormant = stats['high_potential']
potential_text = f"""🎯 <b>Пользователи с высоким потенциалом:</b>
• Активные без покупок: <b>{engaged_non:,}</b>
• Потенциал повторных покупок: <b>{potential_repeat:,}</b>
• Неэффективные Premium: <b>{underperform_prem:,}</b>
• Ценные неактивные: <b>{valuable_dormant:,}</b>"""
# Анализ ценообразования
pricing_text = ""
if stats['pricing_analysis']:
pricing_text = "\n💰 <b>Анализ цен по услугам:</b>\n"
service_summary = {}
for service, price, purchases, successful, refunds in stats['pricing_analysis']:
if service not in service_summary:
service_summary[service] = []
service_summary[service].append((price, purchases, successful, refunds))
for service, prices in list(service_summary.items())[:3]: # топ 3 услуги
total_purchases = sum([p[1] for p in prices])
pricing_text += f"{service}: {total_purchases:,} покупок, цены {min([p[0] for p in prices])}-{max([p[0] for p in prices])} ⭐️\n"
report = f"""🎯 <b>Анализ возможностей оптимизации</b>
{churn_text}
{inefficient_text}
{potential_text}
{pricing_text}
🚀 <b>Приоритетные действия:</b>
• Работайте с активными неплательщиками
• Улучшите качество данных для проблемных услуг
• Реактивируйте ценных неактивных пользователей
• Оптимизируйте ценообразование
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_business")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating optimization report: {e}")
import traceback
logging.error(f"Full traceback: {traceback.format_exc()}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_biz_recommendations")
async def admin_biz_recommendations_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Бизнес рекомендации на основе данных"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_biz_recommendations_stats()
# Формируем рекомендации на основе данных
recommendations = []
if stats['base_metrics']:
total, paying, avg_ltv, active, premium = stats['base_metrics']
conversion_rate = round(paying/total*100, 1) if total > 0 else 0
activity_rate = round(active/total*100, 1) if total > 0 else 0
premium_rate = round(premium/total*100, 1) if total > 0 else 0
# Анализируем и даем рекомендации
if conversion_rate < 5:
recommendations.append("🎯 <b>Низкая конверсия:</b> Улучшите онбординг и первое впечатление")
elif conversion_rate > 15:
recommendations.append("🎯 <b>Высокая конверсия:</b> Масштабируйте маркетинг для привлечения пользователей")
if activity_rate < 30:
recommendations.append("⚡ <b>Низкая активность:</b> Внедрите программу реактивации пользователей")
if premium_rate < 10:
recommendations.append("👑 <b>Мало Premium:</b> Усильте маркетинг Premium-подписки")
# Анализ эффективности услуг
service_recommendations = []
if stats['service_efficiency']:
for service, total_req, successful_count, revenue, refunds in stats['service_efficiency']:
success_rate = round(successful_count/total_req*100, 1) if total_req > 0 else 0
refund_rate = round(refunds/total_req*100, 1) if total_req > 0 else 0
if success_rate < 80:
safe_service = escape_markdown(str(service)) if service else "Unknown"
service_recommendations.append(f"⚠️ <b>{safe_service}:</b> Низкий успех ({success_rate}%) - улучшите качество данных")
if refund_rate > 10:
safe_service = escape_markdown(str(service)) if service else "Unknown"
service_recommendations.append(f"💸 <b>{safe_service}:</b> Высокий возврат ({refund_rate}%) - пересмотрите ценообразование")
# Анализ роста по регионам
regional_recommendations = []
if stats['regional_growth']:
top_growth_regions = stats['regional_growth'][:3]
for lang, new_month, total, paying in top_growth_regions:
conversion = round(paying/total*100, 1) if total > 0 else 0
flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪"}.get(lang, "🌐")
if new_month > 10: # значительный рост
regional_recommendations.append(f"🚀 <b>{flag} {lang}:</b> Активный рост - увеличьте инвестиции в регион")
# Формируем итоговый отчет
base_metrics_text = ""
if stats['base_metrics']:
total, paying, avg_ltv, active, premium = stats['base_metrics']
base_metrics_text = f"""📊 <b>Ключевые метрики:</b>
• Конверсия: <b>{round(paying/total*100, 1) if total > 0 else 0}%</b>
• Активность: <b>{round(active/total*100, 1) if total > 0 else 0}%</b>
• Premium: <b>{round(premium/total*100, 1) if total > 0 else 0}%</b>
• Средний LTV: <b>{avg_ltv or 0:.1f} ⭐️</b>"""
all_recommendations = recommendations + service_recommendations + regional_recommendations
recommendations_text = "\n💡 <b>Приоритетные рекомендации:</b>\n"
for i, rec in enumerate(all_recommendations[:8], 1): # показываем топ-8 рекомендаций
recommendations_text += f"{i}. {rec}\n"
if not all_recommendations:
recommendations_text += "✅ Основные метрики в норме. Сосредоточьтесь на масштабировании."
report = f"""💡 <b>Бизнес рекомендации</b>
{base_metrics_text}
{recommendations_text}
🎯 <b>Следующие шаги:</b>
• Внедрите приоритетные улучшения
• Проведите A/B тестирование изменений
• Отслеживайте метрики еженедельно
• Фокусируйтесь на ROI каждого действия
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_business")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating recommendations report: {e}")
import traceback
logging.error(f"Full traceback: {traceback.format_exc()}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
# ==============================================
# USER ANALYTICS REPORT HANDLERS
# ==============================================
@dp.callback_query(lambda c: c.data == "admin_users_general")
async def admin_users_general_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Общая статистика пользователей"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_users_general_stats()
report = f"""📊 **Общая статистика пользователей**
👥 **Пользователи:**
Всего пользователей: **{stats['total_users']:,}**
• Активных: **{stats['active_users']:,}** ({round(stats['active_users']/stats['total_users']*100, 1) if stats['total_users'] > 0 else 0}%)
• Premium: **{stats['premium_users']:,}** ({round(stats['premium_users']/stats['total_users']*100, 1) if stats['total_users'] > 0 else 0}%)
• Заблокированных: **{stats['blocked_users']:,}**
💰 **Монетизация:**
• Платящих пользователей: **{stats['paying_users']:,}**
• Конверсия в покупку: **{round(stats['paying_users']/stats['total_users']*100, 2) if stats['total_users'] > 0 else 0}%**
• Общая выручка: **{stats['total_revenue']:,.0f}** ⭐️
Всего транзакций: **{stats['total_transactions']:,}**
⚡ **Активность:**
За 24 часа: **{stats['active_24h']:,}**
За 7 дней: **{stats['active_7d']:,}**
За 30 дней: **{stats['active_30d']:,}**
Всего взаимодействий: **{stats['total_interactions']:,}**
• Среднее на пользователя: **{stats['avg_interactions_per_user']:.1f}**
📅 **Сгенерировано:** {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_users")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
except Exception as e:
logging.error(f"Error generating general user stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_users_growth")
async def admin_users_growth_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Статистика роста пользователей"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_users_growth_stats()
# Формируем график роста за последние дни
daily_chart = ""
if stats['daily_growth']:
daily_chart = "\n📈 **Рост по дням (последние 10):**\n"
for day, count in stats['daily_growth']:
daily_chart += f"{day}: **{count}** новых\n"
report = f"""📈 **Рост пользователей**
🆕 **Новые пользователи:**
• Сегодня: **{stats['new_today']:,}**
За неделю: **{stats['new_week']:,}**
За месяц: **{stats['new_month']:,}**
За год: **{stats['new_year']:,}**
📊 **Общая статистика:**
• Первый пользователь: **{stats['first_user_date']}**
• Дней с запуска: **{stats['days_since_start']:,}**
• Средний рост в день: **{round(stats['new_year']/365, 1) if stats['days_since_start'] > 0 else 0}**
{daily_chart}
📅 **Сгенерировано:** {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_users")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
except Exception as e:
logging.error(f"Error generating growth stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_users_premium")
async def admin_users_premium_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ Premium пользователей"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_users_premium_stats()
report = f"""👑 **Premium анализ**
📊 **Распределение пользователей:**
• Premium: **{stats['premium_users']:,}** ({stats['premium_percentage']:.1f}%)
• Обычные: **{stats['regular_users']:,}** ({100-stats['premium_percentage']:.1f}%)
💰 **Средние платежи:**
• Premium пользователи: **{stats['premium_avg_payment']:.1f}** ⭐️
• Обычные пользователи: **{stats['regular_avg_payment']:.1f}** ⭐️
• Разница: **{stats['premium_avg_payment'] - stats['regular_avg_payment']:.1f}** ⭐️
⚡ **Активность:**
• Premium - взаимодействий: **{stats['premium_avg_interactions']:.1f}**
• Обычные - взаимодействий: **{stats['regular_avg_interactions']:.1f}**
🎯 **Конверсия в покупки:**
• Premium пользователи: **{stats['premium_conversion']:.1f}%** ({stats['premium_paying']}/{stats['premium_users']})
• Обычные пользователи: **{stats['regular_conversion']:.1f}%** ({stats['regular_paying']}/{stats['regular_users']})
💡 **Вывод:** Premium пользователи платят в **{round(stats['premium_avg_payment']/stats['regular_avg_payment'], 1) if stats['regular_avg_payment'] > 0 else 0}х** раз больше и в **{round(stats['premium_conversion']/stats['regular_conversion'], 1) if stats['regular_conversion'] > 0 else 0}х** раз чаще покупают услуги.
📅 **Сгенерировано:** {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_users")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
except Exception as e:
logging.error(f"Error generating premium stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_users_geo")
async def admin_users_geo_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""География пользователей"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_users_geography_stats()
# Топ языков с экранированием специальных символов
languages_text = ""
if stats['top_languages']:
languages_text = "\n🌍 <b>Топ языков:</b>\n"
for lang, count, percentage in stats['top_languages']:
# Экранируем потенциально проблемные символы
safe_lang = str(lang).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪", "fr": "🇫🇷", "es": "🇪🇸", "it": "🇮🇹"}.get(lang, "🌐")
languages_text += f"{flag} {safe_lang}: <b>{count:,}</b> ({percentage}%)\n"
# Источники регистрации
sources_text = ""
if stats['registration_sources']:
sources_text = "\n📱 <b>Источники регистрации:</b>\n"
for source, count in stats['registration_sources']:
safe_source = str(source).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
sources_text += f"{safe_source}: <b>{count:,}</b>\n"
report = f"""🌍 <b>География пользователей</b>
📊 <b>Языковая статистика:</b>
Всего языков: <b>{stats['total_languages']}</b>
{languages_text}
{sources_text}
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_users")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating geography stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_users_activity")
async def admin_users_activity_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ активности пользователей"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_users_activity_stats()
# Распределение по активности
activity_text = ""
if stats['interaction_distribution']:
activity_text = "\n👥 **Распределение по активности:**\n"
for category, count in stats['interaction_distribution']:
activity_text += f"{category}: **{count:,}**\n"
report = f"""⚡ **Анализ активности**
📊 **Активность по периодам:**
За 1 день: **{stats['active_1d']:,}**
За 3 дня: **{stats['active_3d']:,}**
За 7 дней: **{stats['active_7d']:,}**
За 14 дней: **{stats['active_14d']:,}**
За 30 дней: **{stats['active_30d']:,}**
• Неактивны 30+ дней: **{stats['inactive_30d']:,}**
📈 **Retention Rate:**
• 1 день: **{round(stats['active_1d']/stats['active_30d']*100, 1) if stats['active_30d'] > 0 else 0}%**
• 7 дней: **{round(stats['active_7d']/stats['active_30d']*100, 1) if stats['active_30d'] > 0 else 0}%**
• 14 дней: **{round(stats['active_14d']/stats['active_30d']*100, 1) if stats['active_30d'] > 0 else 0}%**
{activity_text}
📅 **Сгенерировано:** {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_users")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
except Exception as e:
logging.error(f"Error generating activity stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_users_sources")
async def admin_users_sources_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ источников пользователей"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_users_sources_stats()
# Основные источники
sources_text = ""
if stats['source_breakdown']:
sources_text = "\n📊 **Источники пользователей:**\n"
for source, total, paying, revenue, interactions, new_30d in stats['source_breakdown']:
conversion = round(paying/total*100, 1) if total > 0 else 0
avg_revenue = round(revenue, 1) if revenue else 0
avg_int = round(interactions, 1) if interactions else 0
sources_text += f"• **{source}**: {total:,} чел. (конв: {conversion}%, ср.доход: {avg_revenue}⭐, активность: {avg_int}, новых за 30д: {new_30d})\n"
# Реферальные источники
referrals_text = ""
if stats['top_referrals']:
referrals_text = "\n🔗 **Топ реферальные источники:**\n"
for source, count in stats['top_referrals']:
referrals_text += f"{source}: **{count:,}**\n"
report = f"""📋 **Источники пользователей**
{sources_text}
{referrals_text}
💡 **Анализ:** Наиболее эффективными являются источники с высокой конверсией в покупки и средним доходом на пользователя.
📅 **Сгенерировано:** {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_users")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown")
await callback.answer()
except Exception as e:
logging.error(f"Error generating sources stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
# ==============================================
# FINANCIAL ANALYTICS REPORT HANDLERS
# ==============================================
@dp.callback_query(lambda c: c.data == "admin_finance_revenue")
async def admin_finance_revenue_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ доходов"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_finance_revenue_stats(ADMIN_USER_ID)
# Формируем график доходов
daily_chart = ""
if stats['daily_revenue']:
daily_chart = "\n📈 <b>Доходы по дням (последние 10):</b>\n"
for day, revenue, transactions in stats['daily_revenue']:
daily_chart += f"{day}: <b>{revenue:.0f}</b> ⭐ ({transactions} транз.)\n"
report = f"""💵 <b>Анализ доходов</b>
💰 <b>Общая статистика:</b>
• Общая выручка: <b>{stats['total_revenue']:,.0f}</b> ⭐️
Всего транзакций: <b>{stats['total_transactions']:,}</b>
• Средний чек: <b>{stats['avg_transaction']:.1f}</b> ⭐️
• Уникальных клиентов: <b>{stats['unique_customers']:,}</b>
📊 <b>Доходы по периодам:</b>
За 24 часа: <b>{stats['revenue_24h']:,.0f}</b> ⭐ ({stats['transactions_24h']} транз.)
За 7 дней: <b>{stats['revenue_7d']:,.0f}</b> ⭐ ({stats['transactions_7d']} транз.)
За 30 дней: <b>{stats['revenue_30d']:,.0f}</b> ⭐ ({stats['transactions_30d']} транз.)
📈 <b>Средние показатели:</b>
• Доход на клиента: <b>{round(stats['total_revenue']/stats['unique_customers'], 1) if stats['unique_customers'] > 0 else 0:.1f}</b> ⭐
• Транзакций на клиента: <b>{round(stats['total_transactions']/stats['unique_customers'], 1) if stats['unique_customers'] > 0 else 0:.1f}</b>
{daily_chart}
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_finance")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating revenue stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_finance_services")
async def admin_finance_services_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ по услугам"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_finance_services_stats(ADMIN_USER_ID)
# Проверяем, есть ли данные
if not stats or not stats.get('services_breakdown'):
report = """📊 <b>Анализ по услугам</b>
📭 <b>Нет данных</b>
В системе пока нет завершенных транзакций для анализа.
💡 <b>Возможные причины:</b>
• Таблица payment_logs пустая
• Нет записей со статусом 'completed'
Все транзакции имеют статус 'pending' или 'failed'
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
else:
# Статистика по услугам
services_text = ""
if stats['services_breakdown']:
services_text = "\n📊 <b>Breakdown по услугам:</b>\n"
service_names = {
'decode_vin': '🔍 Декодинг VIN',
'check_salvage': '💥 Проверка Salvage',
'get_photos': '📸 Получение фото'
}
for service, transactions, revenue, avg_price, users, success_count, refunds, avg_data in stats['services_breakdown']:
name = service_names.get(service, service)
success_rate = round(success_count/transactions*100, 1) if transactions > 0 else 0
refund_rate = round(refunds/transactions*100, 1) if transactions > 0 else 0
# Получаем тренды
trends = stats['trends'].get(service, {"week": 0, "month": 0})
services_text += f"{name}:\n"
services_text += f" 💰 Доход: <b>{revenue:.0f}</b> ⭐ ({transactions} транз.)\n"
services_text += f" 💵 Средняя цена: <b>{avg_price:.1f}</b> ⭐\n"
services_text += f" 👥 Клиентов: <b>{users}</b>\n"
services_text += f" ✅ Успешность: <b>{success_rate}%</b>\n"
services_text += f" ↩️ Возвраты: <b>{refund_rate}%</b>\n"
services_text += f" 📈 За неделю/месяц: <b>{trends['week']}/{trends['month']}</b>\n\n"
report = f"""📊 <b>Анализ по услугам</b>
{services_text}
💡 <b>Выводы:</b>
• Наиболее доходная услуга по общей выручке
• Услуга с наивысшей конверсией
• Оптимизация ценообразования по успешности
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_finance")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating services stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_finance_conversion")
async def admin_finance_conversion_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ конверсии"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_finance_conversion_stats(ADMIN_USER_ID)
report = f"""🔄 <b>Анализ конверсии</b>
🎯 <b>Основная конверсия:</b>
Всего пользователей: <b>{stats['total_users']:,}</b>
• Платящих пользователей: <b>{stats['paying_users']:,}</b>
• <b>Конверсия в покупку: {stats['conversion_rate']:.2f}%</b>
👥 <b>Сегментация покупателей:</b>
• Premium покупатели: <b>{stats['premium_buyers']:,}</b>
• Обычные покупатели: <b>{stats['regular_buyers']:,}</b>
• Средняя покупка: <b>{stats['avg_purchase']:.1f}</b> ⭐
🔄 <b>Повторные покупки:</b>
• Разовые покупатели: <b>{stats['one_time_buyers']:,}</b>
• Регулярные (2-5): <b>{stats['regular_buyers_repeat']:,}</b>
• Лояльные (5+): <b>{stats['loyal_buyers']:,}</b>
• Среднее покупок на клиента: <b>{stats['avg_purchases_per_user']:.1f}</b>
📈 <b>Retention метрики:</b>
• Repeat Rate: <b>{stats['repeat_rate']:.1f}%</b>
Всего транзакций: <b>{stats['total_transactions']:,}</b>
💡 <b>Анализ:</b>
{stats['repeat_rate']:.1f}% покупателей совершают повторные покупки
• Потенциал роста конверсии до {100 - stats['conversion_rate']:.1f}%
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_finance")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating conversion stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_finance_refunds")
async def admin_finance_refunds_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ возвратов"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_finance_refunds_stats(ADMIN_USER_ID)
# Breakdown возвратов по услугам
refunds_breakdown = ""
if stats['refund_breakdown']:
refunds_breakdown = "\n↩️ <b>Возвраты по услугам:</b>\n"
service_names = {
'decode_vin': '🔍 Декодинг VIN',
'check_salvage': '💥 Проверка Salvage',
'get_photos': '📸 Получение фото'
}
current_service = None
for service, refund_type, count, amount in stats['refund_breakdown']:
if service != current_service:
current_service = service
name = service_names.get(service, service)
refunds_breakdown += f"\n{name}:\n"
refund_name = {
'auto_refund': 'Авто возврат',
'manual_refund': 'Ручной возврат',
'admin_refund': 'Админ возврат'
}.get(refund_type, refund_type)
refunds_breakdown += f" - {refund_name}: <b>{count}</b> шт. (<b>{amount:.0f}</b> ⭐)\n"
report = f"""↩️ <b>Анализ возвратов</b>
📊 <b>Общая статистика:</b>
Всего транзакций: <b>{stats['total_transactions']:,}</b>
• Возвратов: <b>{stats['refund_count']:,}</b>
• <b>Процент возвратов: {stats['refund_rate']:.2f}%</b>
• Сумма возвратов: <b>{stats['refund_amount']:,.0f}</b> ⭐
🔄 <b>Типы возвратов:</b>
• Автоматические: <b>{stats['auto_refunds']:,}</b>
• Ручные: <b>{stats['manual_refunds']:,}</b>
• Админские: <b>{stats['admin_refunds']:,}</b>
{refunds_breakdown}
💡 <b>Анализ:</b>
• Низкий процент возвратов (&lt;5%) - показатель качества
• Высокий процент (&gt;10%) - требует оптимизации услуг
• Автовозвраты снижают нагрузку на поддержку
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_finance")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating refunds stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_finance_transactions")
async def admin_finance_transactions_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ транзакций"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_finance_transactions_stats(ADMIN_USER_ID)
# Breakdown ошибок по услугам
errors_breakdown = ""
if stats['error_breakdown']:
errors_breakdown = "\n🚨 <b>Ошибки по услугам:</b>\n"
service_names = {
'decode_vin': '🔍 Декодинг VIN',
'check_salvage': '💥 Проверка Salvage',
'get_photos': '📸 Получение фото'
}
for service, payment_failures, service_errors, no_data in stats['error_breakdown']:
name = service_names.get(service, service)
errors_breakdown += f"{name}:\n"
errors_breakdown += f" - Ошибки платежей: <b>{payment_failures}</b>\n"
errors_breakdown += f" - Ошибки сервиса: <b>{service_errors}</b>\n"
errors_breakdown += f" - Нет данных: <b>{no_data}</b>\n"
report = f"""💳 <b>Анализ транзакций</b>
💰 <b>Статистика платежей:</b>
Всего попыток: <b>{stats['total_attempts']:,}</b>
• Успешно: <b>{stats['completed']:,}</b>
В ожидании: <b>{stats['pending']:,}</b>
• Неудачно: <b>{stats['failed']:,}</b>
• <b>Успешность платежей: {stats['payment_success_rate']:.1f}%</b>
⚙️ <b>Статистика сервисов:</b>
• Успешные сервисы: <b>{stats['service_success']:,}</b>
• Нет данных: <b>{stats['no_data']:,}</b>
• Ошибки сервиса: <b>{stats['service_error']:,}</b>
• <b>Успешность сервисов: {stats['service_success_rate']:.1f}%</b>
{errors_breakdown}
📊 <b>Ключевые метрики:</b>
• Конверсия попытка → успех: <b>{stats['payment_success_rate']:.1f}%</b>
• Конверсия платеж → данные: <b>{stats['service_success_rate']:.1f}%</b>
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_finance")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating transactions stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_finance_efficiency")
async def admin_finance_efficiency_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ эффективности"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_finance_efficiency_stats(ADMIN_USER_ID)
# Распределение по часам
hourly_chart = ""
if stats['hourly_distribution']:
hourly_chart = "\n⏰ <b>Активность по часам (UTC):</b>\n"
for hour, transactions, revenue in stats['hourly_distribution'][:12]: # Топ 12 часов
hourly_chart += f"{int(hour):02d}:00 - <b>{transactions}</b> транз. (<b>{revenue:.0f}</b> ⭐)\n"
# Топ VIN по доходам
top_vins_text = ""
if stats['top_vins']:
top_vins_text = "\n🏆 <b>Топ VIN по доходам:</b>\n"
for vin, requests, revenue in stats['top_vins'][:5]: # Топ 5
top_vins_text += f"• <code>{vin}</code>: <b>{requests}</b> запр. = <b>{revenue:.0f}</b> ⭐\n"
report = f"""🎯 <b>Анализ эффективности</b>
💰 <b>Финансовая эффективность:</b>
• Доход на транзакцию: <b>{stats['avg_revenue_per_transaction']:.1f}</b> ⭐
• Транзакций в день: <b>{stats['avg_transactions_per_day']:.1f}</b>
• Доход на клиента: <b>{stats['revenue_per_customer']:.1f}</b> ⭐
• Транзакций на клиента: <b>{stats['transactions_per_customer']:.1f}</b>
{hourly_chart}
{top_vins_text}
📊 <b>Insights:</b>
• LTV клиента: <b>{stats['revenue_per_customer']:.0f}</b> ⭐
• Средняя частота покупок: <b>{stats['transactions_per_customer']:.1f}</b>
• Дневной оборот: <b>{stats['avg_transactions_per_day'] * stats['avg_revenue_per_transaction']:.0f}</b> ⭐
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_finance")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating efficiency stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
# ==============================================
# OPERATIONAL ANALYTICS REPORT HANDLERS
# ==============================================
@dp.callback_query(lambda c: c.data == "admin_ops_performance")
async def admin_ops_performance_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ производительности системы"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_ops_performance_stats()
# Статистика по услугам
services_text = ""
if stats['services_breakdown']:
services_text = "\n⚙️ <b>Производительность по услугам:</b>\n"
service_names = {
'decode_vin': '🔍 Декодинг VIN',
'check_salvage': '💥 Проверка Salvage',
'get_photos': '📸 Получение фото'
}
for service, requests, success_count, avg_data, refunds in stats['services_breakdown']:
name = service_names.get(service, service)
success_rate = round(success_count/requests*100, 1) if requests > 0 else 0
refund_rate = round(refunds/requests*100, 1) if requests > 0 else 0
services_text += f"{name}:\n"
services_text += f" 📊 Запросов: <b>{requests:,}</b>\n"
services_text += f" ✅ Успешность: <b>{success_rate}%</b>\n"
services_text += f" 📈 Ср. данных: <b>{avg_data}</b>\n"
services_text += f" ↩️ Возвраты: <b>{refund_rate}%</b>\n\n"
# Пиковые часы
peak_hours_text = ""
if stats['peak_hours']:
peak_hours_text = "\n⏰ <b>Пиковые часы (топ-5):</b>\n"
for hour, requests in stats['peak_hours'][:5]:
peak_hours_text += f"{int(hour):02d}:00 UTC - <b>{requests}</b> запросов\n"
report = f"""⏱️ <b>Производительность системы</b>
📊 <b>Общая статистика:</b>
Всего запросов: <b>{stats['total_requests']:,}</b>
• Успешных: <b>{stats['successful_requests']:,}</b>
• <b>Общая успешность: {stats['success_rate']:.1f}%</b>
• Без данных: <b>{stats['no_data_requests']:,}</b>
• Ошибки: <b>{stats['error_requests']:,}</b>
• Среднее данных на запрос: <b>{stats['avg_data_found']:.1f}</b>
{services_text}
📸 <b>Статистика фотографий:</b>
• VIN с фото: <b>{stats['photos_stats']['vins_with_photos']:,}</b>
Всего фотографий: <b>{stats['photos_stats']['total_photos']:,}</b>
• Среднее фото на VIN: <b>{stats['photos_stats']['avg_photos_per_vin']:.1f}</b>
{peak_hours_text}
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_operations")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating performance stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_ops_errors")
async def admin_ops_errors_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ ошибок системы"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_ops_errors_stats()
# Ошибки по услугам
service_errors_text = ""
if stats['service_errors_breakdown']:
service_errors_text = "\n🚨 <b>Ошибки по услугам:</b>\n"
service_names = {
'decode_vin': '🔍 Декодинг VIN',
'check_salvage': '💥 Проверка Salvage',
'get_photos': '📸 Получение фото'
}
for service, total_requests, errors, no_data, auto_refunds in stats['service_errors_breakdown']:
name = service_names.get(service, service)
error_rate = round(errors/total_requests*100, 1) if total_requests > 0 else 0
no_data_rate = round(no_data/total_requests*100, 1) if total_requests > 0 else 0
service_errors_text += f"{name}:\n"
service_errors_text += f" 📊 Запросов: <b>{total_requests:,}</b>\n"
service_errors_text += f" 🚨 Ошибки: <b>{errors}</b> ({error_rate}%)\n"
service_errors_text += f" 📭 Нет данных: <b>{no_data}</b> ({no_data_rate}%)\n"
service_errors_text += f" ↩️ Авто возвраты: <b>{auto_refunds}</b>\n\n"
# Частые ошибки
common_errors_text = ""
if stats['common_errors']:
common_errors_text = "\n🔍 <b>Частые ошибки:</b>\n"
for error_snippet, count in stats['common_errors']:
error_text = error_snippet[:60] + "..." if len(error_snippet) > 60 else error_snippet
common_errors_text += f"• <code>{error_text}</code> - <b>{count}</b> раз\n"
# Тренд ошибок
daily_errors_text = ""
if stats['daily_error_trend']:
daily_errors_text = "\n📈 <b>Тренд ошибок (7 дней):</b>\n"
for error_date, errors_count in stats['daily_error_trend']:
date_str = error_date.strftime('%d.%m') if hasattr(error_date, 'strftime') else str(error_date)
daily_errors_text += f"{date_str}: <b>{errors_count}</b> ошибок\n"
report = f"""🚨 <b>Анализ ошибок системы</b>
📊 <b>Общая статистика:</b>
Всего попыток: <b>{stats['total_attempts']:,}</b>
• Ошибки платежей: <b>{stats['payment_failures']:,}</b>
• Ошибки сервисов: <b>{stats['service_errors']:,}</b>
• Случаи "нет данных": <b>{stats['no_data_cases']:,}</b>
• <b>Общий процент ошибок: {stats['error_rate']:.2f}%</b>
• Авто возвраты: <b>{stats['auto_refunds']:,}</b>
{service_errors_text}
{common_errors_text}
{daily_errors_text}
💡 <b>Анализ:</b>
• Нормальный уровень ошибок: &lt;5%
• Требует внимания: 5-10%
• Критично: &gt;10%
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_operations")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating errors stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_ops_load")
async def admin_ops_load_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Мониторинг нагрузки системы"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_ops_load_stats()
# Распределение по часам
hourly_text = ""
if stats['hourly_distribution']:
hourly_text = "\n⏰ <b>Нагрузка по часам (UTC):</b>\n"
for hour, requests, unique_users, avg_data in stats['hourly_distribution'][:8]: # Топ 8
hourly_text += f"{int(hour):02d}:00 - <b>{requests}</b> запр. ({unique_users} польз., ср.данных: {avg_data})\n"
# Популярные VIN
popular_vins_text = ""
if stats['popular_vins']:
popular_vins_text = "\n🏆 <b>Популярные VIN (топ-10):</b>\n"
for vin, request_count, unique_users, successful in stats['popular_vins'][:10]:
success_rate = round(successful/request_count*100, 1) if request_count > 0 else 0
popular_vins_text += f"• <code>{vin}</code>: <b>{request_count}</b> запр. ({unique_users} польз., успех: {success_rate}%)\n"
# Concurrent пользователи
concurrent_text = ""
if stats['concurrent_users']:
concurrent_text = "\n👥 <b>Concurrent активность (топ-6):</b>\n"
for hour_block, concurrent_users, total_requests in stats['concurrent_users'][:6]:
time_str = hour_block.strftime('%d.%m %H:00') if hasattr(hour_block, 'strftime') else str(hour_block)
concurrent_text += f"{time_str}: <b>{concurrent_users}</b> польз. одновременно ({total_requests} запр.)\n"
# Безопасное вычисление среднего запросов на пользователя
avg_requests_per_user = stats['avg_daily_requests'] / stats['avg_daily_users'] if stats['avg_daily_users'] > 0 else 0
report = f"""📈 <b>Мониторинг нагрузки</b>
📊 <b>Средние показатели:</b>
• Запросов в день: <b>{stats['avg_daily_requests']:.1f}</b>
• Пользователей в день: <b>{stats['avg_daily_users']:.1f}</b>
• Пиковый час: <b>{int(stats['peak_hour']):02d}:00 UTC</b>
• Макс. concurrent: <b>{stats['peak_concurrent']}</b> пользователей
{hourly_text}
{popular_vins_text}
{concurrent_text}
💡 <b>Insights:</b>
• Пиковая нагрузка в {int(stats['peak_hour']):02d}:00 UTC
• Среднее {avg_requests_per_user:.1f} запросов на пользователя
• Максимум {stats['peak_concurrent']} одновременных пользователей
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_operations")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating load stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_ops_auctions")
async def admin_ops_auctions_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ аукционных домов"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_ops_auctions_stats()
# Breakdown по аукционам
auctions_text = ""
if stats['auction_breakdown']:
auctions_text = "\n🏪 <b>Статистика аукционов:</b>\n"
for auction_house, records, with_title, with_damage1, with_damage2, min_year, max_year, avg_year in stats['auction_breakdown']:
title_coverage = round(with_title/records*100, 1) if records > 0 else 0
damage_coverage = round(with_damage1/records*100, 1) if records > 0 else 0
auctions_text += f"• <b>{auction_house}</b>:\n"
auctions_text += f" 📊 Записей: <b>{records:,}</b>\n"
auctions_text += f" 📝 Покрытие названий: <b>{title_coverage}%</b>\n"
auctions_text += f" 💥 Покрытие повреждений: <b>{damage_coverage}%</b>\n"
auctions_text += f" 🚗 Годы: {min_year}-{max_year} (ср: {avg_year})\n\n"
# Качество данных
quality_text = ""
if stats['data_quality']:
quality_text = "\n📋 <b>Качество данных:</b>\n"
for auction_house, title_cov, damage1_cov, odometer_cov in stats['data_quality']:
quality_text += f"{auction_house}:\n"
quality_text += f" Названия: <b>{title_cov}%</b> | Повреждения: <b>{damage1_cov}%</b> | Пробег: <b>{odometer_cov}%</b>\n"
# География
geo_text = ""
if stats['geographic_distribution']:
geo_text = "\n🗺️ <b>География (топ-8):</b>\n"
for auction_house, state_code, count in stats['geographic_distribution'][:8]:
geo_text += f"{auction_house} - {state_code}: <b>{count:,}</b> записей\n"
# Тренды обновлений
updates_text = ""
if stats['update_trends']:
updates_text = "\n📅 <b>Тренды обновлений (6 мес.):</b>\n"
for auction_house, month, updates_count in stats['update_trends'][:6]:
updates_text += f"{auction_house} {month}: <b>{updates_count:,}</b> обновлений\n"
report = f"""🏪 <b>Анализ аукционных домов</b>
📊 <b>Общая статистика:</b>
Всего записей: <b>{stats['total_records']:,}</b>
• Доля IAAI: <b>{stats['iaai_percentage']:.1f}%</b>
• Остальные источники: <b>{100-stats['iaai_percentage']:.1f}%</b>
{auctions_text}
{quality_text}
{geo_text}
{updates_text}
💡 <b>Выводы:</b>
• IAAI - основной источник данных ({stats['iaai_percentage']:.0f}%)
• Качество данных варьируется по источникам
• Регулярные обновления поддерживают актуальность
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_operations")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating auctions stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_ops_problem_vins")
async def admin_ops_problem_vins_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Анализ проблемных VIN номеров"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_ops_problem_vins_stats()
# Проверяем, что статистика получена корректно
if not stats or 'availability_stats' not in stats:
report = """🔍 <b>Проблемные VIN номера</b>
📭 <b>Нет данных</b>
В системе пока нет завершенных транзакций для анализа проблемных VIN.
💡 <b>Возможные причины:</b>
• Таблица payment_logs пустая
• Нет записей со статусом 'completed'
• Проблемы с подключением к базе данных
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
else:
# VIN без данных
no_data_text = ""
if stats.get('no_data_vins'):
no_data_text = "\n📭 <b>VIN без данных (топ-10):</b>\n"
for vin, request_count, unique_users, last_request in stats['no_data_vins'][:10]:
date_str = last_request.strftime('%d.%m') if hasattr(last_request, 'strftime') else str(last_request)
no_data_text += f"• <code>{vin}</code>: <b>{request_count}</b> запр. ({unique_users} польз., посл: {date_str})\n"
# VIN с ошибками
error_vins_text = ""
if stats.get('error_vins'):
error_vins_text = "\n🚨 <b>VIN с ошибками (топ-8):</b>\n"
for vin, error_count, affected_users, error_sample in stats['error_vins'][:8]:
error_text = (error_sample[:40] + "...") if error_sample and len(error_sample) > 40 else (error_sample or "No error message")
error_vins_text += f"• <code>{vin}</code>: <b>{error_count}</b> ошибок ({affected_users} польз.)\n"
if error_sample and error_sample != "No error message":
error_vins_text += f" Пример: <code>{error_text}</code>\n"
# VIN без фотографий
no_photos_text = ""
if stats.get('no_photos_vins'):
no_photos_text = "\n📸 <b>VIN без фото (топ-8):</b>\n"
for vin, photo_requests, unique_users in stats['no_photos_vins'][:8]:
no_photos_text += f"• <code>{vin}</code>: <b>{photo_requests}</b> запр. фото ({unique_users} польз.)\n"
# Сводная статистика проблемных VIN
problem_summary_text = ""
if stats.get('problem_vins_summary'):
problem_summary_text = "\n🔍 <b>Топ проблемных VIN:</b>\n"
for vin, total_req, successful, no_data, errors, refunds in stats['problem_vins_summary'][:5]:
problem_rate = round((no_data + errors)/total_req*100, 1) if total_req > 0 else 0
problem_summary_text += f"• <code>{vin}</code>: {total_req} запр. (проблемы: {problem_rate}%, возвраты: {refunds})\n"
availability = stats['availability_stats']
# Если нет проблемных данных, показываем это
if (not stats.get('no_data_vins') and
not stats.get('error_vins') and
not stats.get('no_photos_vins') and
not stats.get('problem_vins_summary')):
no_data_text = "\n✅ <b>Нет VIN без данных с множественными запросами</b>"
error_vins_text = "\n✅ <b>Нет VIN с системными ошибками</b>"
no_photos_text = "\n✅ <b>Нет проблем с запросами фотографий</b>"
problem_summary_text = "\n✅ <b>Все VIN обрабатываются корректно</b>"
report = f"""🔍 <b>Проблемные VIN номера</b>
📊 <b>Общая доступность:</b>
Всего уникальных VIN: <b>{availability['total_requested_vins']:,}</b>
• Успешные VIN: <b>{availability['successful_vins']:,}</b>
• <b>Успешность: {availability['success_rate']:.1f}%</b>
• VIN без данных: <b>{availability['no_data_vins']:,}</b>
• VIN с ошибками: <b>{availability['error_vins']:,}</b>
{no_data_text}
{error_vins_text}
{no_photos_text}
{problem_summary_text}
💡 <b>Рекомендации:</b>
• Проверить источники для VIN без данных
• Исследовать причины ошибок для проблемных VIN
• Пополнить базу фотографий для популярных VIN
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_operations")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating problem VINs stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data == "admin_ops_monitoring")
async def admin_ops_monitoring_callback(callback: CallbackQuery, db: OracleDatabase = None):
"""Системный мониторинг"""
database = db or oracle_db
if callback.from_user.id != ADMIN_USER_ID:
await callback.answer("❌ Access denied.", show_alert=True)
return
try:
stats = await database.get_ops_monitoring_stats()
# Здоровье БД
db_health_text = ""
if stats['db_health']:
db_health_text = "\n🗄️ <b>Здоровье БД:</b>\n"
for table_name, record_count, oldest, newest in stats['db_health']:
oldest_str = oldest.strftime('%d.%m.%Y') if hasattr(oldest, 'strftime') else str(oldest)
newest_str = newest.strftime('%d.%m.%Y') if hasattr(newest, 'strftime') else str(newest)
db_health_text += f"{table_name}: <b>{record_count:,}</b> записей ({oldest_str} - {newest_str})\n"
# Рост данных
growth_text = ""
if stats['data_growth']:
growth_text = "\n📈 <b>Рост данных (6 мес.):</b>\n"
for month, new_transactions, new_users, revenue in stats['data_growth'][:6]:
growth_text += f"{month}: <b>{new_transactions}</b> транз., <b>{new_users}</b> польз., <b>{revenue:.0f}</b> ⭐\n"
# SLA метрики
sla_text = ""
if stats['sla_metrics']:
sla_text = "\n📊 <b>SLA метрики (7 дней):</b>\n"
for service_date, total_req, successful_req, failed_req, availability in stats['sla_metrics'][:7]:
date_str = service_date.strftime('%d.%m') if hasattr(service_date, 'strftime') else str(service_date)
status = "🟢" if availability >= 95 else "🟡" if availability >= 90 else "🔴"
sla_text += f"{date_str}: {status} <b>{availability:.1f}%</b> ({successful_req}/{total_req})\n"
# Capacity изображений
images_text = ""
if stats['images_capacity']:
images_text = "\n📸 <b>Рост изображений (6 мес.):</b>\n"
for month, new_images, new_vins in stats['images_capacity'][:6]:
images_text += f"{month}: <b>{new_images:,}</b> фото, <b>{new_vins:,}</b> новых VIN\n"
# Performance метрики
performance_text = ""
if stats['performance_metrics']:
performance_text = "\n⚡ <b>Performance (7 дней):</b>\n"
for service_type, avg_results, total_requests, last_request in stats['performance_metrics']:
service_names = {
'decode_vin': '🔍 Декодинг',
'check_salvage': '💥 Salvage',
'get_photos': '📸 Фото'
}
name = service_names.get(service_type, service_type)
performance_text += f"{name}: <b>{avg_results:.1f}</b> ср.результатов ({total_requests} запр.)\n"
# Статус системы
status_emoji = "🟢" if stats['system_status'] == "Healthy" else "🟡" if stats['system_status'] == "Warning" else "🔴"
report = f"""👀 <b>Системный мониторинг</b>
🎯 <b>Статус системы: {status_emoji} {stats['system_status']}</b>
• Средний SLA: <b>{stats['avg_sla']:.2f}%</b>
{db_health_text}
{growth_text}
{sla_text}
{images_text}
{performance_text}
💡 <b>Анализ системы:</b>
• SLA ≥95%: Отличное состояние 🟢
• SLA 90-95%: Требует внимания 🟡
• SLA &lt;90%: Критичное состояние 🔴
📅 <b>Сгенерировано:</b> {datetime.now().strftime('%d.%m.%Y %H:%M')}"""
builder = InlineKeyboardBuilder()
builder.button(text="🔙 Назад", callback_data="admin_operations")
builder.button(text="🏠 Main Menu", callback_data="main_menu")
builder.adjust(2)
await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML")
await callback.answer()
except Exception as e:
logging.error(f"Error generating monitoring stats: {e}")
await callback.answer("❌ Ошибка генерации отчета", show_alert=True)
@dp.callback_query(lambda c: c.data and c.data.startswith("pay_detailed_info:"))
async def pay_detailed_info_callback(callback: CallbackQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при инициации платежа
await database.save_user(callback.from_user, "payment_initiation")
# Extract VIN from callback data
vin = callback.data.split(":")[1]
prices = [LabeledPrice(label="Detailed VIN Report", amount=DECODE_PRICE)]
logging.info(f"Sending invoice for VIN: {vin}")
await callback.bot.send_invoice(
chat_id=callback.message.chat.id,
title="Detailed VIN Information",
description=f"Get comprehensive vehicle detailed information for {DECODE_PRICE} Telegram Star",
payload=f"detailed_vin_info:{vin}", # Include VIN in payload
provider_token="", # Empty for Telegram Stars
currency="XTR", # Telegram Stars currency
prices=prices
)
await callback.answer()
@dp.message(VinStates.waiting_for_vin)
async def process_vin(message: Message, state: FSMContext, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при обработке VIN
await database.save_user(message.from_user, "vin_processing")
vin = message.text.strip().upper()
if len(vin) == 17 and vin.isalnum() and all(c not in vin for c in ["I", "O", "Q"]):
try:
make, model, year, cnt = await database.fetch_vin_info(vin)
logging.info(f"Decode VIN 1st step: make: {make}, model: {model}, year: {year}, cnt: {cnt}")
logging.info(f"VIN decode check: make==UNKNOWN: {make == 'UNKNOWN'}, model==UNKNOWN: {model == 'UNKNOWN'}, year==UNKNOWN: {year == 'UNKNOWN'}")
logging.info(f"All UNKNOWN check: {make == 'UNKNOWN' and model == 'UNKNOWN' and year == 'UNKNOWN'}")
logging.info(f"cnt == 0 check: {cnt == 0}")
# Формируем текст ответа
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
logging.info("Setting response_text to 'Unable to decode VIN' because all fields are UNKNOWN")
response_text = "❌ **Unable to decode VIN, possibly incorrect**"
else:
logging.info(f"VIN successfully decoded! Setting response_text to car info: {year} {make} {model}")
response_text = f"🚗 **{year} {make} {model}**\n\n"
# Create keyboard based on cnt value
builder = InlineKeyboardBuilder()
builder.button(text="Try another VIN", callback_data="decode_vin")
builder.button(text="Back to Main Menu", callback_data="main_menu")
if cnt > 9 and not (make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN"):
logging.info("Adding detailed info button because cnt > 9 and VIN is decoded")
builder.button(text=f"Get detailed info. Pay {DECODE_PRICE} ⭐️", callback_data=f"pay_detailed_info:{vin}", pay=True)
builder.adjust(1, 1, 1) # Each button on separate row
else:
logging.info(f"Not adding detailed info button because cnt <= 9 (cnt={cnt}) or VIN not decoded")
builder.adjust(1, 1) # Each button on separate row
logging.info(f"Final response_text before sending in decode VIN: '{response_text}'")
logging.info(f"Response text length: {len(response_text)}")
await message.answer(response_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
except Exception as e:
logging.error(f"Database error for VIN {vin}: {e}")
await message.answer("Error retrieving data from database. Please try again later.")
await state.clear()
else:
await message.answer("Invalid VIN. Please enter a valid 17-character VIN (letters and numbers, no I, O, Q).")
@dp.message(VinStates.waiting_for_check_vin)
async def process_check_vin(message: Message, state: FSMContext, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при обработке VIN
await database.save_user(message.from_user, "check_vin_processing")
vin = message.text.strip().upper()
if len(vin) == 17 and vin.isalnum() and all(c not in vin for c in ["I", "O", "Q"]):
try:
# Получаем базовую информацию о VIN
make, model, year, cnt = await database.fetch_vin_info(vin)
# Получаем количество записей в salvagedb.salvagedb
salvage_count = await database.count_salvage_records(vin)
logging.info(f"Check VIN: make: {make}, model: {model}, year: {year}, cnt: {cnt}, salvage_count: {salvage_count}")
logging.info(f"VIN decode check: make==UNKNOWN: {make == 'UNKNOWN'}, model==UNKNOWN: {model == 'UNKNOWN'}, year==UNKNOWN: {year == 'UNKNOWN'}")
logging.info(f"All UNKNOWN check: {make == 'UNKNOWN' and model == 'UNKNOWN' and year == 'UNKNOWN'}")
logging.info(f"cnt == 0 check: {cnt == 0}")
# Формируем текст ответа
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
logging.info("Setting response_text to 'Unable to decode VIN' because all fields are UNKNOWN")
response_text = "❌ **Unable to decode VIN, possibly incorrect**\n\n"
else:
logging.info(f"VIN successfully decoded! Setting response_text to car info: {year} {make} {model}")
response_text = f"🚗 **{year} {make} {model}**\n\n"
response_text += f"📊 **Records found in database:** {salvage_count}\n\n"
# Создаем клавиатуру в зависимости от наличия записей
builder = InlineKeyboardBuilder()
if salvage_count > 0:
# Есть записи - показываем кнопки: Pay 10⭐ for detailed info, Try another VIN, Back to main menu
builder.button(text=f"Pay {CHECK_PRICE} ⭐️ for detailed info", callback_data=f"pay_check_detailed:{vin}", pay=True)
builder.button(text="Try another VIN", callback_data="check_vin")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(1, 1, 1) # Each button on separate row
else:
# Нет записей - показываем кнопки: Try another VIN, Back to main menu
response_text += " **No salvage records found for this VIN**"
builder.button(text="Try another VIN", callback_data="check_vin")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(1, 1) # Each button on separate row
logging.info(f"Final response_text before sending: '{response_text}'")
logging.info(f"Response text length: {len(response_text)}")
await message.answer(response_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
except Exception as e:
logging.error(f"Database error for check VIN {vin}: {e}")
await message.answer("Error retrieving data from database. Please try again later.")
await state.clear()
else:
await message.answer("Invalid VIN. Please enter a valid 17-character VIN (letters and numbers, no I, O, Q).")
@dp.callback_query(lambda c: c.data and c.data.startswith("pay_check_detailed:"))
async def pay_check_detailed_callback(callback: CallbackQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при инициации платежа
await database.save_user(callback.from_user, "check_payment_initiation")
# Извлекаем VIN из callback data
vin = callback.data.split(":")[1]
prices = [LabeledPrice(label="Detailed Salvage Report", amount=CHECK_PRICE)]
logging.info(f"Sending invoice for salvage check VIN: {vin}")
await callback.bot.send_invoice(
chat_id=callback.message.chat.id,
title="Detailed Salvage Report",
description=f"Get comprehensive salvage history and damage information for {CHECK_PRICE} Telegram Stars",
payload=f"detailed_salvage_check:{vin}", # Уникальный payload для этого типа платежа
provider_token="", # Empty for Telegram Stars
currency="XTR", # Telegram Stars currency
prices=prices
)
await callback.answer()
@dp.callback_query(lambda c: c.data and c.data.startswith("pay_photos:"))
async def pay_photos_callback(callback: CallbackQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при инициации платежа
await database.save_user(callback.from_user, "photos_payment_initiation")
# Извлекаем VIN из callback data
vin = callback.data.split(":")[1]
prices = [LabeledPrice(label="Vehicle Damage Photos", amount=IMG_PRICE)]
logging.info(f"Sending invoice for photos VIN: {vin}")
await callback.bot.send_invoice(
chat_id=callback.message.chat.id,
title="Vehicle Damage Photos",
description=f"Get access to all damage photos for this vehicle for {IMG_PRICE} Telegram Stars",
payload=f"vehicle_photos:{vin}", # Уникальный payload для фотографий
provider_token="", # Empty for Telegram Stars
currency="XTR", # Telegram Stars currency
prices=prices
)
await callback.answer()
@dp.pre_checkout_query()
async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при pre-checkout
await database.save_user(pre_checkout_query.from_user, "pre_checkout")
await pre_checkout_query.answer(ok=True)
@dp.message(Command("admin_stats"))
async def admin_stats_handler(message: Message, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Проверяем, является ли пользователь администратором
if message.from_user.id != ADMIN_USER_ID:
await message.answer("❌ Access denied. This command is for administrators only.")
return
try:
# Получаем общую статистику
stats = await database.get_users_summary()
# Формируем отчет
report = f"""
📊 **Bot Users Statistics**
👥 **Users Overview:**
• Total users: {stats.get('total_users', 0)}
• Premium users: {stats.get('premium_users', 0)}
💰 **Revenue:**
• Total revenue: {stats.get('total_revenue', 0)} ⭐️
• Total transactions: {stats.get('total_transactions', 0)}
📈 **Activity:**
• Active last 24h: {stats.get('active_last_24h', 0)}
• Active last week: {stats.get('active_last_week', 0)}
📅 **Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
"""
await message.answer(report, parse_mode="Markdown")
except Exception as e:
logging.error(f"Error generating admin stats: {e}")
await message.answer("❌ Error generating statistics. Please try again later.")
@dp.message(lambda message: message.successful_payment)
async def successful_payment_handler(message: Message, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные о платеже пользователя
await database.save_user(message.from_user, "successful_payment")
payload = message.successful_payment.invoice_payload
# Определяем сумму платежа в зависимости от типа
if payload.startswith("detailed_salvage_check:"):
payment_amount = float(CHECK_PRICE)
elif payload.startswith("vehicle_photos:"):
payment_amount = float(IMG_PRICE)
else:
payment_amount = float(DECODE_PRICE)
await database.update_user_payment(message.from_user.id, payment_amount)
# Подготавливаем базовые данные платежа для логирования
payment_data = {
'amount': payment_amount,
'transaction_id': message.successful_payment.telegram_payment_charge_id,
'status': 'completed',
'currency': 'XTR',
'refund_status': 'no_refund'
}
if payload.startswith("detailed_vin_info:"):
vin = payload.split(":")[1]
try:
# Get detailed information from database
detailed_info = await database.fetch_detailed_vin_info(vin)
if detailed_info and detailed_info['all_params']:
params = detailed_info['all_params']
# Format the detailed report with all categories
report = f"🚗 **{params.get('model_year', 'N/A')} {params.get('make', 'N/A')} {params.get('model', 'N/A')}**\n"
if params.get('trim'):
report += f"{params.get('trim')}\n"
report += "\n"
# BASIC CHARACTERISTICS
if detailed_info['basic_characteristics']:
report += "📋 **BASIC CHARACTERISTICS**\n"
for key, data in detailed_info['basic_characteristics'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# ENGINE AND POWERTRAIN
if detailed_info['engine_and_powertrain']:
report += "🔧 **ENGINE AND POWERTRAIN**\n"
for key, data in detailed_info['engine_and_powertrain'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# TRANSMISSION
if detailed_info['transmission']:
report += "⚙️ **TRANSMISSION**\n"
for key, data in detailed_info['transmission'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# ACTIVE SAFETY
if detailed_info['active_safety']:
report += "🛡️ **ACTIVE SAFETY**\n"
for key, data in detailed_info['active_safety'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# PASSIVE SAFETY
if detailed_info['passive_safety']:
report += "🚗 **PASSIVE SAFETY**\n"
for key, data in detailed_info['passive_safety'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# DIMENSIONS AND CONSTRUCTION
if detailed_info['dimensions_and_construction']:
report += "📏 **DIMENSIONS AND CONSTRUCTION**\n"
for key, data in detailed_info['dimensions_and_construction'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# BRAKE SYSTEM
if detailed_info['brake_system']:
report += "🔧 **BRAKE SYSTEM**\n"
for key, data in detailed_info['brake_system'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# LIGHTING
if detailed_info['lighting']:
report += "💡 **LIGHTING**\n"
for key, data in detailed_info['lighting'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# ADDITIONAL FEATURES
if detailed_info['additional_features']:
report += "✨ **ADDITIONAL FEATURES**\n"
for key, data in detailed_info['additional_features'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# MANUFACTURING AND LOCALIZATION
if detailed_info['manufacturing_and_localization']:
report += "🏭 **MANUFACTURING AND LOCALIZATION**\n"
for key, data in detailed_info['manufacturing_and_localization'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# NCSA DATA
if detailed_info['ncsa_data']:
report += "📊 **NCSA DATA**\n"
for key, data in detailed_info['ncsa_data'].items():
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
# TECHNICAL INFORMATION AND ERRORS
if detailed_info['technical_information_and_errors']:
report += "⚠️ **TECHNICAL INFORMATION AND ERRORS**\n"
for key, data in detailed_info['technical_information_and_errors'].items():
if data['value'] == "0":
report += "✅ **No errors found**\n"
else:
report += f"• **{data['param_name']}:** {data['value']}\n"
report += "\n"
report += "---\n"
report += f"📋 **VIN:** {vin}\n"
report += f"💰 **Transaction ID:** {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
report += "⚠️ **This report shows salvage/damage history. Please consult with automotive experts for vehicle evaluation.**"
# Create keyboard with action buttons
builder = InlineKeyboardBuilder()
builder.button(text="Try another VIN", callback_data="decode_vin")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(2) # Two buttons on one row
logging.info("Attempting to send message with Markdown...")
try:
await message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown")
logging.info("Message sent successfully!")
except Exception as markdown_error:
logging.error(f"Markdown parsing failed: {markdown_error}")
logging.info("Attempting to send without Markdown...")
# Удаляем markdown символы и отправляем как обычный текст
plain_report = report.replace("**", "").replace("*", "")
await message.answer(plain_report, reply_markup=builder.as_markup())
logging.info("Plain text message sent successfully!")
# Логируем успешную операцию DecodeVin
service_result = {
'status': 'success',
'data_count': len(detailed_info['all_params']),
'vehicle_make': params.get('make', 'N/A'),
'vehicle_model': params.get('model', 'N/A'),
'vehicle_year': params.get('model_year', 'N/A')
}
await database.save_payment_log(message.from_user, "decode_vin", vin, payment_data, service_result)
logging.info(f"Payment logged successfully for DecodeVin service - User: {message.from_user.id}, VIN: {vin}")
# Проверяем, является ли пользователь администратором и возвращаем звезды
if message.from_user.id == ADMIN_USER_ID:
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
payment_data['refund_status'] = 'admin_refund'
await message.answer(
"🔧 **Admin Refund**\n\n"
f"💰 Payment automatically refunded for admin user.\n"
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
" Admin access - no charges applied.",
parse_mode="Markdown"
)
logging.info(f"Admin refund successful for user {message.from_user.id}")
except Exception as refund_error:
logging.error(f"Failed to refund admin payment: {refund_error}")
await message.answer(
"⚠️ **Admin Refund Failed**\n\n"
"Could not automatically refund admin payment. Please contact technical support.\n"
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}",
parse_mode="Markdown"
)
else:
# No detailed information found - refund the payment
service_result = {
'status': 'no_data',
'data_count': 0,
'error': 'No detailed information found for VIN'
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'No detailed information found'
await database.save_payment_log(message.from_user, "decode_vin", vin, payment_data, service_result)
logging.info(f"Payment logged for DecodeVin no data case - User: {message.from_user.id}, VIN: {vin}")
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
await message.answer(
"❌ No detailed information found for this VIN in our database.\n"
"💰 Your payment has been automatically refunded.\n"
"Please verify the VIN and try again."
)
logging.info(f"Refund successful for user {message.from_user.id} - no data found for VIN {vin}")
except Exception as refund_error:
logging.error(f"Failed to refund payment for user {message.from_user.id}: {refund_error}")
await message.answer(
"❌ No detailed information found for this VIN.\n"
"⚠️ Please contact support with this transaction ID for a refund:\n"
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
)
except Exception as e:
logging.error(f"Error getting detailed VIN info for {vin}: {e}")
# Логируем ошибку
service_result = {
'status': 'error',
'data_count': 0,
'error': str(e)
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'Service error'
await database.save_payment_log(message.from_user, "decode_vin", vin, payment_data, service_result)
logging.info(f"Payment logged for DecodeVin error case - User: {message.from_user.id}, VIN: {vin}")
# Attempt to refund the payment due to service error
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
await message.answer(
"❌ Error retrieving detailed information from our database.\n"
"💰 Your payment has been automatically refunded.\n"
"Please try again later or contact support if the issue persists."
)
logging.info(f"Refund successful for user {message.from_user.id}, charge_id: {message.successful_payment.telegram_payment_charge_id}")
except Exception as refund_error:
logging.error(f"Failed to refund payment for user {message.from_user.id}: {refund_error}")
await message.answer(
"❌ Error retrieving detailed information from our database.\n"
"⚠️ Please contact support immediately with this transaction ID for a manual refund:\n"
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
)
elif payload.startswith("detailed_salvage_check:"):
vin = payload.split(":")[1]
try:
logging.info(f"=== DETAILED SALVAGE CHECK DEBUG START for VIN: {vin} ===")
# Получаем детальную информацию о salvage записях
salvage_records = await database.fetch_salvage_detailed_info(vin)
logging.info(f"Salvage records count: {len(salvage_records) if salvage_records else 0}")
if salvage_records:
# Логируем первую запись для отладки
if len(salvage_records) > 0:
logging.info(f"First record data: {salvage_records[0]}")
# Получаем базовую информацию о VIN для заголовка
make, model, year, cnt = await database.fetch_vin_info(vin)
logging.info(f"VIN info: make={make}, model={model}, year={year}, cnt={cnt}")
report = f"🚗 **{year} {make} {model}**\n"
report += f"📋 **VIN:** {escape_markdown(vin)}\n\n"
report += f"🔍 **DETAILED SALVAGE HISTORY REPORT**\n"
report += f"📊 **Total Records Found:** {len(salvage_records)}\n\n"
logging.info(f"Report header created, length: {len(report)}")
# Добавляем информацию по каждой записи
for idx, record in enumerate(salvage_records[:5], 1): # Показываем максимум 5 записей
logging.info(f"Processing record #{idx}: {record}")
record_text = f"📋 **Record #{idx}**\n"
# Дата продажи с красивым форматированием
if record['sale_date']:
formatted_date = format_sale_date(record['sale_date'])
logging.info(f"Formatted date: {formatted_date}")
record_text += f"📅 **Sale Date:** {formatted_date}\n"
# Основное повреждение
if record['dem1']:
logging.info(f"Primary damage: {record['dem1']}")
record_text += f"⚠️ **Primary Damage:** {escape_markdown(record['dem1'])}\n"
# Вторичное повреждение
if record['dem2']:
logging.info(f"Secondary damage: {record['dem2']}")
record_text += f"⚠️ **Secondary Damage:** {escape_markdown(record['dem2'])}\n"
# Одометр
if record['odo']:
try:
odo_value = int(record['odo']) if record['odo'] else 0
odo_status = f" ({record['odos']})" if record['odos'] else ""
logging.info(f"Odometer: {odo_value}, status: {record['odos']}")
record_text += f"🛣️ **Odometer:** {odo_value:,} miles{odo_status}\n"
except (ValueError, TypeError):
logging.info(f"Odometer parsing error for: {record['odo']}")
record_text += f"🛣️ **Odometer:** {record['odo']}\n"
# Стоимость ремонта
if record['j_rep_cost']:
try:
repair_cost = float(record['j_rep_cost']) if record['j_rep_cost'] else 0
if repair_cost > 0:
logging.info(f"Repair cost: {repair_cost}")
record_text += f"💰 **Repair Cost:** ${repair_cost:,.2f}\n"
except (ValueError, TypeError):
logging.info(f"Repair cost parsing error for: {record['j_rep_cost']}")
record_text += f"💰 **Repair Cost:** {record['j_rep_cost']}\n"
# Состояние двигателя/движения
if record['j_runs_drive']:
logging.info(f"Engine status: {record['j_runs_drive']}")
record_text += f"🔧 **Engine Status:** {escape_markdown(record['j_runs_drive'])}\n"
# Локация с конвертированными штатами
if record['j_locate']:
formatted_location = parse_location(record['j_locate'])
logging.info(f"Location: {record['j_locate']} -> {formatted_location}")
record_text += f"📍 **Sale Location:** {formatted_location}\n"
record_text += "\n"
report += record_text
logging.info(f"Record #{idx} text length: {len(record_text)}")
if len(salvage_records) > 5:
report += f"📋 **... and {len(salvage_records) - 5} more records**\n\n"
report += "---\n"
report += f"💰 **Transaction ID:** {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
report += "⚠️ **This report shows salvage/damage history. Please consult with automotive experts for vehicle evaluation.**"
logging.info(f"Final report length: {len(report)}")
logging.info("=== REPORT CONTENT START ===")
logging.info(report)
logging.info("=== REPORT CONTENT END ===")
# Создаем клавиатуру
builder = InlineKeyboardBuilder()
builder.button(text="Try another VIN", callback_data="check_vin")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(2)
logging.info("Attempting to send message with Markdown...")
try:
await message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown")
logging.info("Message sent successfully!")
except Exception as markdown_error:
logging.error(f"Markdown parsing failed: {markdown_error}")
logging.info("Attempting to send without Markdown...")
# Удаляем markdown символы и отправляем как обычный текст
plain_report = report.replace("**", "").replace("*", "")
await message.answer(plain_report, reply_markup=builder.as_markup())
logging.info("Plain text message sent successfully!")
# Логируем успешную операцию CheckSalvage
service_result = {
'status': 'success',
'data_count': len(salvage_records),
'vehicle_make': make,
'vehicle_model': model,
'vehicle_year': year
}
await database.save_payment_log(message.from_user, "check_salvage", vin, payment_data, service_result)
logging.info(f"Payment logged successfully for CheckSalvage service - User: {message.from_user.id}, VIN: {vin}")
# Отправляем отдельное сообщение о фотографиях
if salvage_records and salvage_records[0]['img_count'] > 0:
img_count = salvage_records[0]['img_count']
photo_message = f"📸 **Photo Information**\n\n"
photo_message += f"🖼️ **{img_count} damage photos** found in our database for this vehicle.\n"
photo_message += f"These photos show the actual condition and damage of the vehicle during auction."
logging.info("Sending photo information message...")
await message.answer(photo_message, parse_mode="Markdown")
logging.info("Photo message sent successfully!")
# Проверяем, является ли пользователь администратором и возвращаем звезды
if message.from_user.id == ADMIN_USER_ID:
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
payment_data['refund_status'] = 'admin_refund'
await message.answer(
"🔧 **Admin Refund**\n\n"
f"💰 Payment automatically refunded for admin user.\n"
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
" Admin access - no charges applied.",
parse_mode="Markdown"
)
logging.info(f"Admin refund successful for user {message.from_user.id}")
except Exception as refund_error:
logging.error(f"Failed to refund admin payment: {refund_error}")
await message.answer(
"⚠️ **Admin Refund Failed**\n\n"
"Could not automatically refund admin payment. Please contact technical support.\n"
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}",
parse_mode="Markdown"
)
else:
# Нет записей - возвращаем деньги
service_result = {
'status': 'no_data',
'data_count': 0,
'error': 'No salvage records found for VIN'
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'No salvage records found'
await database.save_payment_log(message.from_user, "check_salvage", vin, payment_data, service_result)
logging.info(f"Payment logged for CheckSalvage no data case - User: {message.from_user.id}, VIN: {vin}")
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
await message.answer(
"❌ No salvage records found for this VIN in our database.\n"
"💰 Your payment has been automatically refunded.\n"
"This is actually good news - no salvage history found!"
)
logging.info(f"Refund successful for user {message.from_user.id} - no salvage data found for VIN {vin}")
except Exception as refund_error:
logging.error(f"Failed to refund payment for user {message.from_user.id}: {refund_error}")
await message.answer(
"❌ No salvage records found for this VIN.\n"
"⚠️ Please contact support with this transaction ID for a refund:\n"
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
)
except Exception as e:
logging.error(f"Error getting salvage info for {vin}: {e}")
# Логируем ошибку
service_result = {
'status': 'error',
'data_count': 0,
'error': str(e)
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'Service error'
await database.save_payment_log(message.from_user, "check_salvage", vin, payment_data, service_result)
logging.info(f"Payment logged for CheckSalvage error case - User: {message.from_user.id}, VIN: {vin}")
# Возвращаем деньги при ошибке
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
await message.answer(
"❌ Error retrieving salvage information from our database.\n"
"💰 Your payment has been automatically refunded.\n"
"Please try again later or contact support if the issue persists."
)
logging.info(f"Refund successful for user {message.from_user.id}, charge_id: {message.successful_payment.telegram_payment_charge_id}")
except Exception as refund_error:
logging.error(f"Failed to refund payment for user {message.from_user.id}: {refund_error}")
await message.answer(
"❌ Error retrieving salvage information from our database.\n"
"⚠️ Please contact support immediately with this transaction ID for a manual refund:\n"
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
)
elif payload.startswith("vehicle_photos:"):
vin = payload.split(":")[1]
try:
# Получаем информацию о VIN и количество фотографий
make, model, year, cnt = await database.fetch_vin_info(vin)
photo_count = await database.count_photo_records(vin)
logging.info(f"Photos payment for VIN: {vin}, make: {make}, model: {model}, year: {year}, photo_count: {photo_count}")
if photo_count > 0:
# Есть фотографии - предоставляем доступ (пока заглушка)
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
response_text = f"📸 **Vehicle Damage Photos**\n\n"
else:
response_text = f"🚗 **{year} {make} {model}**\n\n"
response_text += f"📸 **Vehicle Damage Photos**\n\n"
response_text += f"✅ **Payment successful!** You now have access to **{photo_count} damage photos** for this vehicle.\n\n"
response_text += f"📁 **Photos will be sent separately** - please wait while we prepare your images.\n\n"
response_text += f"---\n"
response_text += f"💰 **Transaction ID:** {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
response_text += f"📋 **VIN:** {escape_markdown(vin)}"
# Создаем клавиатуру с дополнительными действиями
builder = InlineKeyboardBuilder()
builder.button(text="Search another VIN", callback_data="search_car_photo")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(2)
logging.info("Attempting to send photos payment success message...")
try:
await message.answer(response_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
logging.info("Photos payment message sent successfully!")
# Получаем пути к фотографиям из базы данных
logging.info(f"Fetching photo paths for VIN: {vin}")
db_photo_paths = await database.fetch_photo_paths(vin)
logging.info(f"Found {len(db_photo_paths)} photo paths in database")
if db_photo_paths:
# Подготавливаем полные пути к файлам
full_photo_paths = prepare_photo_paths(db_photo_paths)
logging.info(f"Prepared {len(full_photo_paths)} full photo paths")
# Отправляем фотографии
await send_vehicle_photos(message, vin, full_photo_paths, make, model, year)
# Логируем успешную операцию GetPhotos
service_result = {
'status': 'success',
'data_count': len(full_photo_paths),
'vehicle_make': make,
'vehicle_model': model,
'vehicle_year': year
}
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged successfully for GetPhotos service - User: {message.from_user.id}, VIN: {vin}")
else:
await message.answer(
"⚠️ **Warning:** No photo paths found in database despite photo count > 0.\n"
"Please contact support with your transaction details."
)
logging.warning(f"No photo paths found for VIN {vin} despite photo_count = {photo_count}")
# Логируем проблему с путями к фотографиям
service_result = {
'status': 'error',
'data_count': 0,
'error': 'Photo paths not found despite photo count > 0'
}
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged for GetPhotos path error case - User: {message.from_user.id}, VIN: {vin}")
except Exception as markdown_error:
logging.error(f"Markdown parsing failed for photos payment: {markdown_error}")
plain_response = response_text.replace("**", "").replace("*", "")
await message.answer(plain_response, reply_markup=builder.as_markup())
logging.info("Plain text photos payment message sent successfully!")
# Получаем пути к фотографиям из базы данных (fallback)
logging.info(f"Fetching photo paths for VIN: {vin} (fallback)")
db_photo_paths = await database.fetch_photo_paths(vin)
logging.info(f"Found {len(db_photo_paths)} photo paths in database (fallback)")
if db_photo_paths:
# Подготавливаем полные пути к файлам
full_photo_paths = prepare_photo_paths(db_photo_paths)
logging.info(f"Prepared {len(full_photo_paths)} full photo paths (fallback)")
# Отправляем фотографии
await send_vehicle_photos(message, vin, full_photo_paths, make, model, year)
# Логируем успешную операцию GetPhotos (fallback)
service_result = {
'status': 'success',
'data_count': len(full_photo_paths),
'vehicle_make': make,
'vehicle_model': model,
'vehicle_year': year
}
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged successfully for GetPhotos service (fallback) - User: {message.from_user.id}, VIN: {vin}")
else:
await message.answer(
"Warning: No photo paths found in database despite photo count > 0. "
"Please contact support with your transaction details."
)
logging.warning(f"No photo paths found for VIN {vin} despite photo_count = {photo_count} (fallback)")
# Логируем проблему с путями к фотографиям (fallback)
service_result = {
'status': 'error',
'data_count': 0,
'error': 'Photo paths not found despite photo count > 0 (fallback)'
}
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged for GetPhotos fallback path error case - User: {message.from_user.id}, VIN: {vin}")
# Проверяем, является ли пользователь администратором и возвращаем звезды
if message.from_user.id == ADMIN_USER_ID:
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
payment_data['refund_status'] = 'admin_refund'
await message.answer(
"🔧 **Admin Refund**\n\n"
f"💰 Payment automatically refunded for admin user.\n"
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
" Admin access - no charges applied.",
parse_mode="Markdown"
)
logging.info(f"Admin refund successful for user {message.from_user.id}")
except Exception as refund_error:
logging.error(f"Failed to refund admin payment: {refund_error}")
await message.answer(
"⚠️ **Admin Refund Failed**\n\n"
"Could not automatically refund admin payment. Please contact technical support.\n"
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}",
parse_mode="Markdown"
)
else:
# Нет фотографий - возвращаем деньги
service_result = {
'status': 'no_data',
'data_count': 0,
'error': 'No photos found for VIN'
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'No photos found'
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged for GetPhotos no data case - User: {message.from_user.id}, VIN: {vin}")
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
await message.answer(
"❌ No photos found for this VIN in our database.\n"
"💰 Your payment has been automatically refunded.\n"
"Please try another VIN or contact support if you believe this is an error."
)
logging.info(f"Refund successful for user {message.from_user.id} - no photos found for VIN {vin}")
except Exception as refund_error:
logging.error(f"Failed to refund payment for user {message.from_user.id}: {refund_error}")
await message.answer(
"❌ No photos found for this VIN.\n"
"⚠️ Please contact support with this transaction ID for a refund:\n"
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
)
except Exception as e:
logging.error(f"Error getting photos info for {vin}: {e}")
# Логируем ошибку
service_result = {
'status': 'error',
'data_count': 0,
'error': str(e)
}
payment_data['refund_status'] = 'auto_refund'
payment_data['refund_reason'] = 'Service error'
await database.save_payment_log(message.from_user, "get_photos", vin, payment_data, service_result)
logging.info(f"Payment logged for GetPhotos error case - User: {message.from_user.id}, VIN: {vin}")
# Возвращаем деньги при ошибке
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
await message.answer(
"❌ Error retrieving photos information from our database.\n"
"💰 Your payment has been automatically refunded.\n"
"Please try again later or contact support if the issue persists."
)
logging.info(f"Refund successful for user {message.from_user.id}, charge_id: {message.successful_payment.telegram_payment_charge_id}")
except Exception as refund_error:
logging.error(f"Failed to refund payment for user {message.from_user.id}: {refund_error}")
await message.answer(
"❌ Error retrieving photos information from our database.\n"
"⚠️ Please contact support immediately with this transaction ID for a manual refund:\n"
f"🆔 {message.successful_payment.telegram_payment_charge_id}"
)
else:
await message.answer(
f"✅ Payment successful! Thank you for your purchase.\n"
f"Transaction ID: {message.successful_payment.telegram_payment_charge_id}"
)
# Проверяем, является ли пользователь администратором и возвращаем звезды
if message.from_user.id == ADMIN_USER_ID:
try:
await message.bot.refund_star_payment(
user_id=message.from_user.id,
telegram_payment_charge_id=message.successful_payment.telegram_payment_charge_id
)
await message.answer(
"🔧 **Admin Refund**\n\n"
f"💰 Payment automatically refunded for admin user.\n"
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}\n"
" Admin access - no charges applied.",
parse_mode="Markdown"
)
logging.info(f"Admin refund successful for user {message.from_user.id}")
except Exception as refund_error:
logging.error(f"Failed to refund admin payment: {refund_error}")
await message.answer(
"⚠️ **Admin Refund Failed**\n\n"
"Could not automatically refund admin payment. Please contact technical support.\n"
f"🆔 Transaction ID: {escape_markdown(message.successful_payment.telegram_payment_charge_id)}",
parse_mode="Markdown"
)
@dp.message(VinStates.waiting_for_photo_vin)
async def process_photo_vin(message: Message, state: FSMContext, db: OracleDatabase = None):
# Используем переданный db или глобальный oracle_db
database = db or oracle_db
# Сохраняем данные пользователя при обработке VIN
await database.save_user(message.from_user, "photo_vin_processing")
vin = message.text.strip().upper()
if len(vin) == 17 and vin.isalnum() and all(c not in vin for c in ["I", "O", "Q"]):
try:
# Получаем базовую информацию о VIN для заголовка
make, model, year, cnt = await database.fetch_vin_info(vin)
# Получаем количество фотографий
photo_count = await database.count_photo_records(vin)
logging.info(f"Photo search VIN: make: {make}, model: {model}, year: {year}, photo_count: {photo_count}")
# Формируем ответ в зависимости от наличия фотографий
builder = InlineKeyboardBuilder()
if photo_count > 0:
# Есть фотографии - показываем информацию и кнопку оплаты
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
response_text = f"📸 **Photo Information**\n\n"
else:
response_text = f"🚗 **{year} {make} {model}**\n\n"
response_text += f"📸 **Photo Information**\n\n"
response_text += f"🖼️ **{photo_count} damage photos** found in our database for this vehicle.\n"
response_text += f"These photos show the actual condition and damage of the vehicle during auction."
builder.button(text=f"Pay {IMG_PRICE} ⭐️ for photos", callback_data=f"pay_photos:{vin}", pay=True)
builder.button(text="Try another VIN", callback_data="search_car_photo")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(1, 1, 1) # Each button on separate row
else:
# Нет фотографий
if make == "UNKNOWN" and model == "UNKNOWN" and year == "UNKNOWN":
response_text = f"❌ **Unable to decode VIN or no photos found**\n\n"
else:
response_text = f"🚗 **{year} {make} {model}**\n\n"
response_text += f"📸 **No damage photos found** for this VIN in our database."
builder.button(text="Try another VIN", callback_data="search_car_photo")
builder.button(text="Back to Main Menu", callback_data="main_menu")
builder.adjust(1, 1) # Each button on separate row
await message.answer(response_text, reply_markup=builder.as_markup(), parse_mode="Markdown")
except Exception as e:
logging.error(f"Database error for photo search VIN {vin}: {e}")
await message.answer("Error retrieving data from database. Please try again later.")
await state.clear()
else:
await message.answer("Invalid VIN. Please enter a valid 17-character VIN (letters and numbers, no I, O, Q).")
async def on_startup():
await oracle_db.connect()
# Регистрируем middleware для всех типов событий
dp.message.middleware(DbSessionMiddleware(oracle_db))
dp.callback_query.middleware(DbSessionMiddleware(oracle_db))
dp.pre_checkout_query.middleware(DbSessionMiddleware(oracle_db))
async def on_shutdown():
await oracle_db.close()
# Run the bot
async def main() -> None:
# Создаем обработчик сигналов для корректного выхода
stop_event = asyncio.Event()
def signal_handler(signum, frame):
"""Обработчик сигнала для корректного завершения программы"""
logging.info(f"Получен сигнал {signum}. Инициируется корректное завершение программы...")
stop_event.set()
# Регистрируем обработчики сигналов (только для Unix-систем)
if not is_windows():
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
logging.info("Обработчики сигналов зарегистрированы")
else:
# Для Windows используем другой подход
logging.info("Система Windows - используется базовый обработчик KeyboardInterrupt")
try:
bot = Bot(token=TOKEN)
dp.startup.register(on_startup)
dp.shutdown.register(on_shutdown)
# Запускаем polling с обработкой KeyboardInterrupt
if not is_windows():
# Unix-системы
polling_task = asyncio.create_task(dp.start_polling(bot))
stop_task = asyncio.create_task(stop_event.wait())
# Ждем завершения любой из задач
done, pending = await asyncio.wait(
{polling_task, stop_task},
return_when=asyncio.FIRST_COMPLETED
)
# Отменяем оставшиеся задачи
for task in pending:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
else:
# Windows
await dp.start_polling(bot)
except KeyboardInterrupt:
logging.info("Получен KeyboardInterrupt. Завершение программы...")
except Exception as e:
logging.error(f"Критическая ошибка в main(): {e}")
raise
finally:
logging.info("Корректное завершение программы")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logging.info("Программа остановлена пользователем (Ctrl+C)")
sys.exit(0)
except Exception as e:
logging.error(f"Фатальная ошибка: {e}")
sys.exit(1)