- Add GET/PUT /pos/api/config/modules endpoints in POS config_bp.py - Update sidebar.js to filter nav items based on enabled modules - Add Modules section to POS config.html with toggles for WhatsApp, Marketplace, MercadoLibre - Add module load/save logic to POS config.js - Preload modules in app-init.js for sidebar caching - Add tenant module management to Instance Manager - get_tenant_modules / update_tenant_modules in tenant_service.py - GET/PUT /api/tenants/<id>/modules endpoints in tenants_bp.py - Add modules modal to manager index.html - Add module editing UI and logic to manager.js - Add toggle-switch CSS to manager.css
128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
"""
|
|
WhatsApp Learning Service — ruta de aprendizaje para piezas no resueltas.
|
|
|
|
Registra sesiones donde el bot no pudo identificar una pieza, y las resuelve
|
|
asíncronamente cuando el cliente realiza una compra futura.
|
|
"""
|
|
|
|
import json
|
|
|
|
|
|
def register_unresolved_search(phone, customer_id, description, offered_parts, tenant_conn):
|
|
"""Registrar una sesión no resuelta para aprendizaje futuro."""
|
|
if not tenant_conn or not phone or not description:
|
|
return None
|
|
cur = tenant_conn.cursor()
|
|
cur.execute("""
|
|
INSERT INTO wa_learning_sessions (phone, customer_id, description, offered_parts, status, created_at)
|
|
VALUES (%s, %s, %s, %s, 'pending', NOW())
|
|
RETURNING id
|
|
""", (phone, customer_id, description, json.dumps(offered_parts or [])))
|
|
sid = cur.fetchone()[0]
|
|
tenant_conn.commit()
|
|
cur.close()
|
|
return sid
|
|
|
|
|
|
def find_pending_sessions(phone, tenant_conn):
|
|
"""Buscar sesiones pendientes de aprendizaje para un número WA."""
|
|
if not tenant_conn or not phone:
|
|
return []
|
|
cur = tenant_conn.cursor()
|
|
cur.execute("""
|
|
SELECT id, description, offered_parts, created_at
|
|
FROM wa_learning_sessions
|
|
WHERE phone = %s AND status = 'pending'
|
|
ORDER BY created_at DESC
|
|
""", (phone,))
|
|
rows = cur.fetchall()
|
|
cur.close()
|
|
return [{'id': r[0], 'description': r[1], 'offered_parts': r[2], 'created_at': str(r[3])} for r in rows]
|
|
|
|
|
|
def find_pending_sessions_by_customer(customer_id, tenant_conn):
|
|
"""Buscar sesiones pendientes por customer_id."""
|
|
if not tenant_conn or not customer_id:
|
|
return []
|
|
cur = tenant_conn.cursor()
|
|
cur.execute("""
|
|
SELECT id, phone, description, offered_parts, created_at
|
|
FROM wa_learning_sessions
|
|
WHERE customer_id = %s AND status = 'pending'
|
|
ORDER BY created_at DESC
|
|
""", (customer_id,))
|
|
rows = cur.fetchall()
|
|
cur.close()
|
|
return [{'id': r[0], 'phone': r[1], 'description': r[2], 'offered_parts': r[3], 'created_at': str(r[4])} for r in rows]
|
|
|
|
|
|
def resolve_session(session_id, resolved_part_id, sale_id, tenant_conn):
|
|
"""Marcar sesión como resuelta con la pieza comprada."""
|
|
if not tenant_conn or not session_id:
|
|
return
|
|
cur = tenant_conn.cursor()
|
|
cur.execute("""
|
|
UPDATE wa_learning_sessions
|
|
SET status = 'learned', resolved_part_id = %s, resolution_sale_id = %s, resolved_at = NOW()
|
|
WHERE id = %s
|
|
""", (resolved_part_id, sale_id, session_id))
|
|
tenant_conn.commit()
|
|
cur.close()
|
|
|
|
|
|
def get_learning_pairs_for_training(tenant_conn, limit=100):
|
|
"""Obtener pares (descripción del cliente → pieza real) para entrenamiento."""
|
|
if not tenant_conn:
|
|
return []
|
|
cur = tenant_conn.cursor()
|
|
cur.execute("""
|
|
SELECT l.description, i.name, i.part_number, i.brand
|
|
FROM wa_learning_sessions l
|
|
JOIN inventory i ON i.id = l.resolved_part_id
|
|
WHERE l.status = 'learned' AND l.resolved_at > NOW() - INTERVAL '90 days'
|
|
ORDER BY l.resolved_at DESC
|
|
LIMIT %s
|
|
""", (limit,))
|
|
rows = cur.fetchall()
|
|
cur.close()
|
|
return [{'description': r[0], 'part_name': r[1], 'part_number': r[2], 'brand': r[3]} for r in rows]
|
|
|
|
|
|
def check_learning_resolution(sale_id, customer_id, tenant_conn):
|
|
"""
|
|
Hook para llamar después de completar una venta.
|
|
Verifica si esta venta resuelve una sesión de aprendizaje pendiente.
|
|
"""
|
|
if not tenant_conn or not customer_id:
|
|
return
|
|
|
|
sessions = find_pending_sessions_by_customer(customer_id, tenant_conn)
|
|
if not sessions:
|
|
return
|
|
|
|
# Obtener items de esta venta
|
|
cur = tenant_conn.cursor()
|
|
cur.execute("""
|
|
SELECT si.inventory_id, i.name, i.part_number
|
|
FROM sale_items si
|
|
JOIN inventory i ON i.id = si.inventory_id
|
|
WHERE si.sale_id = %s
|
|
""", (sale_id,))
|
|
sale_items = cur.fetchall()
|
|
cur.close()
|
|
|
|
if not sale_items:
|
|
return
|
|
|
|
# Matching heurístico
|
|
for sess in sessions:
|
|
desc_words = set(sess['description'].lower().split())
|
|
for inv_id, item_name, part_number in sale_items:
|
|
item_words = set(item_name.lower().split())
|
|
# Intersección de palabras significativas
|
|
common = desc_words & item_words - {'de', 'la', 'el', 'para', 'un', 'una', 'con', 'y', 'o', 'en', 'al', 'del', 'los', 'las'}
|
|
if len(common) >= 2:
|
|
resolve_session(sess['id'], inv_id, sale_id, tenant_conn)
|
|
print(f"[WA-LEARN] Resolved session {sess['id']} with sale {sale_id}, item {inv_id}")
|
|
break
|