Files
Autoparts-DB/pos/services/wa_learning.py
consultoria-as 718fa06888 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
2026-05-28 00:21:52 +00:00

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