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

@@ -378,17 +378,32 @@ def handle_quote_search(text, state_data, ctx):
search_text = search_text.strip()
state_data['search_description'] = search_text
# Memoria conversacional + historial de compras + persona por tenant
conv_history = []
try:
from blueprints.whatsapp_bp import _get_conversation_history
conv_history = _get_conversation_history(ctx.phone, ctx.tenant_conn, limit=4)
except Exception:
conv_history = []
purchase_ctx = None
try:
from services.part_kits import get_purchase_history
purchase_ctx = get_purchase_history(ctx.phone, ctx.tenant_conn) or None
except Exception:
purchase_ctx = None
persona = (ctx.wa_config or {}).get('ai_persona') or None
try:
from services.ai_chat import chat, chat_with_image
if ctx.media_kind == 'image' and ctx.media_base64:
ai_resp = chat_with_image(
user_message=search_text if search_text != '(imagen)' else 'Identifica esta parte automotriz y sugiere terminos de busqueda.',
image_base64=ctx.media_base64,
conversation_history=[],
inventory_context=None,
conversation_history=conv_history,
inventory_context=purchase_ctx,
)
else:
ai_resp = chat(search_text, conversation_history=[], inventory_context=None)
ai_resp = chat(search_text, conversation_history=conv_history, inventory_context=purchase_ctx, persona=persona)
search_query = ai_resp.get('search_query')
vehicle = ai_resp.get('vehicle')
@@ -669,7 +684,7 @@ def handle_quote_command_created(text, state_data, ctx):
try:
from services.wa_quotation import get_open_quotation, get_quotation_detail
from services.quote_image import generate_quote_image
from services.whatsapp_service import send_image
from services.whatsapp_service import wa_send_image
qid = get_open_quotation(ctx.tenant_conn, ctx.phone)
if qid:
@@ -688,11 +703,11 @@ def handle_quote_command_created(text, state_data, ctx):
}
tenant_name = _get_tenant_business_name(ctx.tenant_conn)
b64_img = generate_quote_image(quote_items, totals, tenant_name=tenant_name)
img_result = send_image(
img_result = wa_send_image(
ctx.wa_config,
ctx.phone,
caption="Aquí está tu cotización 👇",
base64_image=b64_img,
bridge_url=ctx.wa_config.get('bridge_url')
)
if img_result.get('success'):
return (
@@ -715,19 +730,45 @@ def handle_quote_command_created(text, state_data, ctx):
if t in ('confirmar', 'confirmo', 'acepto', 'si', 'sí', 'va', 'ordenar', 'pedir'):
from services.wa_quotation import confirm_quotation
qid = confirm_quotation(ctx.tenant_conn, ctx.phone)
if qid:
reply = (
"✅ *¡Pedido confirmado!*\n\n"
"Tu cotización fue registrada.\n"
"Nos pondremos en contacto contigo para coordinar la entrega.\n\n"
"¡Gracias por tu compra! 🙏"
)
clean_data = {
'customer_id': state_data.get('customer_id'),
'branch_name': state_data.get('branch_name'),
}
return reply, 'support_done', clean_data
result = confirm_quotation(ctx.tenant_conn, ctx.phone,
customer_id=ctx.customer_id,
tenant_id=ctx.tenant_id,
branch_id=ctx.branch_id)
clean_data = {
'customer_id': state_data.get('customer_id'),
'branch_name': state_data.get('branch_name'),
}
if result and result.get('sale_id'):
lines = []
lines.append("✅ *¡Pedido confirmado!*")
lines.append("")
lines.append(f"Tu pedido *#{result['sale_id']}* quedó registrado y separado.")
lines.append("Pasa por tu refaccionaria oco coordina la entrega al confirmar el pago.\n")
lines.append("*Detalle:*")
for it in result.get('items', []):
lines.append(f"• {it['quantity']} x {it['part_number']}")
lines.append("")
lines.append(f"*Total:* ${result['total']:,.2f}")
lines.append("")
lines.append("¡Gracias por tu compra! 🙏")
return "\n".join(lines), 'support_done', clean_data
if result and result.get('error') == 'insufficient_stock':
return (
f"⚠️ No hay stock suficiente de *{result['part_number']}*\n"
f"Disponible: {result['available']} — Solicitado: {result['requested']}\n\n"
"Tu cotización sigue activa. ¿Quieres *confirmar* el resto, "
"*agregar* otra pieza o *limpiar* el pedido?",
'quote_command_created', state_data)
if result and result.get('error') == 'item_not_found':
return (
f"⚠️ La pieza *{result['part_number']}* ya no está disponible.\n\n"
"¿Quieres *agregar* otra pieza o *limpiar* el pedido?",
'quote_command_created', state_data)
if result:
return (
"⚠️ No pude registrar tu pedido en este momento. "
"Intenta de nuevo en unos minutos o contáctanos directamente.",
'quote_command_created', state_data)
if t in ('limpiar', 'borrar', 'nueva', 'cancelar', 'reset'):
from services.wa_quotation import clear_quotation