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