savagedb_bot/main.py

918 lines
46 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

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

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

import asyncio
from os import getenv
import logging
from datetime import datetime
import platform
import os
from aiogram import Bot, Dispatcher
from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, LabeledPrice, PreCheckoutQuery
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 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'
if getenv("DEBUG",'0') == '1':
logging.basicConfig(level=logging.INFO)
else:
logging.basicConfig(level=logging.WARNING)
# Временно включаем детальное логирование для отладки
logging.getLogger().setLevel(logging.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)
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()
# 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="Search 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")
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 == "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()
await command_start_handler(callback.message, database)
await callback.answer()
@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=1)]
logging.info(f"Sending invoice for VIN: {vin}")
await callback.bot.send_invoice(
chat_id=callback.message.chat.id,
title="Detailed VIN Information",
description="Get comprehensive vehicle detailed information for 1 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="Pay 10 ⭐️ 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=10)]
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="Get comprehensive salvage history and damage information for 10 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.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)
ADMIN_USER_ID = int(getenv("ADMIN_USER_ID", "0")) # ID администратора из переменных окружения
@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
# Определяем сумму платежа в зависимости от типа
payment_amount = 10.0 if payload.startswith("detailed_salvage_check:") else 1.0
await database.update_user_payment(message.from_user.id, payment_amount)
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!")
# Проверяем, является ли пользователь администратором и возвращаем звезды
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"
)
else:
# No detailed information found - refund the payment
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}")
# 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!")
# Отправляем отдельное сообщение о фотографиях
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
)
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:
# Нет записей - возвращаем деньги
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}")
# Возвращаем деньги при ошибке
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}"
)
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"
)
async def on_startup():
log_system_info() # Логируем информацию о системе
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:
bot = Bot(token=TOKEN)
dp.startup.register(on_startup)
dp.shutdown.register(on_shutdown)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())