Files
sales-bot-stacks/sales-bot/utils.py
consultoria-as ed1658eb2b feat: Implementar mejoras de funcionalidad y UX del Sales Bot
Nuevas funcionalidades:
- /cancelar: Cancelar ventas propias con motivo opcional
- /deshacer: Deshacer última venta (dentro de 5 minutos)
- /editar: Editar monto y cliente de ventas propias
- /comisiones: Historial de comisiones de últimos 6 meses
- /racha: Sistema de bonos por días consecutivos cumpliendo meta
- /exportar: Exportar ventas a Excel o CSV

Sistema de confirmación obligatoria:
- Todas las ventas requieren confirmación explícita (si/no)
- Preview de venta antes de registrar
- Timeout de 2 minutos para ventas pendientes

Scheduler de notificaciones:
- Recordatorio de mediodía para vendedores sin meta
- Resumen diario automático al final del día
- Resumen semanal los lunes

Otras mejoras:
- Soporte para múltiples imágenes en una venta
- Autocompletado de clientes frecuentes
- Metas personalizadas por vendedor
- Bonos por racha: $20 (3 días), $50 (5 días), $150 (10 días)

Archivos nuevos:
- export_utils.py: Generación de Excel y CSV
- scheduler.py: Tareas programadas con APScheduler

Dependencias nuevas:
- APScheduler==3.10.4
- openpyxl==3.1.2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 02:57:27 +00:00

311 lines
7.5 KiB
Python

"""
Utilidades para el Sales Bot
"""
import re
import os
import logging
logger = logging.getLogger(__name__)
def validar_token_outgoing(token):
"""
Valida el token de webhooks salientes de Mattermost
"""
if not token:
return False
expected_tokens = [
os.getenv('MATTERMOST_WEBHOOK_SECRET'),
os.getenv('MATTERMOST_OUTGOING_TOKEN'),
]
return token in [t for t in expected_tokens if t]
def extraer_monto(texto):
"""
Extrae el monto de una venta del texto del mensaje.
Soporta formatos:
- @monto 1500
- $1,500.00
- $1500
- 1500 pesos
"""
if not texto:
return None
texto = texto.lower()
# Buscar formato @monto XXXX
patron_monto = r'@monto\s+\$?([\d,]+\.?\d*)'
match = re.search(patron_monto, texto)
if match:
monto_str = match.group(1).replace(',', '')
try:
return float(monto_str)
except ValueError:
pass
# Buscar formato $X,XXX.XX o $XXXX
patron_dinero = r'\$\s*([\d,]+\.?\d*)'
match = re.search(patron_dinero, texto)
if match:
monto_str = match.group(1).replace(',', '')
try:
return float(monto_str)
except ValueError:
pass
# Buscar formato XXXX pesos
patron_pesos = r'([\d,]+\.?\d*)\s*pesos'
match = re.search(patron_pesos, texto)
if match:
monto_str = match.group(1).replace(',', '')
try:
return float(monto_str)
except ValueError:
pass
return None
def extraer_cliente(texto):
"""
Extrae el nombre del cliente del texto del mensaje.
Soporta formatos:
- @cliente Juan Pérez
- cliente: Juan Pérez
- a Juan Pérez
"""
if not texto:
return None
# Buscar formato @cliente NOMBRE
patron_cliente = r'@cliente\s+([^\n@$]+)'
match = re.search(patron_cliente, texto, re.IGNORECASE)
if match:
cliente = match.group(1).strip()
# Limpiar palabras clave que no son parte del nombre
cliente = re.sub(r'\s*@monto.*$', '', cliente, flags=re.IGNORECASE)
return cliente.strip() if cliente.strip() else None
# Buscar formato "cliente: NOMBRE" o "cliente NOMBRE"
patron_cliente2 = r'cliente[:\s]+([^\n@$]+)'
match = re.search(patron_cliente2, texto, re.IGNORECASE)
if match:
cliente = match.group(1).strip()
cliente = re.sub(r'\s*@monto.*$', '', cliente, flags=re.IGNORECASE)
return cliente.strip() if cliente.strip() else None
# Buscar formato "a NOMBRE" (después de un monto)
patron_a = r'\$[\d,\.]+\s+a\s+([^\n@$]+)'
match = re.search(patron_a, texto, re.IGNORECASE)
if match:
cliente = match.group(1).strip()
return cliente.strip() if cliente.strip() else None
return None
def formatear_moneda(valor, simbolo='$', decimales=2):
"""
Formatea un número como moneda.
Ejemplo: 1500.5 -> $1,500.50
"""
if valor is None:
return f"{simbolo}0.00"
try:
valor = float(valor)
return f"{simbolo}{valor:,.{decimales}f}"
except (ValueError, TypeError):
return f"{simbolo}0.00"
def extraer_tubos(texto):
"""
Extrae la cantidad de tubos del texto del mensaje.
Soporta formatos:
- @tubos 5
- tubos: 5
- 5 tubos
"""
if not texto:
return None
texto = texto.lower()
# Buscar formato @tubos XXXX
patron_tubos = r'@tubos\s+(\d+)'
match = re.search(patron_tubos, texto)
if match:
try:
return int(match.group(1))
except ValueError:
pass
# Buscar formato "tubos: X" o "tubos X"
patron_tubos2 = r'tubos[:\s]+(\d+)'
match = re.search(patron_tubos2, texto)
if match:
try:
return int(match.group(1))
except ValueError:
pass
# Buscar formato "X tubos"
patron_tubos3 = r'(\d+)\s*tubos?'
match = re.search(patron_tubos3, texto)
if match:
try:
return int(match.group(1))
except ValueError:
pass
return None
# ==================== NUEVAS FUNCIONES FASE 1 ====================
def extraer_id_venta(texto):
"""
Extrae el ID de venta del texto del comando.
Soporta formatos:
- /cancelar 123
- /editar 123 @monto 1500
- #123
"""
if not texto:
return None
# Buscar número al inicio o después del comando
patron_id = r'(?:^|\s)#?(\d+)'
match = re.search(patron_id, texto.strip())
if match:
try:
return int(match.group(1))
except ValueError:
pass
return None
def extraer_motivo(texto):
"""
Extrae el motivo de cancelación del texto.
Soporta formatos:
- /cancelar 123 "cliente no pagó"
- /cancelar 123 motivo: cliente canceló
"""
if not texto:
return None
# Buscar texto entre comillas
patron_comillas = r'["\']([^"\']+)["\']'
match = re.search(patron_comillas, texto)
if match:
return match.group(1).strip()
# Buscar después de "motivo:"
patron_motivo = r'motivo[:\s]+(.+)$'
match = re.search(patron_motivo, texto, re.IGNORECASE)
if match:
return match.group(1).strip()
# Si hay texto después del ID, usarlo como motivo
patron_resto = r'^\d+\s+(.+)$'
match = re.search(patron_resto, texto.strip())
if match:
motivo = match.group(1).strip()
# Excluir si es otro comando
if not motivo.startswith('@') and not motivo.startswith('/'):
return motivo
return None
def extraer_mes(texto):
"""
Extrae el mes del texto del comando.
Soporta formatos:
- 2026-01
- 01-2026
- enero 2026
- enero
"""
if not texto:
return None
texto = texto.strip().lower()
# Formato YYYY-MM
patron_iso = r'(\d{4})-(\d{1,2})'
match = re.search(patron_iso, texto)
if match:
return f"{match.group(1)}-{match.group(2).zfill(2)}"
# Formato MM-YYYY
patron_inv = r'(\d{1,2})-(\d{4})'
match = re.search(patron_inv, texto)
if match:
return f"{match.group(2)}-{match.group(1).zfill(2)}"
# Nombres de meses en español
meses = {
'enero': '01', 'febrero': '02', 'marzo': '03', 'abril': '04',
'mayo': '05', 'junio': '06', 'julio': '07', 'agosto': '08',
'septiembre': '09', 'octubre': '10', 'noviembre': '11', 'diciembre': '12'
}
for nombre, num in meses.items():
if nombre in texto:
# Buscar año
patron_anio = r'(\d{4})'
match = re.search(patron_anio, texto)
if match:
return f"{match.group(1)}-{num}"
# Si no hay año, usar el actual
from datetime import datetime
return f"{datetime.now().year}-{num}"
return None
def parsear_formato_exportar(texto):
"""
Parsea los parámetros del comando /exportar.
Retorna (formato, mes)
"""
if not texto:
return 'excel', None
texto = texto.strip().lower()
# Detectar formato
formato = 'excel'
if 'csv' in texto:
formato = 'csv'
texto = texto.replace('csv', '').strip()
# Detectar mes
mes = extraer_mes(texto)
return formato, mes
def validar_tokens_comando(token, nombre_comando):
"""
Valida tokens para comandos slash.
"""
if not token:
return False
expected_tokens = [
os.getenv(f'MATTERMOST_SLASH_TOKEN_{nombre_comando.upper()}'),
os.getenv('MATTERMOST_OUTGOING_TOKEN'),
os.getenv('MATTERMOST_WEBHOOK_SECRET'),
]
return token in [t for t in expected_tokens if t]