Добавлены новые функции для работы с VIN-номерами в классе OracleDatabase и основной логике бота:
- Расширен метод fetch_vin_info для возврата количества записей. - Реализован новый метод fetch_detailed_vin_info для получения детальной информации о VIN. - Обновлены обработчики в main.py для поддержки новых функций и интеграции с платежной системой. - Добавлены кнопки для взаимодействия с пользователем и обработки успешных платежей.
This commit is contained in:
parent
dcec5ef020
commit
fa11d2f0df
78
db.py
78
db.py
@ -35,25 +35,85 @@ class OracleDatabase:
|
|||||||
import asyncio
|
import asyncio
|
||||||
return await asyncio.to_thread(_query)
|
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)
|
# Manual async wrapper since oracledb is synchronous (threaded)
|
||||||
def _query():
|
def _query():
|
||||||
with self._pool.acquire() as conn:
|
with self._pool.acquire() as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
query = """
|
query = """
|
||||||
select 'None',
|
select 'None',
|
||||||
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='26'),
|
COALESCE((select value from salvagedb.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,
|
(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 m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='28'),
|
COALESCE((select value from salvagedb.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,
|
(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 m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='29'),
|
COALESCE((select value from salvagedb.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
|
(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
|
from (select substr(:vin,1,10) svin, :vin vin from dual) s
|
||||||
"""
|
"""
|
||||||
cur.execute(query, {"vin": vin})
|
cur.execute(query, {"vin": vin})
|
||||||
result = cur.fetchone()
|
result = cur.fetchone()
|
||||||
if result:
|
if result:
|
||||||
return result[1], result[2], result[3] # make, model, year
|
return result[1], result[2], result[3], result[4] # make, model, year, cnt
|
||||||
return "UNKNOWN", "UNKNOWN", "UNKNOWN"
|
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
|
import asyncio
|
||||||
return await asyncio.to_thread(_query)
|
return await asyncio.to_thread(_query)
|
||||||
|
|||||||
218
main.py
218
main.py
@ -4,7 +4,7 @@ import logging
|
|||||||
# import json
|
# import json
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
from aiogram.filters import Command
|
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.utils.keyboard import InlineKeyboardBuilder
|
||||||
from aiogram.fsm.state import State, StatesGroup
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
@ -66,14 +66,56 @@ async def decode_vin_callback(callback: CallbackQuery, state: FSMContext):
|
|||||||
await callback.answer()
|
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)
|
@dp.message(VinStates.waiting_for_vin)
|
||||||
async def process_vin(message: Message, state: FSMContext, db: OracleDatabase):
|
async def process_vin(message: Message, state: FSMContext, db: OracleDatabase):
|
||||||
vin = message.text.strip().upper()
|
vin = message.text.strip().upper()
|
||||||
if len(vin) == 17 and vin.isalnum() and all(c not in vin for c in ["I", "O", "Q"]):
|
if len(vin) == 17 and vin.isalnum() and all(c not in vin for c in ["I", "O", "Q"]):
|
||||||
try:
|
try:
|
||||||
make, model, year = await db.fetch_vin_info(vin)
|
make, model, year, cnt = await db.fetch_vin_info(vin)
|
||||||
response_text = f"🚗 **{year} {make} {model}**"
|
logging.info(f"Decode VIN 1st step: make: {make}, model: {model}, year: {year}, cnt: {cnt}")
|
||||||
await message.answer(response_text, parse_mode="Markdown")
|
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:
|
except Exception as e:
|
||||||
logging.error(f"Database error for VIN {vin}: {e}")
|
logging.error(f"Database error for VIN {vin}: {e}")
|
||||||
await message.answer("Error retrieving data from database. Please try again later.")
|
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:
|
else:
|
||||||
await message.answer("Invalid VIN. Please enter a valid 17-character VIN (letters and numbers, no I, O, Q).")
|
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():
|
async def on_startup():
|
||||||
await oracle_db.connect()
|
await oracle_db.connect()
|
||||||
dp.message.middleware(DbSessionMiddleware(oracle_db))
|
dp.message.middleware(DbSessionMiddleware(oracle_db))
|
||||||
|
|||||||
34
telegram_post_template_decodevin.j2
Normal file
34
telegram_post_template_decodevin.j2
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user