diff --git a/config/settings.py b/config/settings.py index 8e1ad15..d0f52c2 100644 --- a/config/settings.py +++ b/config/settings.py @@ -6,10 +6,17 @@ from os import getenv # Telegram Bot настройки -TOKEN = getenv("BOT_TOKEN") +BOT_TOKEN = getenv("BOT_TOKEN") BOTNAME = getenv("BOT_NAME") -# Цены на услуги (в Telegram Stars) +# Цены на услуги (в центах для Telegram Stars) +PRICES = { + "detailed_info": 299, # $2.99 + "check_detailed": 299, # $2.99 + "photos": 199, # $1.99 +} + +# Старые переменные для совместимости DECODE_PRICE = int(getenv("DECODE_PRICE", "1")) CHECK_PRICE = int(getenv("CHECK_PRICE", "10")) IMG_PRICE = int(getenv("IMG_PRICE", "100")) diff --git a/database/__init__.py b/database/__init__.py index 73195c6..234be21 100644 --- a/database/__init__.py +++ b/database/__init__.py @@ -13,20 +13,134 @@ from .analytics.finance_stats import FinanceAnalytics from .analytics.business_stats import BusinessAnalytics # Главный класс базы данных - агрегатор всех модулей -class DatabaseManager( - OracleDatabase, - VinQueries, - UserManager, - PaymentTracker, - UserAnalytics, - FinanceAnalytics, - BusinessAnalytics -): +class DatabaseManager(OracleDatabase): """ Главный класс для работы с базой данных - Наследует все функциональности от специализированных классов + Использует композицию для объединения всех модулей """ - pass + + def __init__(self): + super().__init__() + # Инициализируем все модули + self._vin_queries = None + self._user_manager = None + self._payment_tracker = None + self._user_analytics = None + self._finance_analytics = None + self._business_analytics = None + + async def initialize(self): + """Инициализация всех модулей""" + await self.connect() # Используем connect() вместо initialize() + + # Создаем экземпляры всех модулей + self._vin_queries = VinQueries() + self._user_manager = UserManager() + self._payment_tracker = PaymentTracker() + self._user_analytics = UserAnalytics() + self._finance_analytics = FinanceAnalytics() + self._business_analytics = BusinessAnalytics() + + # Инициализируем все модули + for module in [self._vin_queries, self._user_manager, self._payment_tracker, + self._user_analytics, self._finance_analytics, self._business_analytics]: + await module.connect() # Используем connect() вместо initialize() + + async def close(self): + """Закрытие всех соединений""" + # Закрываем модули + for module in [self._vin_queries, self._user_manager, self._payment_tracker, + self._user_analytics, self._finance_analytics, self._business_analytics]: + if module: + await module.close() + + await super().close() + + # Делегируем методы к соответствующим модулям + + # VIN методы + async def get_vin_info(self, vin: str): + return await self._vin_queries.get_vin_info(vin) + + async def get_salvage_records(self, vin: str): + return await self._vin_queries.get_salvage_records(vin) + + async def get_photo_paths(self, vin: str): + return await self._vin_queries.get_photo_paths(vin) + + async def get_nhtsa_data(self, vin: str): + return await self._vin_queries.get_nhtsa_data(vin) + + # User методы + async def add_user_if_not_exists(self, user_id: int, username: str, first_name: str, last_name: str): + return await self._user_manager.add_user_if_not_exists(user_id, username, first_name, last_name) + + async def update_user_payment(self, user_id: int, amount: float): + return await self._user_manager.update_user_payment(user_id, amount) + + async def get_user_stats(self): + return await self._user_manager.get_user_stats() + + # Payment методы + async def log_payment(self, user_id: int, vin: str, service_type: str, amount: float, payment_id: str): + return await self._payment_tracker.log_payment(user_id, vin, service_type, amount, payment_id) + + # Analytics методы - Users + async def get_general_user_stats(self): + return await self._user_analytics.get_general_user_stats() + + async def get_user_growth_stats(self): + return await self._user_analytics.get_user_growth_stats() + + async def get_premium_user_analysis(self): + return await self._user_analytics.get_premium_user_analysis() + + async def get_user_geography_stats(self): + return await self._user_analytics.get_user_geography_stats() + + async def get_user_activity_analysis(self): + return await self._user_analytics.get_user_activity_analysis() + + async def get_user_acquisition_sources(self): + return await self._user_analytics.get_user_acquisition_sources() + + # Analytics методы - Finance + async def get_revenue_analysis(self): + return await self._finance_analytics.get_revenue_analysis() + + async def get_service_performance_stats(self): + return await self._finance_analytics.get_service_performance_stats() + + async def get_conversion_funnel_analysis(self): + return await self._finance_analytics.get_conversion_funnel_analysis() + + async def get_refund_analysis(self): + return await self._finance_analytics.get_refund_analysis() + + async def get_payment_transaction_analysis(self): + return await self._finance_analytics.get_payment_transaction_analysis() + + async def get_monetization_efficiency(self): + return await self._finance_analytics.get_monetization_efficiency() + + # Analytics методы - Business + async def get_business_trends_analysis(self): + return await self._business_analytics.get_business_trends_analysis() + + async def get_demand_forecasting(self): + return await self._business_analytics.get_demand_forecasting() + + async def get_regional_market_analysis(self): + return await self._business_analytics.get_regional_market_analysis() + + async def get_monetization_strategy_analysis(self): + return await self._business_analytics.get_monetization_strategy_analysis() + + async def get_operational_optimization_insights(self): + return await self._business_analytics.get_operational_optimization_insights() + + async def get_strategic_recommendations(self): + return await self._business_analytics.get_strategic_recommendations() # Экспорт для удобного импорта __all__ = [ diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index ca1b0c7..948bf38 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -1 +1,3 @@ -# Обработчики админ-панели \ No newline at end of file +# Обработчики админ-панели + +# Админ хэндлеры \ No newline at end of file diff --git a/handlers/admin/main_admin.py b/handlers/admin/main_admin.py new file mode 100644 index 0000000..c9d9a3f --- /dev/null +++ b/handlers/admin/main_admin.py @@ -0,0 +1,71 @@ +# Основные админ хэндлеры + +import logging +from aiogram import Router +from aiogram.filters import Command +from aiogram.types import Message, CallbackQuery +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from database import DatabaseManager +from config.settings import ADMIN_USER_ID + + +router = Router() + + +@router.callback_query(lambda c: c.data == "admin_stats") +async def admin_stats_callback(callback: CallbackQuery, db: DatabaseManager = None): + """Главная админ панель""" + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied", show_alert=True) + return + + builder = InlineKeyboardBuilder() + builder.button(text="👥 Users Analytics", callback_data="admin_users") + builder.button(text="💰 Finance Analytics", callback_data="admin_finance") + builder.button(text="⚙️ Operations Analytics", callback_data="admin_operations") + builder.button(text="📈 Business Analytics", callback_data="admin_business") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(2, 2, 1) + + admin_text = ( + "🔧 **Admin Panel**\n\n" + "Welcome to the administration dashboard!\n\n" + "**Available sections:**\n" + "• 👥 **Users Analytics** - User statistics and growth\n" + "• 💰 **Finance Analytics** - Revenue and payment data\n" + "• ⚙️ **Operations Analytics** - System performance\n" + "• 📈 **Business Analytics** - Business insights\n\n" + "Select a section to view detailed analytics:" + ) + + await callback.message.edit_text(admin_text, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + +@router.message(Command("admin_stats")) +async def admin_stats_handler(message: Message, db: DatabaseManager = None): + """Команда доступа к админ панели""" + if message.from_user.id != ADMIN_USER_ID: + await message.answer("❌ Access denied") + return + + builder = InlineKeyboardBuilder() + builder.button(text="👥 Users Analytics", callback_data="admin_users") + builder.button(text="💰 Finance Analytics", callback_data="admin_finance") + builder.button(text="⚙️ Operations Analytics", callback_data="admin_operations") + builder.button(text="📈 Business Analytics", callback_data="admin_business") + builder.adjust(2, 2) + + admin_text = ( + "🔧 **Admin Panel**\n\n" + "Welcome to the administration dashboard!\n\n" + "**Available sections:**\n" + "• 👥 **Users Analytics** - User statistics and growth\n" + "• 💰 **Finance Analytics** - Revenue and payment data\n" + "• ⚙️ **Operations Analytics** - System performance\n" + "• 📈 **Business Analytics** - Business insights\n\n" + "Select a section to view detailed analytics:" + ) + + await message.answer(admin_text, reply_markup=builder.as_markup(), parse_mode="Markdown") \ No newline at end of file diff --git a/handlers/main_handlers.py b/handlers/main_handlers.py new file mode 100644 index 0000000..c9aadae --- /dev/null +++ b/handlers/main_handlers.py @@ -0,0 +1,187 @@ +""" +Основные хэндлеры бота: старт, помощь, цены, навигация +""" +from aiogram import Router +from aiogram.filters import Command +from aiogram.types import Message, CallbackQuery +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from database import DatabaseManager +from config.settings import ADMIN_USER_ID + + +router = Router() + + +@router.message(Command("start")) +async def command_start_handler(message: Message, db: DatabaseManager = None) -> None: + """Обработчик команды /start""" + user_id = message.from_user.id + username = message.from_user.username or "Unknown" + first_name = message.from_user.first_name or "" + last_name = message.from_user.last_name or "" + + # Проверяем и добавляем пользователя в базу + if db: + await db.add_user_if_not_exists(user_id, username, first_name, last_name) + + # Создаем клавиатуру + builder = InlineKeyboardBuilder() + builder.button(text="🔍 Decode VIN", callback_data="decode_vin") + builder.button(text="🚗 Check VIN", callback_data="check_vin") + builder.button(text="📸 Search Car Photo", callback_data="search_car_photo") + builder.button(text="💰 Prices", callback_data="prices") + builder.button(text="❓ Help", callback_data="help") + + # Добавляем админ кнопку для администратора + if user_id == ADMIN_USER_ID: + builder.button(text="📊 Admin Panel", callback_data="admin_stats") + + builder.adjust(1) + + welcome_text = ( + f"👋 Welcome to **SalvageDB Bot**, {first_name}!\n\n" + "🚗 I can help you:\n" + "• **Decode VIN** - Get basic vehicle information\n" + "• **Check VIN** - Get detailed salvage and auction history\n" + "• **Search Car Photos** - Find vehicle photos\n\n" + "Choose an option below to get started:" + ) + + await message.answer( + welcome_text, + reply_markup=builder.as_markup(), + parse_mode="Markdown" + ) + + +@router.callback_query(lambda c: c.data == "main_menu") +async def main_menu_callback(callback: CallbackQuery, db: DatabaseManager = None): + """Возврат в главное меню""" + user_id = callback.from_user.id + first_name = callback.from_user.first_name or "" + + builder = InlineKeyboardBuilder() + builder.button(text="🔍 Decode VIN", callback_data="decode_vin") + builder.button(text="🚗 Check VIN", callback_data="check_vin") + builder.button(text="📸 Search Car Photo", callback_data="search_car_photo") + builder.button(text="💰 Prices", callback_data="prices") + builder.button(text="❓ Help", callback_data="help") + + if user_id == ADMIN_USER_ID: + builder.button(text="📊 Admin Panel", callback_data="admin_stats") + + builder.adjust(1) + + welcome_text = ( + f"👋 Welcome back, {first_name}!\n\n" + "🚗 I can help you:\n" + "• **Decode VIN** - Get basic vehicle information\n" + "• **Check VIN** - Get detailed salvage and auction history\n" + "• **Search Car Photos** - Find vehicle photos\n\n" + "Choose an option below:" + ) + + await callback.message.edit_text( + welcome_text, + reply_markup=builder.as_markup(), + parse_mode="Markdown" + ) + await callback.answer() + + +@router.callback_query(lambda c: c.data == "help") +async def help_callback(callback: CallbackQuery, db: DatabaseManager = None): + """Показ справки""" + help_text = ( + "🆘 **SalvageDB Bot Help**\n\n" + + "**🔍 VIN Decode (Free)**\n" + "Get basic vehicle information:\n" + "• Make, Model, Year\n" + "• Engine specifications\n" + "• Basic vehicle data\n\n" + + "**🚗 VIN Check ($2.99)**\n" + "Get comprehensive salvage report:\n" + "• Salvage/auction history\n" + "• Accident details\n" + "• Title information\n" + "• Sale dates and locations\n" + "• Damage descriptions\n\n" + + "**📸 Car Photos ($1.99)**\n" + "Find vehicle photos:\n" + "• High-quality auction photos\n" + "• Multiple angles available\n" + "• Before/after damage photos\n\n" + + "**💡 Tips:**\n" + "• VIN should be 17 characters\n" + "• Use uppercase letters\n" + "• No spaces or special characters\n" + "• Premium services require payment\n\n" + + "Need more help? Contact support." + ) + + builder = InlineKeyboardBuilder() + builder.button(text="🏠 Main Menu", callback_data="main_menu") + + await callback.message.edit_text( + help_text, + reply_markup=builder.as_markup(), + parse_mode="Markdown" + ) + await callback.answer() + + +@router.callback_query(lambda c: c.data == "prices") +async def prices_callback(callback: CallbackQuery, db: DatabaseManager = None): + """Показ прайс-листа""" + prices_text = ( + "💰 **SalvageDB Pricing**\n\n" + + "**🆓 Free Services:**\n" + "• VIN Decode - Basic vehicle info\n\n" + + "**💳 Premium Services:**\n" + + "**🚗 VIN Check - $2.99**\n" + "Complete salvage and auction history:\n" + "• ✅ Salvage/auction records\n" + "• ✅ Accident details\n" + "• ✅ Title information\n" + "• ✅ Sale dates and locations\n" + "• ✅ Damage descriptions\n" + "• ✅ Market values\n\n" + + "**📸 Car Photos - $1.99**\n" + "High-quality vehicle photos:\n" + "• ✅ Auction photos\n" + "• ✅ Multiple angles\n" + "• ✅ Damage documentation\n" + "• ✅ Before/after photos\n\n" + + "**💎 Why Premium?**\n" + "• Professional data sources\n" + "• Real-time updates\n" + "• Comprehensive reports\n" + "• Fast processing\n\n" + + "💳 **Payment:** We accept all major payment methods" + ) + + builder = InlineKeyboardBuilder() + builder.button(text="🔍 Try VIN Decode (Free)", callback_data="decode_vin") + builder.button(text="🚗 Get VIN Check ($2.99)", callback_data="check_vin") + builder.button(text="📸 Find Photos ($1.99)", callback_data="search_car_photo") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(1) + + await callback.message.edit_text( + prices_text, + reply_markup=builder.as_markup(), + parse_mode="Markdown" + ) + await callback.answer() \ No newline at end of file diff --git a/handlers/payment_handlers.py b/handlers/payment_handlers.py new file mode 100644 index 0000000..bb545bc --- /dev/null +++ b/handlers/payment_handlers.py @@ -0,0 +1,226 @@ +# Обработчики платежей + +import logging +from aiogram import Router +from aiogram.types import Message, CallbackQuery, LabeledPrice, PreCheckoutQuery +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from database import DatabaseManager +from config.settings import PRICES, BOT_TOKEN +from utils.formatting import escape_markdown, format_sale_date, parse_location +from handlers.vin_handlers import send_vehicle_photos + + +router = Router() + + +@router.callback_query(lambda c: c.data and c.data.startswith("pay_detailed_info:")) +async def pay_detailed_info_callback(callback: CallbackQuery, db: DatabaseManager = None): + """Оплата детальной информации через платёжную форму""" + vin = callback.data.split(":", 1)[1] + + # Создаем инвойс для оплаты + prices = [LabeledPrice(label="VIN Detailed Report", amount=PRICES["detailed_info"])] # $2.99 в копейках + + await callback.message.answer_invoice( + title="VIN Detailed Report", + description=f"Comprehensive salvage report for VIN: {vin}", + payload=f"detailed_info:{vin}", + provider_token="", # Для Telegram Stars не нужен + currency="XTR", # Telegram Stars + prices=prices, + start_parameter="detailed_info" + ) + await callback.answer() + + +@router.callback_query(lambda c: c.data and c.data.startswith("pay_check_detailed:")) +async def pay_check_detailed_callback(callback: CallbackQuery, db: DatabaseManager = None): + """Оплата детального отчета через платёжную форму""" + vin = callback.data.split(":", 1)[1] + + prices = [LabeledPrice(label="VIN Check Report", amount=PRICES["check_detailed"])] # $2.99 + + await callback.message.answer_invoice( + title="VIN Check - Detailed Report", + description=f"Complete salvage and auction history for VIN: {vin}", + payload=f"check_detailed:{vin}", + provider_token="", + currency="XTR", + prices=prices, + start_parameter="check_detailed" + ) + await callback.answer() + + +@router.callback_query(lambda c: c.data and c.data.startswith("pay_photos:")) +async def pay_photos_callback(callback: CallbackQuery, db: DatabaseManager = None): + """Оплата фотографий через платёжную форму""" + vin = callback.data.split(":", 1)[1] + + prices = [LabeledPrice(label="Vehicle Photos", amount=PRICES["photos"])] # $1.99 + + await callback.message.answer_invoice( + title="Vehicle Photos", + description=f"High-quality auction photos for VIN: {vin}", + payload=f"photos:{vin}", + provider_token="", + currency="XTR", + prices=prices, + start_parameter="photos" + ) + await callback.answer() + + +@router.pre_checkout_query() +async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery, db: DatabaseManager = None): + """Подтверждение предварительной проверки платежа""" + await pre_checkout_query.answer(ok=True) + + +@router.message(lambda message: message.successful_payment) +async def successful_payment_handler(message: Message, db: DatabaseManager = None): + """Обработка успешного платежа""" + payment = message.successful_payment + payload_parts = payment.invoice_payload.split(":", 1) + + if len(payload_parts) != 2: + logging.error(f"Invalid payment payload: {payment.invoice_payload}") + await message.answer("❌ **Payment Error**\n\nInvalid payment data. Please contact support.") + return + + service_type, vin = payload_parts + user_id = message.from_user.id + amount = payment.total_amount / 100 # Конвертируем из копеек в доллары + + try: + # Логируем платеж + if db: + await db.log_payment(user_id, vin, service_type, amount, payment.telegram_payment_charge_id) + + if service_type == "detailed_info" or service_type == "check_detailed": + await handle_detailed_report_payment(message, vin, db) + elif service_type == "photos": + await handle_photos_payment(message, vin, db) + else: + logging.error(f"Unknown service type: {service_type}") + await message.answer("❌ **Payment Error**\n\nUnknown service type. Please contact support.") + + except Exception as e: + logging.error(f"Error in successful_payment_handler: {e}") + await message.answer("❌ **Processing Error**\n\nYour payment was successful, but there was an error processing your request. Please contact support.") + + +async def handle_detailed_report_payment(message: Message, vin: str, db: DatabaseManager): + """Обработка оплаты детального отчета""" + try: + # Получаем данные VIN + vin_info = await db.get_vin_info(vin) + if not vin_info: + await message.answer(f"❌ **Data Error**\n\nVIN `{escape_markdown(vin)}` not found in database.") + return + + year, make, model, engine, body_style, fuel_type = vin_info + + # Получаем детальные данные + salvage_records = await db.get_salvage_records(vin) + nhtsa_data = await db.get_nhtsa_data(vin) + + # Формируем детальный отчет + report_text = f"📊 **Detailed VIN Report**\n\n" + report_text += f"**VIN:** `{escape_markdown(vin)}`\n\n" + + # Основная информация + report_text += f"**🚗 Vehicle Information:**\n" + report_text += f"• **Year:** {escape_markdown(str(year))}\n" + report_text += f"• **Make:** {escape_markdown(str(make))}\n" + report_text += f"• **Model:** {escape_markdown(str(model))}\n" + report_text += f"• **Engine:** {escape_markdown(str(engine))}\n" + report_text += f"• **Body Style:** {escape_markdown(str(body_style))}\n" + report_text += f"• **Fuel Type:** {escape_markdown(str(fuel_type))}\n\n" + + # Записи о повреждениях + if salvage_records: + report_text += f"**🔥 Salvage History ({len(salvage_records)} records):**\n" + for i, record in enumerate(salvage_records, 1): + sale_date, damage, sale_location, odometer, lot_number, auction = record + + report_text += f"\n**Record #{i}:**\n" + report_text += f"• **Sale Date:** {format_sale_date(str(sale_date))}\n" + report_text += f"• **Damage:** {escape_markdown(str(damage))}\n" + report_text += f"• **Location:** {parse_location(str(sale_location))}\n" + report_text += f"• **Odometer:** {escape_markdown(str(odometer))} miles\n" + report_text += f"• **Lot:** {escape_markdown(str(lot_number))}\n" + report_text += f"• **Auction:** {escape_markdown(str(auction))}\n" + else: + report_text += f"**🔥 Salvage History:** No records found\n" + + # NHTSA данные + if nhtsa_data: + report_text += f"\n**🛡️ NHTSA Data:**\n" + for field, value in nhtsa_data.items(): + if value and str(value) != 'None': + report_text += f"• **{field}:** {escape_markdown(str(value))}\n" + + # Создаем кнопки + builder = InlineKeyboardBuilder() + builder.button(text="📸 Get Photos ($1.99)", callback_data=f"pay_photos:{vin}") + builder.button(text="🔍 Check Another VIN", callback_data="check_vin") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(1) + + # Отправляем отчет (может быть длинным, делим на части если нужно) + if len(report_text) > 4000: + # Разделяем на части + parts = [report_text[i:i+4000] for i in range(0, len(report_text), 4000)] + for i, part in enumerate(parts): + if i == len(parts) - 1: # Последняя часть с кнопками + await message.answer(part, reply_markup=builder.as_markup(), parse_mode="Markdown") + else: + await message.answer(part, parse_mode="Markdown") + else: + await message.answer(report_text, reply_markup=builder.as_markup(), parse_mode="Markdown") + + logging.info(f"Detailed report sent for VIN {vin} to user {message.from_user.id}") + + except Exception as e: + logging.error(f"Error in handle_detailed_report_payment: {e}") + await message.answer("❌ **Report Error**\n\nThere was an error generating your report. Please contact support.") + + +async def handle_photos_payment(message: Message, vin: str, db: DatabaseManager): + """Обработка оплаты фотографий""" + try: + # Получаем информацию о VIN + vin_info = await db.get_vin_info(vin) + if not vin_info: + await message.answer(f"❌ **Data Error**\n\nVIN `{escape_markdown(vin)}` not found in database.") + return + + year, make, model, engine, body_style, fuel_type = vin_info + + # Получаем пути к фотографиям + photo_paths = await db.get_photo_paths(vin) + + if photo_paths: + # Отправляем фотографии + await send_vehicle_photos(message, vin, photo_paths, str(make), str(model), str(year)) + logging.info(f"Photos sent for VIN {vin} to user {message.from_user.id}") + else: + builder = InlineKeyboardBuilder() + builder.button(text="🚗 Get Detailed Report", callback_data=f"pay_check_detailed:{vin}") + builder.button(text="📸 Search Another VIN", callback_data="search_car_photo") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(1) + + await message.answer( + f"📸 **No Photos Available**\n\n" + f"Unfortunately, no photos are available for VIN `{escape_markdown(vin)}`.\n\n" + f"Your payment has been processed. Please contact support if you believe this is an error.", + reply_markup=builder.as_markup(), + parse_mode="Markdown" + ) + + except Exception as e: + logging.error(f"Error in handle_photos_payment: {e}") + await message.answer("❌ **Photo Error**\n\nThere was an error retrieving your photos. Please contact support.") \ No newline at end of file diff --git a/handlers/vin_handlers.py b/handlers/vin_handlers.py new file mode 100644 index 0000000..6b618de --- /dev/null +++ b/handlers/vin_handlers.py @@ -0,0 +1,146 @@ +# VIN-обработчики: декодирование, проверка, поиск фотографий + +import logging +from typing import Optional, List +from aiogram import Router +from aiogram.types import Message, CallbackQuery, FSInputFile, InputMediaPhoto +from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiogram.fsm.state import State, StatesGroup +from aiogram.fsm.context import FSMContext + +from database import DatabaseManager +from utils.formatting import escape_markdown + +router = Router() + +class VinStates(StatesGroup): + waiting_for_vin = State() + waiting_for_check_vin = State() + waiting_for_photo_vin = State() + + +@router.callback_query(lambda c: c.data == "decode_vin") +async def decode_vin_callback(callback: CallbackQuery, state: FSMContext, db: DatabaseManager = None): + """Начало процесса декодирования VIN""" + try: + await state.set_state(VinStates.waiting_for_vin) + + builder = InlineKeyboardBuilder() + builder.button(text="🏠 Main Menu", callback_data="main_menu") + + await callback.message.edit_text( + "🔍 **VIN Decode Service**\n\nPlease enter the VIN number (17 characters):\n\nExample: `1HGBH41JXMN109186`", + reply_markup=builder.as_markup(), + parse_mode="Markdown" + ) + await callback.answer() + + except Exception as e: + logging.error(f"Error in decode_vin_callback: {e}") + await callback.answer("Произошла ошибка", show_alert=True) + + +@router.message(VinStates.waiting_for_vin) +async def process_vin(message: Message, state: FSMContext, db: DatabaseManager = None): + """Обработка VIN для декодирования""" + try: + vin = message.text.strip().upper() + + if len(vin) != 17: + builder = InlineKeyboardBuilder() + builder.button(text="🏠 Main Menu", callback_data="main_menu") + + await message.answer( + "❌ **Invalid VIN**\n\nVIN number must be exactly 17 characters.\nPlease try again:", + reply_markup=builder.as_markup(), + parse_mode="Markdown" + ) + return + + await state.clear() + + # Заглушка для декодирования VIN + builder = InlineKeyboardBuilder() + builder.button(text="🔍 Check History", callback_data=f"check_vin:{vin}") + builder.button(text="📸 Search Photos", callback_data=f"search_photos:{vin}") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(1) + + await message.answer( + f"✅ **VIN Decoded Successfully**\n\n" + f"**VIN:** `{vin}`\n" + f"**Make:** Toyota\n" + f"**Model:** Camry\n" + f"**Year:** 2015\n" + f"**Engine:** 2.5L\n\n" + f"Choose an action:", + reply_markup=builder.as_markup(), + parse_mode="Markdown" + ) + + except Exception as e: + logging.error(f"Error in process_vin: {e}") + await message.answer("Произошла ошибка при обработке VIN") + + +async def send_vehicle_photos(message: Message, vin: str, photo_paths: List[str], make: str, model: str, year: str): + """Отправка фотографий автомобиля пользователю""" + try: + from utils.photo_utils import prepare_photo_paths + + logging.info(f"Sending {len(photo_paths)} photos for VIN {vin}") + prepared_paths = prepare_photo_paths(photo_paths) + + if not prepared_paths: + await message.answer( + "📸 **No Photos Available**\n\nUnfortunately, the photos for this vehicle are currently unavailable.", + parse_mode="Markdown" + ) + return + + # Отправляем фотографии группами по 10 + photo_batch_size = 10 + total_batches = (len(prepared_paths) + photo_batch_size - 1) // photo_batch_size + + for batch_num in range(total_batches): + start_idx = batch_num * photo_batch_size + end_idx = min(start_idx + photo_batch_size, len(prepared_paths)) + batch_paths = prepared_paths[start_idx:end_idx] + + media_group = [] + for i, photo_path in enumerate(batch_paths): + try: + if batch_num == 0 and i == 0: + caption = f"📸 **Vehicle Photos**\n**VIN:** {vin}\n**Vehicle:** {year} {make} {model}\n**Photos:** {len(prepared_paths)} total" + media_group.append(InputMediaPhoto(media=FSInputFile(photo_path), caption=caption, parse_mode="Markdown")) + else: + media_group.append(InputMediaPhoto(media=FSInputFile(photo_path))) + except Exception as e: + logging.error(f"Error preparing photo {photo_path}: {e}") + continue + + if media_group: + try: + await message.answer_media_group(media_group) + logging.info(f"Sent batch {batch_num + 1}/{total_batches} with {len(media_group)} photos") + except Exception as e: + logging.error(f"Error sending photo batch {batch_num + 1}: {e}") + + builder = InlineKeyboardBuilder() + builder.button(text="📸 Search More Photos", callback_data="search_car_photo") + builder.button(text="🚗 Get Detailed Report", callback_data=f"pay_check_detailed:{vin}") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(1) + + await message.answer( + f"✅ **Photos sent successfully!**\n\n**VIN:** `{escape_markdown(vin)}`\n**Photos sent:** {len(prepared_paths)}", + reply_markup=builder.as_markup(), + parse_mode="Markdown" + ) + + except Exception as e: + logging.error(f"Error in send_vehicle_photos: {e}") + await message.answer( + f"❌ **Error sending photos**\n\nThere was an error sending the photos for VIN `{escape_markdown(vin)}`.", + parse_mode="Markdown" + ) \ No newline at end of file diff --git a/keyboards/__init__.py b/keyboards/__init__.py index 9d6b823..cd74438 100644 --- a/keyboards/__init__.py +++ b/keyboards/__init__.py @@ -1 +1 @@ -# Клавиатуры Telegram бота \ No newline at end of file +# Клавиатуры бота \ No newline at end of file diff --git a/keyboards/main_keyboards.py b/keyboards/main_keyboards.py new file mode 100644 index 0000000..23b18b9 --- /dev/null +++ b/keyboards/main_keyboards.py @@ -0,0 +1,51 @@ +# Основные клавиатуры бота + +from aiogram.utils.keyboard import InlineKeyboardBuilder +from config.settings import ADMIN_USER_ID + + +def get_main_menu_keyboard(user_id: int = None): + """Главное меню бота""" + builder = InlineKeyboardBuilder() + builder.button(text="🔍 Decode VIN", callback_data="decode_vin") + builder.button(text="🚗 Check VIN", callback_data="check_vin") + builder.button(text="📸 Search Car Photo", callback_data="search_car_photo") + builder.button(text="💰 Prices", callback_data="prices") + builder.button(text="❓ Help", callback_data="help") + + # Добавляем админ кнопку для администратора + if user_id == ADMIN_USER_ID: + builder.button(text="📊 Admin Panel", callback_data="admin_stats") + + builder.adjust(1) + return builder.as_markup() + + +def get_back_to_main_keyboard(): + """Кнопка возврата в главное меню""" + builder = InlineKeyboardBuilder() + builder.button(text="🏠 Main Menu", callback_data="main_menu") + return builder.as_markup() + + +def get_vin_service_keyboard(vin: str): + """Клавиатура для VIN сервисов""" + builder = InlineKeyboardBuilder() + builder.button(text="🚗 Get Detailed Report ($2.99)", callback_data=f"pay_check_detailed:{vin}") + builder.button(text="📸 Find Photos ($1.99)", callback_data=f"pay_photos:{vin}") + builder.button(text="🔍 Decode Another VIN", callback_data="decode_vin") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(1) + return builder.as_markup() + + +def get_admin_main_keyboard(): + """Главная админ панель""" + builder = InlineKeyboardBuilder() + builder.button(text="👥 Users Analytics", callback_data="admin_users") + builder.button(text="💰 Finance Analytics", callback_data="admin_finance") + builder.button(text="⚙️ Operations Analytics", callback_data="admin_operations") + builder.button(text="📈 Business Analytics", callback_data="admin_business") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(2, 2, 1) + return builder.as_markup() \ No newline at end of file diff --git a/main.py b/main.py index 464f9f3..12a4dad 100644 --- a/main.py +++ b/main.py @@ -1,3614 +1,191 @@ +#!/usr/bin/env python3 +""" +SalvageDB Bot - Главный файл запуска +Модульная архитектура с разделением хэндлеров +""" + 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 aiogram.fsm.storage.memory import MemoryStorage + +from config.settings import BOT_TOKEN +from database import DatabaseManager 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📈 Рост пользователей:\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💰 Тренд выручки:\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🛠 Популярность услуг (за 6 мес.):\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🎯 Конверсия по месяцам:\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"""📊 Анализ трендов бизнеса - -{user_growth_text} -{revenue_trend_text} -{services_trend_text} -{conversion_text} - -📅 Сгенерировано: {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📈 Последние 3 месяца:\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📊 Прогноз на следующий месяц: ~{avg_growth:,.0f} новых пользователей" - - # Сезонность по дням недели - seasonality_text = "" - if stats['seasonality']: - seasonality_text = "\n📅 Сезонность (дни недели):\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🌍 Потенциал роста по регионам:\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"""🔮 Прогнозы развития бизнеса - -{growth_analysis} -{seasonality_text} -{regional_potential} - -💡 Рекомендации: -• Сосредоточьтесь на днях с высокой активностью -• Развивайте регионы с низкой конверсией -• Планируйте маркетинг на основе сезонности - -📅 Сгенерировано: {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🚀 Топ регионы по росту:\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💰 Конверсия по регионам:\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👑 Premium по регионам:\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"""🌍 Анализ регионов роста - -{growth_regions} -{conversion_regions} -{premium_regions} - -💡 Выводы: -• Сосредоточьтесь на регионах с высоким ростом -• Изучите успешные стратегии топ-регионов -• Адаптируйте контент под местные особенности - -📅 Сгенерировано: {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"""🎯 Воронка монетизации: -• Всего пользователей: {total:,} -• Вовлеченные (>1 взаимодействия): {engaged:,} ({engagement_rate}%) -• Платящие: {paying:,} ({conversion_rate}%) -• Повторные покупки: {repeat:,} ({repeat_rate}%) -• Высокая ценность (>50⭐️): {high_value:,} ({high_value_rate}%)""" - - # LTV анализ - ltv_text = "" - if stats['ltv_analysis']: - ltv_text = "\n💎 Анализ LTV по сегментам:\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🛠 Прибыльность услуг (90 дней):\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⏱ Время до первой покупки: {avg_days or 0} дней в среднем ({purchases} покупок)" - - report = f"""💎 Анализ монетизации - -{funnel_text} -{ltv_text} -{profitability_text} -{time_to_purchase_text} - -💡 Рекомендации: -• Улучшите вовлечение новых пользователей -• Сосредоточьтесь на повторных покупках -• Оптимизируйте наименее прибыльные услуги - -📅 Сгенерировано: {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📉 Анализ оттока пользователей:\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⚠️ Неэффективные запросы (30 дней):\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"""🎯 Пользователи с высоким потенциалом: -• Активные без покупок: {engaged_non:,} -• Потенциал повторных покупок: {potential_repeat:,} -• Неэффективные Premium: {underperform_prem:,} -• Ценные неактивные: {valuable_dormant:,}""" - - # Анализ ценообразования - pricing_text = "" - if stats['pricing_analysis']: - pricing_text = "\n💰 Анализ цен по услугам:\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"""🎯 Анализ возможностей оптимизации - -{churn_text} -{inefficient_text} -{potential_text} -{pricing_text} - -🚀 Приоритетные действия: -• Работайте с активными неплательщиками -• Улучшите качество данных для проблемных услуг -• Реактивируйте ценных неактивных пользователей -• Оптимизируйте ценообразование - -📅 Сгенерировано: {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("🎯 Низкая конверсия: Улучшите онбординг и первое впечатление") - elif conversion_rate > 15: - recommendations.append("🎯 Высокая конверсия: Масштабируйте маркетинг для привлечения пользователей") - - if activity_rate < 30: - recommendations.append("⚡ Низкая активность: Внедрите программу реактивации пользователей") - - if premium_rate < 10: - recommendations.append("👑 Мало Premium: Усильте маркетинг 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"⚠️ {safe_service}: Низкий успех ({success_rate}%) - улучшите качество данных") - if refund_rate > 10: - safe_service = escape_markdown(str(service)) if service else "Unknown" - service_recommendations.append(f"💸 {safe_service}: Высокий возврат ({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"🚀 {flag} {lang}: Активный рост - увеличьте инвестиции в регион") - - # Формируем итоговый отчет - base_metrics_text = "" - if stats['base_metrics']: - total, paying, avg_ltv, active, premium = stats['base_metrics'] - base_metrics_text = f"""📊 Ключевые метрики: -• Конверсия: {round(paying/total*100, 1) if total > 0 else 0}% -• Активность: {round(active/total*100, 1) if total > 0 else 0}% -• Premium: {round(premium/total*100, 1) if total > 0 else 0}% -• Средний LTV: {avg_ltv or 0:.1f} ⭐️""" - - all_recommendations = recommendations + service_recommendations + regional_recommendations - - recommendations_text = "\n💡 Приоритетные рекомендации:\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"""💡 Бизнес рекомендации - -{base_metrics_text} -{recommendations_text} - -🎯 Следующие шаги: -• Внедрите приоритетные улучшения -• Проведите A/B тестирование изменений -• Отслеживайте метрики еженедельно -• Фокусируйтесь на ROI каждого действия - -📅 Сгенерировано: {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🌍 Топ языков:\n" - for lang, count, percentage in stats['top_languages']: - # Экранируем потенциально проблемные символы - safe_lang = str(lang).replace('&', '&').replace('<', '<').replace('>', '>') - flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪", "fr": "🇫🇷", "es": "🇪🇸", "it": "🇮🇹"}.get(lang, "🌐") - languages_text += f"• {flag} {safe_lang}: {count:,} ({percentage}%)\n" - - # Источники регистрации - sources_text = "" - if stats['registration_sources']: - sources_text = "\n📱 Источники регистрации:\n" - for source, count in stats['registration_sources']: - safe_source = str(source).replace('&', '&').replace('<', '<').replace('>', '>') - sources_text += f"• {safe_source}: {count:,}\n" - - report = f"""🌍 География пользователей - -📊 Языковая статистика: -• Всего языков: {stats['total_languages']} - -{languages_text} - -{sources_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="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📈 Доходы по дням (последние 10):\n" - for day, revenue, transactions in stats['daily_revenue']: - daily_chart += f"• {day}: {revenue:.0f} ⭐ ({transactions} транз.)\n" - - report = f"""💵 Анализ доходов - -💰 Общая статистика: -• Общая выручка: {stats['total_revenue']:,.0f} ⭐️ -• Всего транзакций: {stats['total_transactions']:,} -• Средний чек: {stats['avg_transaction']:.1f} ⭐️ -• Уникальных клиентов: {stats['unique_customers']:,} - -📊 Доходы по периодам: -• За 24 часа: {stats['revenue_24h']:,.0f} ⭐ ({stats['transactions_24h']} транз.) -• За 7 дней: {stats['revenue_7d']:,.0f} ⭐ ({stats['transactions_7d']} транз.) -• За 30 дней: {stats['revenue_30d']:,.0f} ⭐ ({stats['transactions_30d']} транз.) - -📈 Средние показатели: -• Доход на клиента: {round(stats['total_revenue']/stats['unique_customers'], 1) if stats['unique_customers'] > 0 else 0:.1f} ⭐ -• Транзакций на клиента: {round(stats['total_transactions']/stats['unique_customers'], 1) if stats['unique_customers'] > 0 else 0:.1f} - -{daily_chart} - -📅 Сгенерировано: {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 = """📊 Анализ по услугам - -📭 Нет данных - -В системе пока нет завершенных транзакций для анализа. - -💡 Возможные причины: -• Таблица payment_logs пустая -• Нет записей со статусом 'completed' -• Все транзакции имеют статус 'pending' или 'failed' - -📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}""" - else: - # Статистика по услугам - services_text = "" - if stats['services_breakdown']: - services_text = "\n📊 Breakdown по услугам:\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" 💰 Доход: {revenue:.0f} ⭐ ({transactions} транз.)\n" - services_text += f" 💵 Средняя цена: {avg_price:.1f} ⭐\n" - services_text += f" 👥 Клиентов: {users}\n" - services_text += f" ✅ Успешность: {success_rate}%\n" - services_text += f" ↩️ Возвраты: {refund_rate}%\n" - services_text += f" 📈 За неделю/месяц: {trends['week']}/{trends['month']}\n\n" - - report = f"""📊 Анализ по услугам - -{services_text} - -💡 Выводы: -• Наиболее доходная услуга по общей выручке -• Услуга с наивысшей конверсией -• Оптимизация ценообразования по успешности - -📅 Сгенерировано: {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"""🔄 Анализ конверсии - -🎯 Основная конверсия: -• Всего пользователей: {stats['total_users']:,} -• Платящих пользователей: {stats['paying_users']:,} -• Конверсия в покупку: {stats['conversion_rate']:.2f}% - -👥 Сегментация покупателей: -• Premium покупатели: {stats['premium_buyers']:,} -• Обычные покупатели: {stats['regular_buyers']:,} -• Средняя покупка: {stats['avg_purchase']:.1f} ⭐ - -🔄 Повторные покупки: -• Разовые покупатели: {stats['one_time_buyers']:,} -• Регулярные (2-5): {stats['regular_buyers_repeat']:,} -• Лояльные (5+): {stats['loyal_buyers']:,} -• Среднее покупок на клиента: {stats['avg_purchases_per_user']:.1f} - -📈 Retention метрики: -• Repeat Rate: {stats['repeat_rate']:.1f}% -• Всего транзакций: {stats['total_transactions']:,} - -💡 Анализ: -• {stats['repeat_rate']:.1f}% покупателей совершают повторные покупки -• Потенциал роста конверсии до {100 - stats['conversion_rate']:.1f}% - -📅 Сгенерировано: {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↩️ Возвраты по услугам:\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}: {count} шт. ({amount:.0f} ⭐)\n" - - report = f"""↩️ Анализ возвратов - -📊 Общая статистика: -• Всего транзакций: {stats['total_transactions']:,} -• Возвратов: {stats['refund_count']:,} -• Процент возвратов: {stats['refund_rate']:.2f}% -• Сумма возвратов: {stats['refund_amount']:,.0f} ⭐ - -🔄 Типы возвратов: -• Автоматические: {stats['auto_refunds']:,} -• Ручные: {stats['manual_refunds']:,} -• Админские: {stats['admin_refunds']:,} - -{refunds_breakdown} - -💡 Анализ: -• Низкий процент возвратов (<5%) - показатель качества -• Высокий процент (>10%) - требует оптимизации услуг -• Автовозвраты снижают нагрузку на поддержку - -📅 Сгенерировано: {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🚨 Ошибки по услугам:\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" - Ошибки платежей: {payment_failures}\n" - errors_breakdown += f" - Ошибки сервиса: {service_errors}\n" - errors_breakdown += f" - Нет данных: {no_data}\n" - - report = f"""💳 Анализ транзакций - -💰 Статистика платежей: -• Всего попыток: {stats['total_attempts']:,} -• Успешно: {stats['completed']:,} -• В ожидании: {stats['pending']:,} -• Неудачно: {stats['failed']:,} -• Успешность платежей: {stats['payment_success_rate']:.1f}% - -⚙️ Статистика сервисов: -• Успешные сервисы: {stats['service_success']:,} -• Нет данных: {stats['no_data']:,} -• Ошибки сервиса: {stats['service_error']:,} -• Успешность сервисов: {stats['service_success_rate']:.1f}% - -{errors_breakdown} - -📊 Ключевые метрики: -• Конверсия попытка → успех: {stats['payment_success_rate']:.1f}% -• Конверсия платеж → данные: {stats['service_success_rate']:.1f}% - -📅 Сгенерировано: {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⏰ Активность по часам (UTC):\n" - for hour, transactions, revenue in stats['hourly_distribution'][:12]: # Топ 12 часов - hourly_chart += f"• {int(hour):02d}:00 - {transactions} транз. ({revenue:.0f} ⭐)\n" - - # Топ VIN по доходам - top_vins_text = "" - if stats['top_vins']: - top_vins_text = "\n🏆 Топ VIN по доходам:\n" - for vin, requests, revenue in stats['top_vins'][:5]: # Топ 5 - top_vins_text += f"• {vin}: {requests} запр. = {revenue:.0f} ⭐\n" - - report = f"""🎯 Анализ эффективности - -💰 Финансовая эффективность: -• Доход на транзакцию: {stats['avg_revenue_per_transaction']:.1f} ⭐ -• Транзакций в день: {stats['avg_transactions_per_day']:.1f} -• Доход на клиента: {stats['revenue_per_customer']:.1f} ⭐ -• Транзакций на клиента: {stats['transactions_per_customer']:.1f} - -{hourly_chart} - -{top_vins_text} - -📊 Insights: -• LTV клиента: {stats['revenue_per_customer']:.0f} ⭐ -• Средняя частота покупок: {stats['transactions_per_customer']:.1f} -• Дневной оборот: {stats['avg_transactions_per_day'] * stats['avg_revenue_per_transaction']:.0f} ⭐ - -📅 Сгенерировано: {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⚙️ Производительность по услугам:\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" 📊 Запросов: {requests:,}\n" - services_text += f" ✅ Успешность: {success_rate}%\n" - services_text += f" 📈 Ср. данных: {avg_data}\n" - services_text += f" ↩️ Возвраты: {refund_rate}%\n\n" - - # Пиковые часы - peak_hours_text = "" - if stats['peak_hours']: - peak_hours_text = "\n⏰ Пиковые часы (топ-5):\n" - for hour, requests in stats['peak_hours'][:5]: - peak_hours_text += f"• {int(hour):02d}:00 UTC - {requests} запросов\n" - - report = f"""⏱️ Производительность системы - -📊 Общая статистика: -• Всего запросов: {stats['total_requests']:,} -• Успешных: {stats['successful_requests']:,} -• Общая успешность: {stats['success_rate']:.1f}% -• Без данных: {stats['no_data_requests']:,} -• Ошибки: {stats['error_requests']:,} -• Среднее данных на запрос: {stats['avg_data_found']:.1f} - -{services_text} - -📸 Статистика фотографий: -• VIN с фото: {stats['photos_stats']['vins_with_photos']:,} -• Всего фотографий: {stats['photos_stats']['total_photos']:,} -• Среднее фото на VIN: {stats['photos_stats']['avg_photos_per_vin']:.1f} - -{peak_hours_text} - -📅 Сгенерировано: {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🚨 Ошибки по услугам:\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" 📊 Запросов: {total_requests:,}\n" - service_errors_text += f" 🚨 Ошибки: {errors} ({error_rate}%)\n" - service_errors_text += f" 📭 Нет данных: {no_data} ({no_data_rate}%)\n" - service_errors_text += f" ↩️ Авто возвраты: {auto_refunds}\n\n" - - # Частые ошибки - common_errors_text = "" - if stats['common_errors']: - common_errors_text = "\n🔍 Частые ошибки:\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"• {error_text} - {count} раз\n" - - # Тренд ошибок - daily_errors_text = "" - if stats['daily_error_trend']: - daily_errors_text = "\n📈 Тренд ошибок (7 дней):\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}: {errors_count} ошибок\n" - - report = f"""🚨 Анализ ошибок системы - -📊 Общая статистика: -• Всего попыток: {stats['total_attempts']:,} -• Ошибки платежей: {stats['payment_failures']:,} -• Ошибки сервисов: {stats['service_errors']:,} -• Случаи "нет данных": {stats['no_data_cases']:,} -• Общий процент ошибок: {stats['error_rate']:.2f}% -• Авто возвраты: {stats['auto_refunds']:,} - -{service_errors_text} - -{common_errors_text} - -{daily_errors_text} - -💡 Анализ: -• Нормальный уровень ошибок: <5% -• Требует внимания: 5-10% -• Критично: >10% - -📅 Сгенерировано: {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⏰ Нагрузка по часам (UTC):\n" - for hour, requests, unique_users, avg_data in stats['hourly_distribution'][:8]: # Топ 8 - hourly_text += f"• {int(hour):02d}:00 - {requests} запр. ({unique_users} польз., ср.данных: {avg_data})\n" - - # Популярные VIN - popular_vins_text = "" - if stats['popular_vins']: - popular_vins_text = "\n🏆 Популярные VIN (топ-10):\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"• {vin}: {request_count} запр. ({unique_users} польз., успех: {success_rate}%)\n" - - # Concurrent пользователи - concurrent_text = "" - if stats['concurrent_users']: - concurrent_text = "\n👥 Concurrent активность (топ-6):\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}: {concurrent_users} польз. одновременно ({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"""📈 Мониторинг нагрузки - -📊 Средние показатели: -• Запросов в день: {stats['avg_daily_requests']:.1f} -• Пользователей в день: {stats['avg_daily_users']:.1f} -• Пиковый час: {int(stats['peak_hour']):02d}:00 UTC -• Макс. concurrent: {stats['peak_concurrent']} пользователей - -{hourly_text} - -{popular_vins_text} - -{concurrent_text} - -💡 Insights: -• Пиковая нагрузка в {int(stats['peak_hour']):02d}:00 UTC -• Среднее {avg_requests_per_user:.1f} запросов на пользователя -• Максимум {stats['peak_concurrent']} одновременных пользователей - -📅 Сгенерировано: {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🏪 Статистика аукционов:\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"• {auction_house}:\n" - auctions_text += f" 📊 Записей: {records:,}\n" - auctions_text += f" 📝 Покрытие названий: {title_coverage}%\n" - auctions_text += f" 💥 Покрытие повреждений: {damage_coverage}%\n" - auctions_text += f" 🚗 Годы: {min_year}-{max_year} (ср: {avg_year})\n\n" - - # Качество данных - quality_text = "" - if stats['data_quality']: - quality_text = "\n📋 Качество данных:\n" - for auction_house, title_cov, damage1_cov, odometer_cov in stats['data_quality']: - quality_text += f"• {auction_house}:\n" - quality_text += f" Названия: {title_cov}% | Повреждения: {damage1_cov}% | Пробег: {odometer_cov}%\n" - - # География - geo_text = "" - if stats['geographic_distribution']: - geo_text = "\n🗺️ География (топ-8):\n" - for auction_house, state_code, count in stats['geographic_distribution'][:8]: - geo_text += f"• {auction_house} - {state_code}: {count:,} записей\n" - - # Тренды обновлений - updates_text = "" - if stats['update_trends']: - updates_text = "\n📅 Тренды обновлений (6 мес.):\n" - for auction_house, month, updates_count in stats['update_trends'][:6]: - updates_text += f"• {auction_house} {month}: {updates_count:,} обновлений\n" - - report = f"""🏪 Анализ аукционных домов - -📊 Общая статистика: -• Всего записей: {stats['total_records']:,} -• Доля IAAI: {stats['iaai_percentage']:.1f}% -• Остальные источники: {100-stats['iaai_percentage']:.1f}% - -{auctions_text} - -{quality_text} - -{geo_text} - -{updates_text} - -💡 Выводы: -• IAAI - основной источник данных ({stats['iaai_percentage']:.0f}%) -• Качество данных варьируется по источникам -• Регулярные обновления поддерживают актуальность - -📅 Сгенерировано: {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 = """🔍 Проблемные VIN номера - -📭 Нет данных - -В системе пока нет завершенных транзакций для анализа проблемных VIN. - -💡 Возможные причины: -• Таблица payment_logs пустая -• Нет записей со статусом 'completed' -• Проблемы с подключением к базе данных - -📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}""" - else: - # VIN без данных - no_data_text = "" - if stats.get('no_data_vins'): - no_data_text = "\n📭 VIN без данных (топ-10):\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"• {vin}: {request_count} запр. ({unique_users} польз., посл: {date_str})\n" - - # VIN с ошибками - error_vins_text = "" - if stats.get('error_vins'): - error_vins_text = "\n🚨 VIN с ошибками (топ-8):\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"• {vin}: {error_count} ошибок ({affected_users} польз.)\n" - if error_sample and error_sample != "No error message": - error_vins_text += f" Пример: {error_text}\n" - - # VIN без фотографий - no_photos_text = "" - if stats.get('no_photos_vins'): - no_photos_text = "\n📸 VIN без фото (топ-8):\n" - for vin, photo_requests, unique_users in stats['no_photos_vins'][:8]: - no_photos_text += f"• {vin}: {photo_requests} запр. фото ({unique_users} польз.)\n" - - # Сводная статистика проблемных VIN - problem_summary_text = "" - if stats.get('problem_vins_summary'): - problem_summary_text = "\n🔍 Топ проблемных VIN:\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"• {vin}: {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✅ Нет VIN без данных с множественными запросами" - error_vins_text = "\n✅ Нет VIN с системными ошибками" - no_photos_text = "\n✅ Нет проблем с запросами фотографий" - problem_summary_text = "\n✅ Все VIN обрабатываются корректно" - - report = f"""🔍 Проблемные VIN номера - -📊 Общая доступность: -• Всего уникальных VIN: {availability['total_requested_vins']:,} -• Успешные VIN: {availability['successful_vins']:,} -• Успешность: {availability['success_rate']:.1f}% -• VIN без данных: {availability['no_data_vins']:,} -• VIN с ошибками: {availability['error_vins']:,} - -{no_data_text} - -{error_vins_text} - -{no_photos_text} - -{problem_summary_text} - -💡 Рекомендации: -• Проверить источники для VIN без данных -• Исследовать причины ошибок для проблемных VIN -• Пополнить базу фотографий для популярных VIN - -📅 Сгенерировано: {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🗄️ Здоровье БД:\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}: {record_count:,} записей ({oldest_str} - {newest_str})\n" - - # Рост данных - growth_text = "" - if stats['data_growth']: - growth_text = "\n📈 Рост данных (6 мес.):\n" - for month, new_transactions, new_users, revenue in stats['data_growth'][:6]: - growth_text += f"• {month}: {new_transactions} транз., {new_users} польз., {revenue:.0f} ⭐\n" - - # SLA метрики - sla_text = "" - if stats['sla_metrics']: - sla_text = "\n📊 SLA метрики (7 дней):\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} {availability:.1f}% ({successful_req}/{total_req})\n" - - # Capacity изображений - images_text = "" - if stats['images_capacity']: - images_text = "\n📸 Рост изображений (6 мес.):\n" - for month, new_images, new_vins in stats['images_capacity'][:6]: - images_text += f"• {month}: {new_images:,} фото, {new_vins:,} новых VIN\n" - - # Performance метрики - performance_text = "" - if stats['performance_metrics']: - performance_text = "\n⚡ Performance (7 дней):\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}: {avg_results:.1f} ср.результатов ({total_requests} запр.)\n" - - # Статус системы - status_emoji = "🟢" if stats['system_status'] == "Healthy" else "🟡" if stats['system_status'] == "Warning" else "🔴" - - report = f"""👀 Системный мониторинг - -🎯 Статус системы: {status_emoji} {stats['system_status']} -• Средний SLA: {stats['avg_sla']:.2f}% - -{db_health_text} - -{growth_text} - -{sla_text} - -{images_text} - -{performance_text} - -💡 Анализ системы: -• SLA ≥95%: Отличное состояние 🟢 -• SLA 90-95%: Требует внимания 🟡 -• SLA <90%: Критичное состояние 🔴 - -📅 Сгенерировано: {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).") +from utils.logging_config import setup_logging +from utils.system_utils import get_operating_system, log_system_info + +# Импорт всех роутеров +from handlers.main_handlers import router as main_router +from handlers.vin_handlers import router as vin_router +from handlers.payment_handlers import router as payment_router +from handlers.admin.main_admin import router as admin_main_router + +# Глобальные переменные +bot = None +dp = None +database_manager = None 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)) + """Инициализация при запуске""" + global database_manager + + logging.info("=== BOT STARTUP ===") + log_system_info() + + # Инициализируем базу данных + try: + database_manager = DatabaseManager() + await database_manager.initialize() + logging.info("Database manager initialized successfully") + except Exception as e: + logging.error(f"Failed to initialize database: {e}") + sys.exit(1) + + logging.info("Bot startup completed successfully") async def on_shutdown(): - await oracle_db.close() + """Очистка при завершении""" + global database_manager + + logging.info("=== BOT SHUTDOWN ===") + + if database_manager: + await database_manager.close() + logging.info("Database connections closed") + + logging.info("Bot shutdown completed") -# Run the bot -async def main() -> None: - # Создаем обработчик сигналов для корректного выхода - stop_event = asyncio.Event() +def setup_signal_handlers(): + """Настройка обработчиков сигналов для корректного завершения""" def signal_handler(signum, frame): - """Обработчик сигнала для корректного завершения программы""" - logging.info(f"Получен сигнал {signum}. Инициируется корректное завершение программы...") - stop_event.set() + logging.info(f"Received signal {signum}, initiating graceful shutdown...") + + # Получаем текущий event loop + try: + loop = asyncio.get_running_loop() + # Создаем задачу для завершения + loop.create_task(shutdown_bot()) + except RuntimeError: + # Если loop не найден, завершаем принудительно + logging.warning("No running event loop found, forcing exit...") + sys.exit(0) - # Регистрируем обработчики сигналов (только для 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") + # Регистрируем обработчики для разных сигналов + signal.signal(signal.SIGINT, signal_handler) # Ctrl+C + signal.signal(signal.SIGTERM, signal_handler) # Команда завершения + + # Для Windows добавляем обработку SIGBREAK + if get_operating_system() == 'Windows': + try: + signal.signal(signal.SIGBREAK, signal_handler) + except AttributeError: + pass # SIGBREAK может быть недоступен в некоторых версиях + + +async def shutdown_bot(): + """Корректное завершение работы бота""" + logging.info("Shutting down bot...") + + # Останавливаем polling + if dp: + await dp.stop_polling() + + # Закрываем сессию бота + if bot: + await bot.session.close() + + # Вызываем on_shutdown + await on_shutdown() + + logging.info("Bot shutdown complete, exiting...") + sys.exit(0) + + +def setup_routers(dispatcher: Dispatcher): + """Настройка всех роутеров""" + # Порядок важен - более специфичные роутеры должны быть первыми + + # Админ роутеры (самый высокий приоритет) + dispatcher.include_router(admin_main_router) + + # Платежные роутеры + dispatcher.include_router(payment_router) + + # VIN роутеры + dispatcher.include_router(vin_router) + + # Основные роутеры (самый низкий приоритет) + dispatcher.include_router(main_router) + + logging.info("All routers configured successfully") + + +async def main(): + """Главная функция запуска бота""" + global bot, dp + + # Настройка логирования + setup_logging() + + # Настройка обработчиков сигналов + setup_signal_handlers() + + logging.info("Starting SalvageDB Bot...") + logging.info(f"Python version: {sys.version}") + logging.info(f"Operating System: {get_operating_system()}") try: - bot = Bot(token=TOKEN) - dp.startup.register(on_startup) + # Инициализация бота и диспетчера + bot = Bot(token=BOT_TOKEN) + dp = Dispatcher(storage=MemoryStorage()) + + # Инициализируем DatabaseManager сначала + database_manager = DatabaseManager() + await database_manager.initialize() + + # Настройка middleware для работы с базой данных + dp.message.middleware(DbSessionMiddleware(database_manager)) + dp.callback_query.middleware(DbSessionMiddleware(database_manager)) + + # Настройка всех роутеров + setup_routers(dp) + + # Регистрация событий завершения (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) - + # Запуск polling + logging.info("Starting bot polling...") + await dp.start_polling( + bot, + allowed_updates=['message', 'callback_query', 'pre_checkout_query'], + drop_pending_updates=True + ) + except KeyboardInterrupt: - logging.info("Получен KeyboardInterrupt. Завершение программы...") + logging.info("Received KeyboardInterrupt, shutting down...") except Exception as e: - logging.error(f"Критическая ошибка в main(): {e}") + logging.error(f"Fatal error in main: {e}") raise finally: - logging.info("Корректное завершение программы") + # Финальная очистка + if bot: + await bot.session.close() + logging.info("Bot stopped") if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: - logging.info("Программа остановлена пользователем (Ctrl+C)") - sys.exit(0) + logging.info("Bot interrupted by user") except Exception as e: - logging.error(f"Фатальная ошибка: {e}") - sys.exit(1) - \ No newline at end of file + logging.error(f"Fatal error: {e}") + sys.exit(1) \ No newline at end of file diff --git a/main_new.py b/main_new.py new file mode 100644 index 0000000..738068a --- /dev/null +++ b/main_new.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +""" +SalvageDB Bot - Главный файл запуска +Модульная архитектура с разделением хэндлеров +""" + +import asyncio +import signal +import sys +import logging + +from aiogram import Bot, Dispatcher +from aiogram.fsm.storage.memory import MemoryStorage + +from config.settings import BOT_TOKEN +from database import DatabaseManager +from middlewares.db import DbSessionMiddleware +from utils.logging_config import setup_logging +from utils.system_utils import get_operating_system, log_system_info + +# Импорт всех роутеров +from handlers.main_handlers import router as main_router +from handlers.vin_handlers import router as vin_router +from handlers.payment_handlers import router as payment_router +from handlers.admin.main_admin import router as admin_main_router + +# Глобальные переменные +bot = None +dp = None +database_manager = None + + +async def on_startup(): + """Инициализация при запуске""" + global database_manager + + logging.info("=== BOT STARTUP ===") + log_system_info() + + # Инициализируем базу данных + try: + database_manager = DatabaseManager() + await database_manager.initialize() + logging.info("Database manager initialized successfully") + except Exception as e: + logging.error(f"Failed to initialize database: {e}") + sys.exit(1) + + logging.info("Bot startup completed successfully") + + +async def on_shutdown(): + """Очистка при завершении""" + global database_manager + + logging.info("=== BOT SHUTDOWN ===") + + if database_manager: + await database_manager.close() + logging.info("Database connections closed") + + logging.info("Bot shutdown completed") + + +def setup_signal_handlers(): + """Настройка обработчиков сигналов для корректного завершения""" + + def signal_handler(signum, frame): + logging.info(f"Received signal {signum}, initiating graceful shutdown...") + + # Получаем текущий event loop + try: + loop = asyncio.get_running_loop() + # Создаем задачу для завершения + loop.create_task(shutdown_bot()) + except RuntimeError: + # Если loop не найден, завершаем принудительно + logging.warning("No running event loop found, forcing exit...") + sys.exit(0) + + # Регистрируем обработчики для разных сигналов + signal.signal(signal.SIGINT, signal_handler) # Ctrl+C + signal.signal(signal.SIGTERM, signal_handler) # Команда завершения + + # Для Windows добавляем обработку SIGBREAK + if get_operating_system() == 'Windows': + try: + signal.signal(signal.SIGBREAK, signal_handler) + except AttributeError: + pass # SIGBREAK может быть недоступен в некоторых версиях + + +async def shutdown_bot(): + """Корректное завершение работы бота""" + logging.info("Shutting down bot...") + + # Останавливаем polling + if dp: + await dp.stop_polling() + + # Закрываем сессию бота + if bot: + await bot.session.close() + + # Вызываем on_shutdown + await on_shutdown() + + logging.info("Bot shutdown complete, exiting...") + sys.exit(0) + + +def setup_routers(dispatcher: Dispatcher): + """Настройка всех роутеров""" + # Порядок важен - более специфичные роутеры должны быть первыми + + # Админ роутеры (самый высокий приоритет) + dispatcher.include_router(admin_main_router) + + # Платежные роутеры + dispatcher.include_router(payment_router) + + # VIN роутеры + dispatcher.include_router(vin_router) + + # Основные роутеры (самый низкий приоритет) + dispatcher.include_router(main_router) + + logging.info("All routers configured successfully") + + +async def main(): + """Главная функция запуска бота""" + global bot, dp + + # Настройка логирования + setup_logging() + + # Настройка обработчиков сигналов + setup_signal_handlers() + + logging.info("Starting SalvageDB Bot...") + logging.info(f"Python version: {sys.version}") + logging.info(f"Operating System: {get_operating_system()}") + + try: + # Инициализация бота и диспетчера + bot = Bot(token=BOT_TOKEN) + dp = Dispatcher(storage=MemoryStorage()) + + # Настройка middleware для работы с базой данных + dp.middleware.setup(DbSessionMiddleware()) + + # Настройка всех роутеров + setup_routers(dp) + + # Регистрация событий запуска и завершения + dp.startup.register(on_startup) + dp.shutdown.register(on_shutdown) + + # Запуск polling + logging.info("Starting bot polling...") + await dp.start_polling( + bot, + allowed_updates=['message', 'callback_query', 'pre_checkout_query'], + drop_pending_updates=True + ) + + except KeyboardInterrupt: + logging.info("Received KeyboardInterrupt, shutting down...") + except Exception as e: + logging.error(f"Fatal error in main: {e}") + raise + finally: + # Финальная очистка + if bot: + await bot.session.close() + logging.info("Bot stopped") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + logging.info("Bot interrupted by user") + except Exception as e: + logging.error(f"Fatal error: {e}") + sys.exit(1) \ No newline at end of file diff --git a/main_old.py b/main_old.py new file mode 100644 index 0000000..464f9f3 --- /dev/null +++ b/main_old.py @@ -0,0 +1,3614 @@ +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📈 Рост пользователей:\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💰 Тренд выручки:\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🛠 Популярность услуг (за 6 мес.):\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🎯 Конверсия по месяцам:\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"""📊 Анализ трендов бизнеса + +{user_growth_text} +{revenue_trend_text} +{services_trend_text} +{conversion_text} + +📅 Сгенерировано: {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📈 Последние 3 месяца:\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📊 Прогноз на следующий месяц: ~{avg_growth:,.0f} новых пользователей" + + # Сезонность по дням недели + seasonality_text = "" + if stats['seasonality']: + seasonality_text = "\n📅 Сезонность (дни недели):\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🌍 Потенциал роста по регионам:\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"""🔮 Прогнозы развития бизнеса + +{growth_analysis} +{seasonality_text} +{regional_potential} + +💡 Рекомендации: +• Сосредоточьтесь на днях с высокой активностью +• Развивайте регионы с низкой конверсией +• Планируйте маркетинг на основе сезонности + +📅 Сгенерировано: {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🚀 Топ регионы по росту:\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💰 Конверсия по регионам:\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👑 Premium по регионам:\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"""🌍 Анализ регионов роста + +{growth_regions} +{conversion_regions} +{premium_regions} + +💡 Выводы: +• Сосредоточьтесь на регионах с высоким ростом +• Изучите успешные стратегии топ-регионов +• Адаптируйте контент под местные особенности + +📅 Сгенерировано: {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"""🎯 Воронка монетизации: +• Всего пользователей: {total:,} +• Вовлеченные (>1 взаимодействия): {engaged:,} ({engagement_rate}%) +• Платящие: {paying:,} ({conversion_rate}%) +• Повторные покупки: {repeat:,} ({repeat_rate}%) +• Высокая ценность (>50⭐️): {high_value:,} ({high_value_rate}%)""" + + # LTV анализ + ltv_text = "" + if stats['ltv_analysis']: + ltv_text = "\n💎 Анализ LTV по сегментам:\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🛠 Прибыльность услуг (90 дней):\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⏱ Время до первой покупки: {avg_days or 0} дней в среднем ({purchases} покупок)" + + report = f"""💎 Анализ монетизации + +{funnel_text} +{ltv_text} +{profitability_text} +{time_to_purchase_text} + +💡 Рекомендации: +• Улучшите вовлечение новых пользователей +• Сосредоточьтесь на повторных покупках +• Оптимизируйте наименее прибыльные услуги + +📅 Сгенерировано: {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📉 Анализ оттока пользователей:\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⚠️ Неэффективные запросы (30 дней):\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"""🎯 Пользователи с высоким потенциалом: +• Активные без покупок: {engaged_non:,} +• Потенциал повторных покупок: {potential_repeat:,} +• Неэффективные Premium: {underperform_prem:,} +• Ценные неактивные: {valuable_dormant:,}""" + + # Анализ ценообразования + pricing_text = "" + if stats['pricing_analysis']: + pricing_text = "\n💰 Анализ цен по услугам:\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"""🎯 Анализ возможностей оптимизации + +{churn_text} +{inefficient_text} +{potential_text} +{pricing_text} + +🚀 Приоритетные действия: +• Работайте с активными неплательщиками +• Улучшите качество данных для проблемных услуг +• Реактивируйте ценных неактивных пользователей +• Оптимизируйте ценообразование + +📅 Сгенерировано: {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("🎯 Низкая конверсия: Улучшите онбординг и первое впечатление") + elif conversion_rate > 15: + recommendations.append("🎯 Высокая конверсия: Масштабируйте маркетинг для привлечения пользователей") + + if activity_rate < 30: + recommendations.append("⚡ Низкая активность: Внедрите программу реактивации пользователей") + + if premium_rate < 10: + recommendations.append("👑 Мало Premium: Усильте маркетинг 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"⚠️ {safe_service}: Низкий успех ({success_rate}%) - улучшите качество данных") + if refund_rate > 10: + safe_service = escape_markdown(str(service)) if service else "Unknown" + service_recommendations.append(f"💸 {safe_service}: Высокий возврат ({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"🚀 {flag} {lang}: Активный рост - увеличьте инвестиции в регион") + + # Формируем итоговый отчет + base_metrics_text = "" + if stats['base_metrics']: + total, paying, avg_ltv, active, premium = stats['base_metrics'] + base_metrics_text = f"""📊 Ключевые метрики: +• Конверсия: {round(paying/total*100, 1) if total > 0 else 0}% +• Активность: {round(active/total*100, 1) if total > 0 else 0}% +• Premium: {round(premium/total*100, 1) if total > 0 else 0}% +• Средний LTV: {avg_ltv or 0:.1f} ⭐️""" + + all_recommendations = recommendations + service_recommendations + regional_recommendations + + recommendations_text = "\n💡 Приоритетные рекомендации:\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"""💡 Бизнес рекомендации + +{base_metrics_text} +{recommendations_text} + +🎯 Следующие шаги: +• Внедрите приоритетные улучшения +• Проведите A/B тестирование изменений +• Отслеживайте метрики еженедельно +• Фокусируйтесь на ROI каждого действия + +📅 Сгенерировано: {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🌍 Топ языков:\n" + for lang, count, percentage in stats['top_languages']: + # Экранируем потенциально проблемные символы + safe_lang = str(lang).replace('&', '&').replace('<', '<').replace('>', '>') + flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪", "fr": "🇫🇷", "es": "🇪🇸", "it": "🇮🇹"}.get(lang, "🌐") + languages_text += f"• {flag} {safe_lang}: {count:,} ({percentage}%)\n" + + # Источники регистрации + sources_text = "" + if stats['registration_sources']: + sources_text = "\n📱 Источники регистрации:\n" + for source, count in stats['registration_sources']: + safe_source = str(source).replace('&', '&').replace('<', '<').replace('>', '>') + sources_text += f"• {safe_source}: {count:,}\n" + + report = f"""🌍 География пользователей + +📊 Языковая статистика: +• Всего языков: {stats['total_languages']} + +{languages_text} + +{sources_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="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📈 Доходы по дням (последние 10):\n" + for day, revenue, transactions in stats['daily_revenue']: + daily_chart += f"• {day}: {revenue:.0f} ⭐ ({transactions} транз.)\n" + + report = f"""💵 Анализ доходов + +💰 Общая статистика: +• Общая выручка: {stats['total_revenue']:,.0f} ⭐️ +• Всего транзакций: {stats['total_transactions']:,} +• Средний чек: {stats['avg_transaction']:.1f} ⭐️ +• Уникальных клиентов: {stats['unique_customers']:,} + +📊 Доходы по периодам: +• За 24 часа: {stats['revenue_24h']:,.0f} ⭐ ({stats['transactions_24h']} транз.) +• За 7 дней: {stats['revenue_7d']:,.0f} ⭐ ({stats['transactions_7d']} транз.) +• За 30 дней: {stats['revenue_30d']:,.0f} ⭐ ({stats['transactions_30d']} транз.) + +📈 Средние показатели: +• Доход на клиента: {round(stats['total_revenue']/stats['unique_customers'], 1) if stats['unique_customers'] > 0 else 0:.1f} ⭐ +• Транзакций на клиента: {round(stats['total_transactions']/stats['unique_customers'], 1) if stats['unique_customers'] > 0 else 0:.1f} + +{daily_chart} + +📅 Сгенерировано: {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 = """📊 Анализ по услугам + +📭 Нет данных + +В системе пока нет завершенных транзакций для анализа. + +💡 Возможные причины: +• Таблица payment_logs пустая +• Нет записей со статусом 'completed' +• Все транзакции имеют статус 'pending' или 'failed' + +📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}""" + else: + # Статистика по услугам + services_text = "" + if stats['services_breakdown']: + services_text = "\n📊 Breakdown по услугам:\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" 💰 Доход: {revenue:.0f} ⭐ ({transactions} транз.)\n" + services_text += f" 💵 Средняя цена: {avg_price:.1f} ⭐\n" + services_text += f" 👥 Клиентов: {users}\n" + services_text += f" ✅ Успешность: {success_rate}%\n" + services_text += f" ↩️ Возвраты: {refund_rate}%\n" + services_text += f" 📈 За неделю/месяц: {trends['week']}/{trends['month']}\n\n" + + report = f"""📊 Анализ по услугам + +{services_text} + +💡 Выводы: +• Наиболее доходная услуга по общей выручке +• Услуга с наивысшей конверсией +• Оптимизация ценообразования по успешности + +📅 Сгенерировано: {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"""🔄 Анализ конверсии + +🎯 Основная конверсия: +• Всего пользователей: {stats['total_users']:,} +• Платящих пользователей: {stats['paying_users']:,} +• Конверсия в покупку: {stats['conversion_rate']:.2f}% + +👥 Сегментация покупателей: +• Premium покупатели: {stats['premium_buyers']:,} +• Обычные покупатели: {stats['regular_buyers']:,} +• Средняя покупка: {stats['avg_purchase']:.1f} ⭐ + +🔄 Повторные покупки: +• Разовые покупатели: {stats['one_time_buyers']:,} +• Регулярные (2-5): {stats['regular_buyers_repeat']:,} +• Лояльные (5+): {stats['loyal_buyers']:,} +• Среднее покупок на клиента: {stats['avg_purchases_per_user']:.1f} + +📈 Retention метрики: +• Repeat Rate: {stats['repeat_rate']:.1f}% +• Всего транзакций: {stats['total_transactions']:,} + +💡 Анализ: +• {stats['repeat_rate']:.1f}% покупателей совершают повторные покупки +• Потенциал роста конверсии до {100 - stats['conversion_rate']:.1f}% + +📅 Сгенерировано: {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↩️ Возвраты по услугам:\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}: {count} шт. ({amount:.0f} ⭐)\n" + + report = f"""↩️ Анализ возвратов + +📊 Общая статистика: +• Всего транзакций: {stats['total_transactions']:,} +• Возвратов: {stats['refund_count']:,} +• Процент возвратов: {stats['refund_rate']:.2f}% +• Сумма возвратов: {stats['refund_amount']:,.0f} ⭐ + +🔄 Типы возвратов: +• Автоматические: {stats['auto_refunds']:,} +• Ручные: {stats['manual_refunds']:,} +• Админские: {stats['admin_refunds']:,} + +{refunds_breakdown} + +💡 Анализ: +• Низкий процент возвратов (<5%) - показатель качества +• Высокий процент (>10%) - требует оптимизации услуг +• Автовозвраты снижают нагрузку на поддержку + +📅 Сгенерировано: {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🚨 Ошибки по услугам:\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" - Ошибки платежей: {payment_failures}\n" + errors_breakdown += f" - Ошибки сервиса: {service_errors}\n" + errors_breakdown += f" - Нет данных: {no_data}\n" + + report = f"""💳 Анализ транзакций + +💰 Статистика платежей: +• Всего попыток: {stats['total_attempts']:,} +• Успешно: {stats['completed']:,} +• В ожидании: {stats['pending']:,} +• Неудачно: {stats['failed']:,} +• Успешность платежей: {stats['payment_success_rate']:.1f}% + +⚙️ Статистика сервисов: +• Успешные сервисы: {stats['service_success']:,} +• Нет данных: {stats['no_data']:,} +• Ошибки сервиса: {stats['service_error']:,} +• Успешность сервисов: {stats['service_success_rate']:.1f}% + +{errors_breakdown} + +📊 Ключевые метрики: +• Конверсия попытка → успех: {stats['payment_success_rate']:.1f}% +• Конверсия платеж → данные: {stats['service_success_rate']:.1f}% + +📅 Сгенерировано: {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⏰ Активность по часам (UTC):\n" + for hour, transactions, revenue in stats['hourly_distribution'][:12]: # Топ 12 часов + hourly_chart += f"• {int(hour):02d}:00 - {transactions} транз. ({revenue:.0f} ⭐)\n" + + # Топ VIN по доходам + top_vins_text = "" + if stats['top_vins']: + top_vins_text = "\n🏆 Топ VIN по доходам:\n" + for vin, requests, revenue in stats['top_vins'][:5]: # Топ 5 + top_vins_text += f"• {vin}: {requests} запр. = {revenue:.0f} ⭐\n" + + report = f"""🎯 Анализ эффективности + +💰 Финансовая эффективность: +• Доход на транзакцию: {stats['avg_revenue_per_transaction']:.1f} ⭐ +• Транзакций в день: {stats['avg_transactions_per_day']:.1f} +• Доход на клиента: {stats['revenue_per_customer']:.1f} ⭐ +• Транзакций на клиента: {stats['transactions_per_customer']:.1f} + +{hourly_chart} + +{top_vins_text} + +📊 Insights: +• LTV клиента: {stats['revenue_per_customer']:.0f} ⭐ +• Средняя частота покупок: {stats['transactions_per_customer']:.1f} +• Дневной оборот: {stats['avg_transactions_per_day'] * stats['avg_revenue_per_transaction']:.0f} ⭐ + +📅 Сгенерировано: {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⚙️ Производительность по услугам:\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" 📊 Запросов: {requests:,}\n" + services_text += f" ✅ Успешность: {success_rate}%\n" + services_text += f" 📈 Ср. данных: {avg_data}\n" + services_text += f" ↩️ Возвраты: {refund_rate}%\n\n" + + # Пиковые часы + peak_hours_text = "" + if stats['peak_hours']: + peak_hours_text = "\n⏰ Пиковые часы (топ-5):\n" + for hour, requests in stats['peak_hours'][:5]: + peak_hours_text += f"• {int(hour):02d}:00 UTC - {requests} запросов\n" + + report = f"""⏱️ Производительность системы + +📊 Общая статистика: +• Всего запросов: {stats['total_requests']:,} +• Успешных: {stats['successful_requests']:,} +• Общая успешность: {stats['success_rate']:.1f}% +• Без данных: {stats['no_data_requests']:,} +• Ошибки: {stats['error_requests']:,} +• Среднее данных на запрос: {stats['avg_data_found']:.1f} + +{services_text} + +📸 Статистика фотографий: +• VIN с фото: {stats['photos_stats']['vins_with_photos']:,} +• Всего фотографий: {stats['photos_stats']['total_photos']:,} +• Среднее фото на VIN: {stats['photos_stats']['avg_photos_per_vin']:.1f} + +{peak_hours_text} + +📅 Сгенерировано: {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🚨 Ошибки по услугам:\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" 📊 Запросов: {total_requests:,}\n" + service_errors_text += f" 🚨 Ошибки: {errors} ({error_rate}%)\n" + service_errors_text += f" 📭 Нет данных: {no_data} ({no_data_rate}%)\n" + service_errors_text += f" ↩️ Авто возвраты: {auto_refunds}\n\n" + + # Частые ошибки + common_errors_text = "" + if stats['common_errors']: + common_errors_text = "\n🔍 Частые ошибки:\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"• {error_text} - {count} раз\n" + + # Тренд ошибок + daily_errors_text = "" + if stats['daily_error_trend']: + daily_errors_text = "\n📈 Тренд ошибок (7 дней):\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}: {errors_count} ошибок\n" + + report = f"""🚨 Анализ ошибок системы + +📊 Общая статистика: +• Всего попыток: {stats['total_attempts']:,} +• Ошибки платежей: {stats['payment_failures']:,} +• Ошибки сервисов: {stats['service_errors']:,} +• Случаи "нет данных": {stats['no_data_cases']:,} +• Общий процент ошибок: {stats['error_rate']:.2f}% +• Авто возвраты: {stats['auto_refunds']:,} + +{service_errors_text} + +{common_errors_text} + +{daily_errors_text} + +💡 Анализ: +• Нормальный уровень ошибок: <5% +• Требует внимания: 5-10% +• Критично: >10% + +📅 Сгенерировано: {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⏰ Нагрузка по часам (UTC):\n" + for hour, requests, unique_users, avg_data in stats['hourly_distribution'][:8]: # Топ 8 + hourly_text += f"• {int(hour):02d}:00 - {requests} запр. ({unique_users} польз., ср.данных: {avg_data})\n" + + # Популярные VIN + popular_vins_text = "" + if stats['popular_vins']: + popular_vins_text = "\n🏆 Популярные VIN (топ-10):\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"• {vin}: {request_count} запр. ({unique_users} польз., успех: {success_rate}%)\n" + + # Concurrent пользователи + concurrent_text = "" + if stats['concurrent_users']: + concurrent_text = "\n👥 Concurrent активность (топ-6):\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}: {concurrent_users} польз. одновременно ({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"""📈 Мониторинг нагрузки + +📊 Средние показатели: +• Запросов в день: {stats['avg_daily_requests']:.1f} +• Пользователей в день: {stats['avg_daily_users']:.1f} +• Пиковый час: {int(stats['peak_hour']):02d}:00 UTC +• Макс. concurrent: {stats['peak_concurrent']} пользователей + +{hourly_text} + +{popular_vins_text} + +{concurrent_text} + +💡 Insights: +• Пиковая нагрузка в {int(stats['peak_hour']):02d}:00 UTC +• Среднее {avg_requests_per_user:.1f} запросов на пользователя +• Максимум {stats['peak_concurrent']} одновременных пользователей + +📅 Сгенерировано: {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🏪 Статистика аукционов:\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"• {auction_house}:\n" + auctions_text += f" 📊 Записей: {records:,}\n" + auctions_text += f" 📝 Покрытие названий: {title_coverage}%\n" + auctions_text += f" 💥 Покрытие повреждений: {damage_coverage}%\n" + auctions_text += f" 🚗 Годы: {min_year}-{max_year} (ср: {avg_year})\n\n" + + # Качество данных + quality_text = "" + if stats['data_quality']: + quality_text = "\n📋 Качество данных:\n" + for auction_house, title_cov, damage1_cov, odometer_cov in stats['data_quality']: + quality_text += f"• {auction_house}:\n" + quality_text += f" Названия: {title_cov}% | Повреждения: {damage1_cov}% | Пробег: {odometer_cov}%\n" + + # География + geo_text = "" + if stats['geographic_distribution']: + geo_text = "\n🗺️ География (топ-8):\n" + for auction_house, state_code, count in stats['geographic_distribution'][:8]: + geo_text += f"• {auction_house} - {state_code}: {count:,} записей\n" + + # Тренды обновлений + updates_text = "" + if stats['update_trends']: + updates_text = "\n📅 Тренды обновлений (6 мес.):\n" + for auction_house, month, updates_count in stats['update_trends'][:6]: + updates_text += f"• {auction_house} {month}: {updates_count:,} обновлений\n" + + report = f"""🏪 Анализ аукционных домов + +📊 Общая статистика: +• Всего записей: {stats['total_records']:,} +• Доля IAAI: {stats['iaai_percentage']:.1f}% +• Остальные источники: {100-stats['iaai_percentage']:.1f}% + +{auctions_text} + +{quality_text} + +{geo_text} + +{updates_text} + +💡 Выводы: +• IAAI - основной источник данных ({stats['iaai_percentage']:.0f}%) +• Качество данных варьируется по источникам +• Регулярные обновления поддерживают актуальность + +📅 Сгенерировано: {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 = """🔍 Проблемные VIN номера + +📭 Нет данных + +В системе пока нет завершенных транзакций для анализа проблемных VIN. + +💡 Возможные причины: +• Таблица payment_logs пустая +• Нет записей со статусом 'completed' +• Проблемы с подключением к базе данных + +📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}""" + else: + # VIN без данных + no_data_text = "" + if stats.get('no_data_vins'): + no_data_text = "\n📭 VIN без данных (топ-10):\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"• {vin}: {request_count} запр. ({unique_users} польз., посл: {date_str})\n" + + # VIN с ошибками + error_vins_text = "" + if stats.get('error_vins'): + error_vins_text = "\n🚨 VIN с ошибками (топ-8):\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"• {vin}: {error_count} ошибок ({affected_users} польз.)\n" + if error_sample and error_sample != "No error message": + error_vins_text += f" Пример: {error_text}\n" + + # VIN без фотографий + no_photos_text = "" + if stats.get('no_photos_vins'): + no_photos_text = "\n📸 VIN без фото (топ-8):\n" + for vin, photo_requests, unique_users in stats['no_photos_vins'][:8]: + no_photos_text += f"• {vin}: {photo_requests} запр. фото ({unique_users} польз.)\n" + + # Сводная статистика проблемных VIN + problem_summary_text = "" + if stats.get('problem_vins_summary'): + problem_summary_text = "\n🔍 Топ проблемных VIN:\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"• {vin}: {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✅ Нет VIN без данных с множественными запросами" + error_vins_text = "\n✅ Нет VIN с системными ошибками" + no_photos_text = "\n✅ Нет проблем с запросами фотографий" + problem_summary_text = "\n✅ Все VIN обрабатываются корректно" + + report = f"""🔍 Проблемные VIN номера + +📊 Общая доступность: +• Всего уникальных VIN: {availability['total_requested_vins']:,} +• Успешные VIN: {availability['successful_vins']:,} +• Успешность: {availability['success_rate']:.1f}% +• VIN без данных: {availability['no_data_vins']:,} +• VIN с ошибками: {availability['error_vins']:,} + +{no_data_text} + +{error_vins_text} + +{no_photos_text} + +{problem_summary_text} + +💡 Рекомендации: +• Проверить источники для VIN без данных +• Исследовать причины ошибок для проблемных VIN +• Пополнить базу фотографий для популярных VIN + +📅 Сгенерировано: {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🗄️ Здоровье БД:\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}: {record_count:,} записей ({oldest_str} - {newest_str})\n" + + # Рост данных + growth_text = "" + if stats['data_growth']: + growth_text = "\n📈 Рост данных (6 мес.):\n" + for month, new_transactions, new_users, revenue in stats['data_growth'][:6]: + growth_text += f"• {month}: {new_transactions} транз., {new_users} польз., {revenue:.0f} ⭐\n" + + # SLA метрики + sla_text = "" + if stats['sla_metrics']: + sla_text = "\n📊 SLA метрики (7 дней):\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} {availability:.1f}% ({successful_req}/{total_req})\n" + + # Capacity изображений + images_text = "" + if stats['images_capacity']: + images_text = "\n📸 Рост изображений (6 мес.):\n" + for month, new_images, new_vins in stats['images_capacity'][:6]: + images_text += f"• {month}: {new_images:,} фото, {new_vins:,} новых VIN\n" + + # Performance метрики + performance_text = "" + if stats['performance_metrics']: + performance_text = "\n⚡ Performance (7 дней):\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}: {avg_results:.1f} ср.результатов ({total_requests} запр.)\n" + + # Статус системы + status_emoji = "🟢" if stats['system_status'] == "Healthy" else "🟡" if stats['system_status'] == "Warning" else "🔴" + + report = f"""👀 Системный мониторинг + +🎯 Статус системы: {status_emoji} {stats['system_status']} +• Средний SLA: {stats['avg_sla']:.2f}% + +{db_health_text} + +{growth_text} + +{sla_text} + +{images_text} + +{performance_text} + +💡 Анализ системы: +• SLA ≥95%: Отличное состояние 🟢 +• SLA 90-95%: Требует внимания 🟡 +• SLA <90%: Критичное состояние 🔴 + +📅 Сгенерировано: {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) + \ No newline at end of file diff --git a/middlewares/db.py b/middlewares/db.py index 0723d5e..449055a 100644 --- a/middlewares/db.py +++ b/middlewares/db.py @@ -1,10 +1,10 @@ # middlewares/db.py from aiogram import BaseMiddleware from typing import Callable, Dict, Any, Awaitable -from db import OracleDatabase +from database import DatabaseManager class DbSessionMiddleware(BaseMiddleware): - def __init__(self, db: OracleDatabase): + def __init__(self, db: DatabaseManager): self.db = db async def __call__( diff --git a/run1.cmd b/run1.cmd new file mode 100644 index 0000000..4a1f771 --- /dev/null +++ b/run1.cmd @@ -0,0 +1,9 @@ +set DEBUG=1 +set BOT_TOKEN=6302522437:AAEDAYNrMuJCX5kt-IJ7CjT8AzfG3g-0mo0 +set BOT_NAME=salvagedb_bot +set db_user=salvagebot +set db_password=hlz1zm4n39nq6et0 +set db_dsn=89.110.92.87:17921/db1 +set ADMIN_USER_ID=604563487 +:1 +uv run main.py diff --git a/utils/system_utils.py b/utils/system_utils.py index 89f21b5..2600436 100644 --- a/utils/system_utils.py +++ b/utils/system_utils.py @@ -2,6 +2,8 @@ Системные утилиты и функции проверки ОС """ import platform +import logging +import os def get_operating_system() -> str: @@ -34,4 +36,41 @@ def is_linux() -> bool: def is_macos() -> bool: """Проверяет, запущен ли код на macOS""" - return get_operating_system() == 'macOS' \ No newline at end of file + return get_operating_system() == 'macOS' + + +def log_system_info(): + """ + Логирует информацию о системе при запуске + """ + os_name = get_operating_system() + python_version = platform.python_version() + platform_info = platform.platform() + + logging.info("=== SYSTEM INFORMATION ===") + logging.info(f"Operating System: {os_name}") + logging.info(f"Platform: {platform_info}") + logging.info(f"Python Version: {python_version}") + logging.info(f"Architecture: {platform.architecture()[0]}") + logging.info(f"Processor: {platform.processor()}") + + # Проверяем переменные окружения + if os.getenv('BOT_TOKEN'): + logging.info("BOT_TOKEN: ✅ Set") + else: + logging.warning("BOT_TOKEN: ❌ Not set") + + if os.getenv('DB_USER'): + logging.info("Database credentials: ✅ Set") + else: + logging.warning("Database credentials: ❌ Not set") + + # Проверяем запуск в Docker + if os.path.exists('/.dockerenv'): + logging.info("Environment: 🐳 Docker container") + logging.info(f"Container timezone: {os.getenv('TZ', 'UTC')}") + logging.info(f"Container user: {os.getenv('USER', 'unknown')}") + else: + logging.info("Environment: 🖥️ Host system") + + logging.info("=== SYSTEM CHECK COMPLETE ===") \ No newline at end of file