1430 lines
73 KiB
Python
1430 lines
73 KiB
Python
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, 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 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 ""
|
||
|
||
# Базовый путь из константы
|
||
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:
|
||
# 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:
|
||
# Проверяем существование файла
|
||
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)}"
|
||
)
|
||
|
||
|
||
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()
|
||
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="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 == "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()
|
||
await command_start_handler(callback.message, database)
|
||
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. Search 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 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)
|
||
|
||
|
||
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
|
||
|
||
# Определяем сумму платежа в зависимости от типа
|
||
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)
|
||
|
||
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}"
|
||
)
|
||
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)
|
||
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}")
|
||
|
||
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)
|
||
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)")
|
||
|
||
# Проверяем, является ли пользователь администратором и возвращаем звезды
|
||
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 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}")
|
||
|
||
# Возвращаем деньги при ошибке
|
||
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():
|
||
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())
|
||
|