feat(pos): add inventory-aware chat context (#31) and VIN decoder (#17)

Chat now fetches tenant inventory summary (brands, counts, low-stock)
and injects it into the AI system prompt so responses prioritize local
stock. VIN decoder uses free NHTSA vPIC API to decode 17-char VINs and
auto-fills the vehicle selector dropdowns when a catalog match is found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 08:14:45 +00:00
parent 5d5a2777eb
commit f9589f4a4e
6 changed files with 406 additions and 5 deletions

View File

@@ -17,6 +17,7 @@ from flask import Blueprint, request, jsonify, g
from middleware import require_auth
from tenant_db import get_master_conn, get_tenant_conn
from services import catalog_service
from services.vin_decoder import decode_vin
catalog_bp = Blueprint('catalog', __name__, url_prefix='/pos/api/catalog')
@@ -184,3 +185,126 @@ def search():
data = catalog_service.smart_search(master, q, tenant, branch_id, limit)
return jsonify({'data': data})
return _with_conns(_do)
# ─── VIN Decoder ───
@catalog_bp.route('/vin/<vin>', methods=['GET'])
@require_auth('catalog.view')
def decode_vin_route(vin):
"""Decode a VIN and try to match to a brand/model/year in our catalog DB."""
vin = (vin or "").strip().upper()
if len(vin) != 17:
return jsonify({'error': 'VIN debe tener exactamente 17 caracteres.'}), 400
try:
info = decode_vin(vin)
except Exception as e:
return jsonify({'error': f'Error al decodificar VIN: {str(e)}'}), 502
if info.get('error'):
return jsonify(info), 200 # Return info even with partial errors
# Try to match the decoded vehicle to our catalog DB
db_match = None
master = None
try:
master = get_master_conn()
db_match = _match_vin_to_catalog(master, info)
except Exception:
pass
finally:
if master:
try:
master.close()
except Exception:
pass
result = {**info}
if db_match:
result['catalog_match'] = db_match
return jsonify(result)
def _match_vin_to_catalog(master_conn, vin_info):
"""Try to find brand_id, model_id, year_id, mye_id from decoded VIN info."""
make = (vin_info.get('make') or '').upper().strip()
model = (vin_info.get('model') or '').strip()
year = vin_info.get('year')
if not make:
return None
cur = master_conn.cursor()
result = {}
try:
# Find brand (try exact, then LIKE)
cur.execute(
"SELECT id_brand, name_brand FROM brands WHERE UPPER(name_brand) = %s",
(make,)
)
brand_row = cur.fetchone()
if not brand_row:
cur.execute(
"SELECT id_brand, name_brand FROM brands WHERE UPPER(name_brand) LIKE %s ORDER BY name_brand LIMIT 1",
(f"%{make}%",)
)
brand_row = cur.fetchone()
if not brand_row:
return None
result['brand_id'] = brand_row[0]
result['brand_name'] = brand_row[1]
# Find model
if model:
cur.execute(
"""SELECT m.id_model, m.name_model
FROM models m
WHERE m.brand_id = %s AND UPPER(m.name_model) LIKE %s
ORDER BY m.name_model LIMIT 5""",
(brand_row[0], f"%{model.upper()}%")
)
model_row = cur.fetchone()
if model_row:
result['model_id'] = model_row[0]
result['model_name'] = model_row[1]
# Find year
if year:
cur.execute(
"SELECT id_year, year_car FROM years WHERE year_car = %s",
(int(year),)
)
year_row = cur.fetchone()
if year_row:
result['year_id'] = year_row[0]
result['year_car'] = year_row[1]
# Find MYE options
cur.execute(
"""SELECT mye.id_mye, e.name_engine, mye.trim_level
FROM model_year_engine mye
JOIN engines e ON e.id_engine = mye.engine_id
WHERE mye.model_id = %s AND mye.year_id = %s
ORDER BY e.name_engine
LIMIT 10""",
(model_row[0], year_row[0])
)
mye_rows = cur.fetchall()
if mye_rows:
result['engines'] = [
{'id_mye': r[0], 'name_engine': r[1], 'trim_level': r[2]}
for r in mye_rows
]
# Auto-select if only one engine
if len(mye_rows) == 1:
result['id_mye'] = mye_rows[0][0]
return result
except Exception:
return None
finally:
cur.close()

View File

@@ -23,8 +23,23 @@ def chat():
history = body.get("history") or []
# Call AI
ai_response = ai_chat.chat(user_message, history)
# Fetch inventory context so the AI knows what this tenant has in stock
inventory_context = None
tenant_for_context = None
try:
tenant_for_context = get_tenant_conn(g.tenant_id)
inventory_context = ai_chat.get_inventory_context(tenant_for_context, g.branch_id)
except Exception:
pass
finally:
if tenant_for_context:
try:
tenant_for_context.close()
except Exception:
pass
# Call AI with inventory context
ai_response = ai_chat.chat(user_message, history, inventory_context=inventory_context)
search_results = []
vehicle_match = None