# /home/Autopartes/pos/blueprints/catalog_bp.py """Catalog blueprint: TecDoc vehicle navigation with local stock enrichment. Endpoints (all under /pos/api/catalog): GET /brands — vehicle brands with parts GET /models?brand_id= — models for a brand GET /years?model_id= — years for a model GET /engines?model_id=&year_id= — engines for model+year GET /categories?mye_id= — part categories for vehicle GET /groups?mye_id=&category_id= — part subcategories for vehicle+category GET /parts?mye_id=&group_id= — parts with local stock enrichment GET /part/ — full part detail (stock + bodegas + alternatives) GET /search?q= — smart search (part number or text) """ 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') def _with_conns(fn): """Helper: open master + tenant connections, call fn, close both. fn receives (master_conn, tenant_conn, branch_id). """ master = None tenant = None try: master = get_master_conn() tenant = get_tenant_conn(g.tenant_id) branch_id = request.args.get('branch_id', g.branch_id) return fn(master, tenant, branch_id) except Exception as e: return jsonify({'error': str(e)}), 500 finally: if master: try: master.close() except: pass if tenant: try: tenant.close() except: pass def _master_only(fn): """Helper: open only master connection for hierarchy endpoints.""" master = None try: master = get_master_conn() return fn(master) except Exception as e: return jsonify({'error': str(e)}), 500 finally: if master: try: master.close() except: pass # ─── Hierarchy navigation (master DB only) ─── @catalog_bp.route('/brands', methods=['GET']) @require_auth('catalog.view') def brands(): year_id = request.args.get('year_id', type=int) def _do(master): data = catalog_service.get_brands(master, year_id=year_id) return jsonify({'data': data}) return _master_only(_do) @catalog_bp.route('/models', methods=['GET']) @require_auth('catalog.view') def models(): brand_id = request.args.get('brand_id', type=int) year_id = request.args.get('year_id', type=int) if not brand_id: return jsonify({'error': 'brand_id required'}), 400 def _do(master): data = catalog_service.get_models(master, brand_id, year_id=year_id) return jsonify({'data': data}) return _master_only(_do) @catalog_bp.route('/years', methods=['GET']) @require_auth('catalog.view') def years(): model_id = request.args.get('model_id', type=int) if not model_id: return jsonify({'error': 'model_id required'}), 400 def _do(master): data = catalog_service.get_years(master, model_id) return jsonify({'data': data}) return _master_only(_do) @catalog_bp.route('/years-all', methods=['GET']) @require_auth('catalog.view') def years_all(): """Get all available years (for vehicle selector dropdown).""" def _do(master): cur = master.cursor() cur.execute("SELECT DISTINCT id_year, year_car FROM years ORDER BY year_car DESC") rows = cur.fetchall() cur.close() return jsonify({'data': [{'id_year': r[0], 'year_car': r[1]} for r in rows]}) return _master_only(_do) @catalog_bp.route('/engines', methods=['GET']) @require_auth('catalog.view') def engines(): model_id = request.args.get('model_id', type=int) year_id = request.args.get('year_id', type=int) if not model_id or not year_id: return jsonify({'error': 'model_id and year_id required'}), 400 def _do(master): data = catalog_service.get_engines(master, model_id, year_id) return jsonify({'data': data}) return _master_only(_do) @catalog_bp.route('/categories', methods=['GET']) @require_auth('catalog.view') def categories(): mye_id = request.args.get('mye_id', type=int) if not mye_id: return jsonify({'error': 'mye_id required'}), 400 def _do(master): data = catalog_service.get_categories(master, mye_id) return jsonify({'data': data}) return _master_only(_do) @catalog_bp.route('/groups', methods=['GET']) @require_auth('catalog.view') def groups(): mye_id = request.args.get('mye_id', type=int) category_id = request.args.get('category_id', type=int) if not mye_id or not category_id: return jsonify({'error': 'mye_id and category_id required'}), 400 def _do(master): data = catalog_service.get_groups(master, mye_id, category_id) return jsonify({'data': data}) return _master_only(_do) # ─── Parts with stock enrichment (master + tenant) ─── @catalog_bp.route('/parts', methods=['GET']) @require_auth('catalog.view') def parts(): mye_id = request.args.get('mye_id', type=int) group_id = request.args.get('group_id', type=int) page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 30, type=int) if not mye_id or not group_id: return jsonify({'error': 'mye_id and group_id required'}), 400 def _do(master, tenant, branch_id): result = catalog_service.get_parts(master, mye_id, group_id, tenant, branch_id, page, per_page) return jsonify(result) return _with_conns(_do) @catalog_bp.route('/part/', methods=['GET']) @require_auth('catalog.view') def part_detail(part_id): def _do(master, tenant, branch_id): result = catalog_service.get_part_detail(master, part_id, tenant, branch_id) if not result: return jsonify({'error': 'Part not found'}), 404 return jsonify(result) return _with_conns(_do) @catalog_bp.route('/search', methods=['GET']) @require_auth('catalog.view') def search(): q = request.args.get('q', '').strip() if not q or len(q) < 2: return jsonify({'data': []}) limit = request.args.get('limit', 50, type=int) def _do(master, tenant, branch_id): 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/', 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()