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

@@ -96,17 +96,106 @@ def detect_quote_intent(text, has_open_quote=False):
return None, None
def confirm_quotation(tenant_conn, phone):
"""Mark the open quotation as confirmed/accepted."""
def confirm_quotation(tenant_conn, phone, customer_id=None, tenant_id=None, branch_id=None):
"""Confirm the open quotation and convert it into a pending-payment sale.
- Validates stock for every line before touching anything.
- Creates the sale via pos_engine.process_sale (status 'pending_payment',
stock reserved, no cash movement — payable later at the counter).
- Notifies owners via the notification engine (best effort).
Returns dict {'quotation_id', 'sale_id', 'total', 'items'} on success,
dict {'error': ..., ...} on failure, or None if there was no open quote.
"""
qid = get_open_quotation(tenant_conn, phone)
if not qid:
return None
detail = get_quotation_detail(tenant_conn, qid)
if not detail or not detail.get('items'):
return None
# Resolve target branch (first active)
if not branch_id:
cur = tenant_conn.cursor()
cur.execute("SELECT id FROM branches ORDER BY id LIMIT 1")
row = cur.fetchone()
cur.close()
if not row:
return {'error': 'no_branch', 'quotation_id': qid}
branch_id = row[0]
# Resolve inventory ids and validate stock
cur = tenant_conn.cursor()
resolved = []
for it in detail['items']:
cur.execute("""
SELECT i.id, COALESCE(s.stock, 0) FROM inventory i
LEFT JOIN (
SELECT inventory_id, SUM(stock) AS stock FROM inventory_stock
GROUP BY inventory_id
) s ON s.inventory_id = i.id
WHERE i.part_number = %s AND i.is_active LIMIT 1
""", (it['part_number'],))
row = cur.fetchone()
if not row:
cur.close()
return {'error': 'item_not_found', 'part_number': it['part_number'],
'quotation_id': qid}
inv_id, stock = row
if (stock or 0) < it['quantity']:
cur.close()
return {'error': 'insufficient_stock', 'part_number': it['part_number'],
'available': int(stock or 0), 'requested': it['quantity'],
'quotation_id': qid}
resolved.append({
'inventory_id': inv_id,
'quantity': it['quantity'],
'unit_price': it['unit_price'],
'tax_rate': it['tax_rate'],
'discount_pct': 0,
})
cur.close()
from services.pos_engine import process_sale
sale_data = {
'items': resolved,
'customer_id': customer_id,
'payment_method': 'pendiente',
'sale_type': 'cash',
'branch_id': branch_id,
'amount_paid': 0,
'notes': f'Pedido por WhatsApp (cotizacion #{qid})',
}
try:
sale = process_sale(tenant_conn, sale_data)
except ValueError as e:
tenant_conn.rollback()
return {'error': 'sale_failed', 'message': str(e), 'quotation_id': qid}
except Exception:
tenant_conn.rollback()
raise
cur = tenant_conn.cursor()
cur.execute("UPDATE quotations SET status = 'converted' WHERE id = %s", (qid,))
tenant_conn.commit()
cur.close()
tenant_conn.commit()
clear_last_shown(tenant_conn, phone)
return qid
# Notify owners (best effort — never block the reply)
try:
if tenant_id:
from services.notification_engine import notify_new_sale
notify_new_sale(tenant_conn, tenant_id, sale['id'], sale['total'], 'whatsapp')
except Exception:
pass
return {
'quotation_id': qid,
'sale_id': sale['id'],
'total': sale['total'],
'items': detail['items'],
}
# ─── Persistent last-shown-part per phone ────────────────────────────