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