feat: module toggles in POS config and Instance Manager
- 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
This commit is contained in:
127
pos/services/wa_learning.py
Normal file
127
pos/services/wa_learning.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user