Добавлены новые функции для работы с VIN-номерами в классе OracleDatabase и основной логике бота:

- Расширен метод fetch_vin_info для возврата количества записей.
- Реализован новый метод fetch_detailed_vin_info для получения детальной информации о VIN.
- Обновлены обработчики в main.py для поддержки новых функций и интеграции с платежной системой.
- Добавлены кнопки для взаимодействия с пользователем и обработки успешных платежей.
This commit is contained in:
Vlad 2025-05-25 01:58:18 +03:00
parent dcec5ef020
commit fa11d2f0df
3 changed files with 317 additions and 13 deletions

78
db.py
View File

@ -35,25 +35,85 @@ class OracleDatabase:
import asyncio
return await asyncio.to_thread(_query)
async def fetch_vin_info(self, vin: str) -> Tuple[str, str, str]:
async def fetch_vin_info(self, vin: str) -> Tuple[str, str, str, int]:
# Manual async wrapper since oracledb is synchronous (threaded)
def _query():
with self._pool.acquire() as conn:
with conn.cursor() as cur:
query = """
select 'None',
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='26'),
(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Make'),'UNKNOWN') make,
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='28'),
(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model'),'UNKNOWN') model,
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='29'),
(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model Year'),'UNKNOWN') year
COALESCE((select value from salvagedb.m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='26'),
(select val from salvagedb.vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Make'),'UNKNOWN') make,
COALESCE((select value from salvagedb.m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='28'),
(select val from salvagedb.vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model'),'UNKNOWN') model,
COALESCE((select value from salvagedb.m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='29'),
(select val from salvagedb.vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model Year'),'UNKNOWN') year,
(select count(*) from salvagedb.m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin) cnt
from (select substr(:vin,1,10) svin, :vin vin from dual) s
"""
cur.execute(query, {"vin": vin})
result = cur.fetchone()
if result:
return result[1], result[2], result[3] # make, model, year
return "UNKNOWN", "UNKNOWN", "UNKNOWN"
return result[1], result[2], result[3], result[4] # make, model, year, cnt
return "UNKNOWN", "UNKNOWN", "UNKNOWN", 0
import asyncio
return await asyncio.to_thread(_query)
async def fetch_detailed_vin_info(self, vin: str) -> dict:
# Manual async wrapper since oracledb is synchronous (threaded)
def _query():
with self._pool.acquire() as conn:
with conn.cursor() as cur:
query = """
select dp.parameter_name,
dp.category_name,
d.value,
dp.endesc
from salvagedb.m_JSONS_FROM_NHTSA d
left join salvagedb.decode_params dp
on d.variableid = dp.parameter_id
where svin = substr(:vin, 1, 10)
order by dp.category_level
"""
cur.execute(query, {"vin": vin})
results = cur.fetchall()
# Organize data by categories
detailed_info = {
'basic_characteristics': {},
'engine_and_powertrain': {},
'active_safety': {},
'transmission': {},
'passive_safety': {},
'dimensions_and_construction': {},
'brake_system': {},
'lighting': {},
'additional_features': {},
'manufacturing_and_localization': {},
'ncsa_data': {},
'technical_information_and_errors': {},
'all_params': {} # Flat dictionary for easy access
}
for row in results:
param_name, category_name, value, description = row
if param_name and value:
# Create key from parameter name (lowercase, spaces to underscores)
key = param_name.lower().replace(' ', '_').replace('(', '').replace(')', '').replace('/', '_').replace('-', '_')
# Add to flat dictionary
detailed_info['all_params'][key] = value
# Add to category-specific dictionary
if category_name:
category_key = category_name.lower().replace(' ', '_').replace('(', '').replace(')', '').replace('/', '_').replace('-', '_')
if category_key in detailed_info:
detailed_info[category_key][key] = {
'value': value,
'description': description,
'param_name': param_name
}
return detailed_info
import asyncio
return await asyncio.to_thread(_query)

218
main.py
View File

@ -4,7 +4,7 @@ import logging
# import json
from aiogram import Bot, Dispatcher
from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery
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
@ -66,14 +66,56 @@ async def decode_vin_callback(callback: CallbackQuery, state: FSMContext):
await callback.answer()
@dp.callback_query(lambda c: c.data == "main_menu")
async def main_menu_callback(callback: CallbackQuery, state: FSMContext):
await state.clear()
await command_start_handler(callback.message)
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):
# Extract VIN from callback data
vin = callback.data.split(":")[1]
prices = [LabeledPrice(label="Detailed VIN Report", amount=1)]
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):
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 = await db.fetch_vin_info(vin)
response_text = f"🚗 **{year} {make} {model}**"
await message.answer(response_text, parse_mode="Markdown")
make, model, year, cnt = await db.fetch_vin_info(vin)
logging.info(f"Decode VIN 1st step: make: {make}, model: {model}, year: {year}, cnt: {cnt}")
response_text = f"🚗 **{year} {make} {model}**\n\n"
if cnt == 0:
response_text = "** Unable to decode VIN, possibly incorrect **"
# 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:
builder.button(text="Get detailed info. Pay 1 ⭐️", callback_data=f"pay_detailed_info:{vin}", pay=True)
builder.adjust(1, 1, 1) # Each button on separate row
else:
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 VIN {vin}: {e}")
await message.answer("Error retrieving data from database. Please try again later.")
@ -81,6 +123,174 @@ async def process_vin(message: Message, state: FSMContext, db: OracleDatabase):
else:
await message.answer("Invalid VIN. Please enter a valid 17-character VIN (letters and numbers, no I, O, Q).")
@dp.pre_checkout_query()
async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery):
await pre_checkout_query.answer(ok=True)
@dp.message(lambda message: message.successful_payment)
async def successful_payment_handler(message: Message, db: OracleDatabase):
payload = message.successful_payment.invoice_payload
if payload.startswith("detailed_vin_info:"):
vin = payload.split(":")[1]
try:
# Get detailed information from database
detailed_info = await db.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 += f"✅ **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:** {message.successful_payment.telegram_payment_charge_id}"
await message.answer(report, 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}"
)
else:
await message.answer(
f"✅ Payment successful! Thank you for your purchase.\n"
f"Transaction ID: {message.successful_payment.telegram_payment_charge_id}"
)
async def on_startup():
await oracle_db.connect()
dp.message.middleware(DbSessionMiddleware(oracle_db))

View File

@ -0,0 +1,34 @@
🚗 **{{ year }} {{ make }} {{ model }}**
{{ trim if trim else '' }}
📋 **BASIC INFO**
• **Make:** {{ make }}
• **Model:** {{ model }}
• **Year:** {{ model_year }}
• **Type:** {{ vehicle_type }}
• **Body:** {{ body_class }}
{% if doors %}• **Doors:** {{ doors }}{% endif %}
🔧 **ENGINE**
{% if engine_cylinders %}• **Engine:** {{ displacement_l }}L V{{ engine_cylinders }}{% endif %}
{% if displacement_l %}• **Displacement:** {{ displacement_l }}L ({{ displacement_ci }} CI){% endif %}
{% if engine_configuration %}• **Config:** {{ engine_configuration }}{% endif %}
{% if engine_brake_hp %}• **Power:** {{ engine_brake_hp }} HP{% if engine_brake_hp_range %} (up to {{ engine_brake_hp_range }} HP){% endif %}{% endif %}
{% if engine_power_kw %}• **Power (kW):** {{ engine_power_kw }} kW{% endif %}
{% if fuel_type_primary %}• **Fuel:** {{ fuel_type_primary }}{% endif %}
{% if valve_train_design %}• **Valve Train:** {{ valve_train_design }}{% endif %}
{% if engine_manufacturer %}• **Engine Mfg:** {{ engine_manufacturer }}{% endif %}
{% if seat_belts_type %}🛡️ **SAFETY**
• **Seat Belts:** {{ seat_belts_type }}
{% endif %}
🏭 **MANUFACTURING**
{% if manufacturer_name %}• **Manufacturer:** {{ manufacturer_name }}{% endif %}
{% if plant_city and plant_state %}• **Made in:** {{ plant_city }}, {{ plant_state }}{% endif %}
{% if plant_country %}• **Country:** {{ plant_country }}{% endif %}
{% if error_code and error_code == "0" %}✅ **No errors found**{% endif %}
---
📊 **Quick Specs:** {{ displacement_l }}L V{{ engine_cylinders if engine_cylinders else 'X' }} • {{ engine_brake_hp if engine_brake_hp else 'N/A' }} HP • {{ fuel_type_primary if fuel_type_primary else 'N/A' }} • {{ doors if doors else 'N/A' }}-door