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>
This commit is contained in:
2026-01-19 02:57:27 +00:00
parent 5d9cbd4812
commit ed1658eb2b
8 changed files with 1808 additions and 148 deletions

View File

@@ -164,3 +164,147 @@ def extraer_tubos(texto):
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]