Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos
- F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado
- F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment
- F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings)
- F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant
- Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas
- Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant)
- Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant
- Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
This commit is contained in:
2026-10-12 00:57:31 +00:00
parent f2d4558b80
commit a386ca5b4a
165 changed files with 31313 additions and 2203 deletions

View File

@@ -19,11 +19,32 @@ from tenant_db import get_master_conn, get_tenant_conn
from services import catalog_service
from services.vin_decoder import decode_vin
from services.plate_lookup import search_plate, register_plate, is_valid_mexican_plate, normalize_plate
from services.inventory_vehicle_compat import get_inventory_by_vehicle
from config import CATALOG_OEM_ENABLED
catalog_bp = Blueprint('catalog', __name__, url_prefix='/pos/api/catalog')
# ─── Guard: tablas TecDoc retiradas (junio 2026) ──────────────────────────
_TECDOC_TABLES_CACHE = {'checked': False, 'exists': False}
def _tecdoc_available(conn):
"""True si las tablas TecDoc del catálogo OEM existen en la maestra."""
if _TECDOC_TABLES_CACHE['checked']:
return _TECDOC_TABLES_CACHE['exists']
try:
cur = conn.cursor()
cur.execute("SELECT 1 FROM pg_tables WHERE schemaname='public' AND tablename='parts' LIMIT 1")
exists = cur.fetchone() is not None
cur.close()
except Exception:
exists = False
_TECDOC_TABLES_CACHE['checked'] = True
_TECDOC_TABLES_CACHE['exists'] = exists
return exists
def _oem_blocked():
"""Return a 403 response if OEM catalog is disabled."""
if not CATALOG_OEM_ENABLED:
@@ -66,6 +87,9 @@ def _with_conns(fn):
branch_id = request.args.get('branch_id', g.branch_id)
return fn(master, tenant, branch_id)
except Exception as e:
if _is_retired_schema_error(e):
return jsonify({'data': [], 'oem_disabled': True,
'error': 'Función no disponible (esquema TecDoc retirado)'}, 200), 200
return jsonify({'error': str(e)}), 500
finally:
if master:
@@ -76,6 +100,15 @@ def _with_conns(fn):
except: pass
def _is_retired_schema_error(e):
"""True si el error es por tablas TecDoc retiradas del esquema."""
try:
from psycopg2.errors import UndefinedTable, UndefinedColumn
return isinstance(e, (UndefinedTable, UndefinedColumn))
except Exception:
return 'does not exist' in str(e)
def _master_only(fn):
"""Helper: open only master connection for hierarchy endpoints."""
master = None
@@ -83,6 +116,9 @@ def _master_only(fn):
master = get_master_conn()
return fn(master)
except Exception as e:
if _is_retired_schema_error(e):
return jsonify({'data': [], 'oem_disabled': True,
'error': 'Función no disponible (esquema TecDoc retirado)'}, 200), 200
return jsonify({'error': str(e)}), 500
finally:
if master:
@@ -487,6 +523,33 @@ def decode_vin_route(vin):
# ─── Plate Lookup ───
@catalog_bp.route('/vehicle-inventory', methods=['GET'])
@require_auth('catalog.view')
def vehicle_inventory():
"""Local inventory items compatible with a vehicle (model_year_engine_id)."""
mye_id = request.args.get('mye_id', type=int)
if not mye_id:
return jsonify({'error': 'mye_id requerido'}), 400
tenant = get_tenant_conn(g.tenant_id)
try:
rows = get_inventory_by_vehicle(tenant, None, mye_id,
branch_id=getattr(g, 'branch_id', None))
items = [{
'id': r[0], 'part_number': r[1], 'name': r[2], 'brand': r[3] or '',
'price_1': float(r[4] or 0), 'price_2': float(r[5] or 0),
'price_3': float(r[6] or 0), 'image_url': r[7] or '',
'stock': int(r[9] or 0),
} for r in rows]
return jsonify({'data': items})
finally:
try:
tenant.close()
except Exception:
pass
@catalog_bp.route('/plate/<plate>', methods=['GET'])
@require_auth('catalog.view')
def plate_lookup(plate):
@@ -541,6 +604,9 @@ def plate_lookup(plate):
return jsonify(response)
except Exception as e:
if _is_retired_schema_error(e):
return jsonify({'data': [], 'oem_disabled': True,
'error': 'Función no disponible (esquema TecDoc retirado)'}, 200), 200
return jsonify({'error': str(e)}), 500
finally:
if tenant:
@@ -576,6 +642,9 @@ def plate_register():
)
return jsonify({'id': rec_id, 'message': 'Placa registrada.'})
except Exception as e:
if _is_retired_schema_error(e):
return jsonify({'data': [], 'oem_disabled': True,
'error': 'Función no disponible (esquema TecDoc retirado)'}, 200), 200
return jsonify({'error': str(e)}), 500
finally:
if tenant: