From fa11d2f0dfc31b9716cb9541f80133a898781346 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sun, 25 May 2025 01:58:18 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20VIN-=D0=BD=D0=BE?= =?UTF-8?q?=D0=BC=D0=B5=D1=80=D0=B0=D0=BC=D0=B8=20=D0=B2=20=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D1=81=D0=B5=20OracleDatabase=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D1=81=D0=BD=D0=BE=D0=B2=D0=BD=D0=BE=D0=B9=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B8=D0=BA=D0=B5=20=D0=B1=D0=BE=D1=82=D0=B0:=20-=20=D0=A0?= =?UTF-8?q?=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B5=D0=BD=20=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B4=20fetch=5Fvin=5Finfo=20=D0=B4=D0=BB=D1=8F=20=D0=B2?= =?UTF-8?q?=D0=BE=D0=B7=D0=B2=D1=80=D0=B0=D1=82=D0=B0=20=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D1=81=D1=82=D0=B2=D0=B0=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B5=D0=B9.=20-=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9?= =?UTF-8?q?=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20fetch=5Fdetailed=5Fvin=5Fin?= =?UTF-8?q?fo=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BE=20VIN.=20-=20=D0=9E=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D1=87=D0=B8=D0=BA=D0=B8=20=D0=B2=20main.py=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=B9=20=D0=B8=20=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D1=81=20=D0=BF?= =?UTF-8?q?=D0=BB=D0=B0=D1=82=D0=B5=D0=B6=D0=BD=D0=BE=D0=B9=20=D1=81=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B5=D0=BC=D0=BE=D0=B9.=20-=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BD=D0=BE=D0=BF?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B7=D0=B0=D0=B8?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=BC=20=D0=B8=20=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D1=83=D1=81=D0=BF=D0=B5=D1=88?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BF=D0=BB=D0=B0=D1=82=D0=B5=D0=B6=D0=B5?= =?UTF-8?q?=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db.py | 78 ++++++++-- main.py | 218 +++++++++++++++++++++++++++- telegram_post_template_decodevin.j2 | 34 +++++ 3 files changed, 317 insertions(+), 13 deletions(-) create mode 100644 telegram_post_template_decodevin.j2 diff --git a/db.py b/db.py index 4144dc2..ffd57e1 100644 --- a/db.py +++ b/db.py @@ -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) diff --git a/main.py b/main.py index 07e3a72..0cdd7f0 100644 --- a/main.py +++ b/main.py @@ -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)) diff --git a/telegram_post_template_decodevin.j2 b/telegram_post_template_decodevin.j2 new file mode 100644 index 0000000..9d2c9d6 --- /dev/null +++ b/telegram_post_template_decodevin.j2 @@ -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 \ No newline at end of file