""" Handlers para procesamiento de ventas en Sales Bot Incluye sistema de confirmación interactiva obligatoria """ import logging import re import os from datetime import datetime, timedelta, timezone from utils import extraer_monto, extraer_cliente, formatear_moneda, extraer_tubos from ocr_processor import OCRProcessor logger = logging.getLogger(__name__) # Zona horaria de México TZ_MEXICO = timezone(timedelta(hours=-6)) # Cache de ventas pendientes de confirmación # Formato: {username: {datos_venta, timestamp}} VENTAS_PENDIENTES = {} # Timeout para confirmación (en minutos) CONFIRMACION_TIMEOUT = int(os.getenv('CONFIRMACION_TIMEOUT_MINUTOS', 2)) def limpiar_ventas_expiradas(): """Elimina ventas pendientes que han expirado""" ahora = datetime.now(TZ_MEXICO) expiradas = [] for username, datos in VENTAS_PENDIENTES.items(): timestamp = datos.get('timestamp') if timestamp: if ahora - timestamp > timedelta(minutes=CONFIRMACION_TIMEOUT): expiradas.append(username) for username in expiradas: del VENTAS_PENDIENTES[username] logger.info(f"Venta pendiente de {username} expirada") def handle_venta_message(data, mattermost, nocodb): """ Maneja mensajes de venta en Mattermost con confirmación interactiva obligatoria. Flujo: 1. Usuario envía mensaje de venta 2. Bot muestra preview y pide confirmación 3. Usuario responde "si" o "no" 4. Bot registra la venta (si confirmada) """ try: user_name = data.get('user_name') text = data.get('text', '').strip() text_lower = text.lower() # Limpiar ventas expiradas limpiar_ventas_expiradas() # ==================== PROCESAR CONFIRMACIÓN ==================== # Si es confirmación de venta pendiente if text_lower in ['si', 'sí', 'confirmar', 'ok', 'yes'] and user_name in VENTAS_PENDIENTES: venta_data = VENTAS_PENDIENTES.pop(user_name) return registrar_venta_confirmada(venta_data, mattermost, nocodb) # Si es cancelación de venta pendiente if text_lower in ['no', 'cancelar', 'cancel'] and user_name in VENTAS_PENDIENTES: VENTAS_PENDIENTES.pop(user_name) mensaje = '❌ **Venta cancelada**\n\nPuedes registrar una nueva venta cuando quieras.' mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':x:') return {'text': mensaje} # Si ya tiene venta pendiente, recordar if user_name in VENTAS_PENDIENTES: venta_pendiente = VENTAS_PENDIENTES[user_name] tiempo_restante = CONFIRMACION_TIMEOUT - (datetime.now(TZ_MEXICO) - venta_pendiente['timestamp']).seconds // 60 mensaje = ( f"⏳ **Ya tienes una venta pendiente**\n\n" f"**Monto:** ${venta_pendiente.get('monto', 0):,.2f}\n" f"**Cliente:** {venta_pendiente.get('cliente', 'Sin especificar')}\n\n" f"Responde **si** para confirmar o **no** para cancelar.\n" f"_Expira en {tiempo_restante} minuto(s)_" ) mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':hourglass:') return {'text': mensaje} # ==================== PROCESAR NUEVA VENTA ==================== channel_name = data.get('channel_name') post_id = data.get('post_id') file_ids = data.get('file_ids', '') if file_ids and isinstance(file_ids, str): file_ids = [f.strip() for f in file_ids.split(',') if f.strip()] elif not file_ids: file_ids = [] logger.info(f"Procesando venta de {user_name}: {text}, archivos: {file_ids}") # Extraer información del texto monto = extraer_monto(text) cliente = extraer_cliente(text) tubos_manual = extraer_tubos(text) # ==================== PROCESAR IMÁGENES (MÚLTIPLES) ==================== imagenes_url = [] ocr_info = "" productos_ocr = [] monto_ocr_total = 0 tubos_ocr_total = 0 if file_ids: logger.info(f"Procesando {len(file_ids)} archivos adjuntos") for file_id in file_ids: try: imagen_data = mattermost.get_file(file_id) file_info = mattermost.get_file_info(file_id) if file_info and imagen_data: filename = file_info.get('name', 'ticket.jpg') file_size = file_info.get('size', 0) bot_token = os.getenv('MATTERMOST_BOT_TOKEN') mattermost_url = os.getenv('MATTERMOST_URL') imagen_url = f"{mattermost_url}/api/v4/files/{file_id}?access_token={bot_token}" imagenes_url.append(imagen_url) logger.info(f"Archivo adjunto: {filename}, tamaño: {file_size} bytes") # Procesar con OCR try: ocr = OCRProcessor() resultado_ocr = ocr.procesar_ticket(imagen_data) if resultado_ocr: monto_ocr = resultado_ocr.get('monto_detectado', 0) fecha_ocr = resultado_ocr.get('fecha_detectada') productos = resultado_ocr.get('productos', []) if monto_ocr: monto_ocr_total += monto_ocr if productos: productos_ocr.extend(productos) # Contar tubos de tinte for p in productos: marca = p.get('marca', '').lower() producto = p.get('producto', '').lower() if 'tinte' in marca or 'tinte' in producto or 'cromatique' in marca: tubos_ocr_total += p.get('cantidad', 0) if fecha_ocr and not ocr_info: ocr_info += f"\n📅 Fecha ticket: {fecha_ocr}" except Exception as ocr_error: logger.error(f"Error en OCR para {file_id}: {str(ocr_error)}") except Exception as file_error: logger.error(f"Error procesando archivo {file_id}: {str(file_error)}") # Resumen de OCR if len(file_ids) > 1: ocr_info += f"\n📷 Imágenes procesadas: {len(imagenes_url)}" if monto_ocr_total > 0: ocr_info += f"\n💡 Monto OCR: ${monto_ocr_total:,.2f}" if not monto: monto = monto_ocr_total if tubos_ocr_total > 0: ocr_info += f"\n🧪 Tubos detectados: {tubos_ocr_total}" if productos_ocr: ocr_info += f"\n📦 Productos: {len(productos_ocr)}" # ==================== VALIDAR DATOS ==================== if not monto: # Sugerir clientes frecuentes si no hay cliente sugerencias = "" if not cliente: clientes_frecuentes = nocodb.get_clientes_frecuentes(user_name, limit=3) if clientes_frecuentes: lista = '\n'.join([f" • {c}" for c in clientes_frecuentes]) sugerencias = f"\n\n💡 **Clientes frecuentes:**\n{lista}" mensaje = ( f"@{user_name} Necesito el monto de la venta.\n\n" "**Formatos válidos:**\n" "• `venta @monto 1500 @cliente Juan Pérez`\n" "• `vendí $1500 a Juan Pérez`\n" "• `venta @monto 1500 @tubos 3`\n" "• Adjunta foto del ticket" f"{sugerencias}" ) mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':moneybag:') return {'text': mensaje} if not cliente: cliente = "Cliente sin nombre" # Usar tubos manuales o de OCR tubos_detectados = tubos_manual or tubos_ocr_total or 0 # ==================== CREAR PREVIEW Y PEDIR CONFIRMACIÓN ==================== # Guardar datos de la venta pendiente VENTAS_PENDIENTES[user_name] = { 'user_name': user_name, 'monto': monto, 'cliente': cliente, 'tubos': tubos_detectados, 'tubos_manual': tubos_manual, 'productos_ocr': productos_ocr, 'imagenes_url': imagenes_url, 'ocr_info': ocr_info, 'channel_name': channel_name, 'post_id': post_id, 'text': text, 'timestamp': datetime.now(TZ_MEXICO) } # Construir mensaje de preview mensaje_preview = f"📋 **Preview de Venta**\n\n" mensaje_preview += f"**Vendedor:** @{user_name}\n" mensaje_preview += f"**Monto:** ${monto:,.2f}\n" mensaje_preview += f"**Cliente:** {cliente}\n" if tubos_detectados > 0: mensaje_preview += f"**Tubos:** {tubos_detectados} 🧪\n" if imagenes_url: mensaje_preview += f"**Tickets:** {len(imagenes_url)} imagen(es) 📸\n" if ocr_info: mensaje_preview += f"\n**Datos detectados:**{ocr_info}\n" mensaje_preview += f"\n¿Confirmar esta venta? Responde **si** o **no**\n" mensaje_preview += f"_(Expira en {CONFIRMACION_TIMEOUT} minutos)_" mattermost.post_message_webhook(mensaje_preview, username='Sales Bot', icon_emoji=':clipboard:') return {'text': mensaje_preview} except Exception as e: logger.error(f"Error en handle_venta_message: {str(e)}", exc_info=True) mensaje_error = f"❌ Error: {str(e)}" return {'text': mensaje_error} def registrar_venta_confirmada(venta_data, mattermost, nocodb): """ Registra una venta después de ser confirmada por el usuario. """ try: user_name = venta_data.get('user_name') monto = venta_data.get('monto') cliente = venta_data.get('cliente') tubos_manual = venta_data.get('tubos_manual') productos_ocr = venta_data.get('productos_ocr', []) imagenes_url = venta_data.get('imagenes_url', []) ocr_info = venta_data.get('ocr_info', '') channel_name = venta_data.get('channel_name') post_id = venta_data.get('post_id') text = venta_data.get('text', '') # Verificar/crear vendedor vendedor = nocodb.get_vendedor(user_name) if not vendedor: user_info = mattermost.get_user_by_username(user_name) email = user_info.get('email', f"{user_name}@consultoria-as.com") if user_info else f"{user_name}@consultoria-as.com" nombre = user_info.get('first_name', user_name) if user_info else user_name vendedor = nocodb.crear_vendedor( username=user_name, nombre_completo=nombre, email=email, meta_diaria_tubos=3 ) if vendedor: mensaje_bienvenida = ( f"👋 ¡Bienvenido @{user_name}!\n" f"**Sistema de comisiones:**\n" f"• Meta diaria: {nocodb.META_DIARIA_TUBOS} tubos de tinte\n" f"• Comisión: ${nocodb.COMISION_POR_TUBO:.0f} por tubo después del {nocodb.META_DIARIA_TUBOS}º\n" f"¡Empieza a registrar tus ventas!" ) mattermost.post_message_webhook(mensaje_bienvenida, username='Sales Bot', icon_emoji=':wave:') # Usar primera imagen como principal imagen_url = imagenes_url[0] if imagenes_url else None # Registrar venta venta = nocodb.registrar_venta( vendedor_username=user_name, monto=monto, cliente=cliente, descripcion=text, mensaje_id=post_id, canal=channel_name, imagen_url=imagen_url ) if venta: venta_id = venta.get('Id') # Guardar productos detectados por OCR if productos_ocr: productos_guardados = nocodb.guardar_productos_venta(venta_id, productos_ocr) if productos_guardados: logger.info(f"Guardados {len(productos_guardados)} productos para venta {venta_id}") # Guardar tubos manuales si se especificaron elif tubos_manual and tubos_manual > 0: productos_manuales = [{ 'producto': 'Tinte (registro manual)', 'marca': 'Manual', 'cantidad': tubos_manual, 'precio_unitario': monto / tubos_manual if tubos_manual > 0 else 0, 'importe': monto }] nocodb.guardar_productos_venta(venta_id, productos_manuales) logger.info(f"Guardados {tubos_manual} tubos manuales para venta {venta_id}") # Actualizar tabla de metas try: nocodb.actualizar_meta_vendedor(user_name) logger.info(f"Metas actualizadas para {user_name}") except Exception as meta_error: logger.error(f"Error actualizando metas: {str(meta_error)}") # Verificar racha racha = nocodb.verificar_racha(user_name) # Reacción de éxito if post_id: mattermost.add_reaction(post_id, 'white_check_mark') # Obtener estadísticas del día stats_dia = nocodb.get_estadisticas_vendedor_dia(user_name) # Construir mensaje de confirmación mensaje_confirmacion = ( f"✅ **Venta #{venta_id} registrada**\n\n" f"**Vendedor:** @{user_name}\n" f"**Monto:** {formatear_moneda(monto)}\n" f"**Cliente:** {cliente}\n" ) if imagenes_url: mensaje_confirmacion += f"📸 **Tickets:** {len(imagenes_url)} guardado(s){ocr_info}\n" # Mostrar estadísticas de tubos y comisiones if stats_dia: tubos_hoy = stats_dia.get('tubos_vendidos', 0) comision_hoy = stats_dia.get('comision', 0) meta = stats_dia.get('meta_diaria', 3) tubos_comisionables = stats_dia.get('tubos_comisionables', 0) # Determinar emoji según progreso if tubos_hoy >= meta * 2: emoji = '🔥' mensaje_extra = '¡Increíble día!' elif tubos_hoy >= meta: emoji = '⭐' mensaje_extra = '¡Meta cumplida!' elif tubos_hoy >= meta - 1: emoji = '💪' mensaje_extra = '¡Casi llegas!' else: emoji = '📊' mensaje_extra = '¡Sigue así!' mensaje_confirmacion += ( f"\n**Resumen del día:** {emoji}\n" f"• Tubos vendidos hoy: {tubos_hoy} 🧪\n" f"• Meta diaria: {meta} tubos\n" ) if tubos_hoy > meta: mensaje_confirmacion += ( f"• Tubos con comisión: {tubos_comisionables}\n" f"• Comisión ganada hoy: {formatear_moneda(comision_hoy)} 💰\n" ) else: faltan = meta - tubos_hoy mensaje_confirmacion += f"• Faltan {faltan} tubos para comisión\n" mensaje_confirmacion += f"• {mensaje_extra}" # Mostrar info de racha si aplica if racha and racha.get('dias_consecutivos', 0) >= 3: dias = racha['dias_consecutivos'] bonus = racha.get('bonus', 0) mensaje_confirmacion += f"\n\n🔥 **Racha: {dias} días** " if bonus > 0: mensaje_confirmacion += f"(+${bonus:,.2f} bonus)" # Enviar confirmación mattermost.post_message_webhook( mensaje_confirmacion, username='Sales Bot', icon_emoji=':moneybag:' ) return {'text': mensaje_confirmacion} else: mensaje_error = "❌ Error al registrar la venta. Intenta de nuevo." mattermost.post_message_webhook(mensaje_error, username='Sales Bot', icon_emoji=':x:') return {'text': mensaje_error} except Exception as e: logger.error(f"Error en registrar_venta_confirmada: {str(e)}", exc_info=True) mensaje_error = f"❌ Error: {str(e)}" return {'text': mensaje_error} def generar_reporte_diario(mattermost, nocodb): """ Genera reporte diario de ventas y comisiones """ try: hoy = datetime.now(TZ_MEXICO).strftime('%Y-%m-%d') # Obtener todas las ventas del día ventas_hoy = nocodb.get_ventas_dia() # Agrupar por vendedor vendedores_hoy = {} for venta in ventas_hoy: vendedor = venta.get('vendedor_username') if vendedor not in vendedores_hoy: vendedores_hoy[vendedor] = [] vendedores_hoy[vendedor].append(venta) # Calcular estadísticas por vendedor stats_vendedores = [] for vendedor in vendedores_hoy.keys(): stats = nocodb.get_estadisticas_vendedor_dia(vendedor, hoy) if stats: stats_vendedores.append(stats) # Ordenar por tubos vendidos stats_vendedores.sort(key=lambda x: x.get('tubos_vendidos', 0), reverse=True) # Calcular totales total_tubos_dia = sum(s.get('tubos_vendidos', 0) for s in stats_vendedores) total_comisiones = sum(s.get('comision', 0) for s in stats_vendedores) total_monto = sum(s.get('monto_total_dia', 0) for s in stats_vendedores) # Construir mensaje mensaje = ( f"📊 **Reporte Diario - {datetime.now(TZ_MEXICO).strftime('%d/%m/%Y')}**\n\n" f"**Resumen del día:**\n" f"• Tubos vendidos: {total_tubos_dia} 🧪\n" f"• Comisiones pagadas: {formatear_moneda(total_comisiones)} 💰\n" f"• Monto total: {formatear_moneda(total_monto)}\n" f"• Ventas: {len(ventas_hoy)}\n\n" ) if stats_vendedores: mensaje += "**Top Vendedores del Día:**\n" for i, stats in enumerate(stats_vendedores[:5], 1): vendedor = stats.get('vendedor') tubos = stats.get('tubos_vendidos', 0) comision = stats.get('comision', 0) emoji = '🥇' if i == 1 else '🥈' if i == 2 else '🥉' if i == 3 else '🏅' if comision > 0: mensaje += f"{emoji} @{vendedor} - {tubos} tubos ({formatear_moneda(comision)} comisión)\n" else: mensaje += f"{emoji} @{vendedor} - {tubos} tubos\n" # Obtener canal de reportes team_name = os.getenv('MATTERMOST_TEAM_NAME') channel_reportes = os.getenv('MATTERMOST_CHANNEL_REPORTES') if channel_reportes: canal = mattermost.get_channel_by_name(team_name, channel_reportes) if canal: mattermost.post_message(canal['id'], mensaje) logger.info("Reporte diario generado") return {'status': 'success', 'message': 'Reporte generado'} else: logger.warning(f"Canal {channel_reportes} no encontrado") return {'status': 'error', 'message': 'Canal no encontrado'} else: # Enviar por webhook si no hay canal específico mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':chart_with_upwards_trend:') return {'status': 'success', 'message': 'Reporte enviado por webhook'} except Exception as e: logger.error(f"Error generando reporte diario: {str(e)}", exc_info=True) return {'status': 'error', 'message': str(e)} def comando_estadisticas(user_name, mattermost, nocodb): """ Muestra estadísticas personales del vendedor Comando: /stats o /estadisticas """ try: # Estadísticas del día stats_hoy = nocodb.get_estadisticas_vendedor_dia(user_name) # Estadísticas del mes stats_mes = nocodb.get_estadisticas_vendedor_mes(user_name) if not stats_hoy and not stats_mes: mensaje = f"@{user_name} Aún no tienes ventas registradas." return mensaje mensaje = f"📈 **Estadísticas de @{user_name}**\n\n" # Hoy if stats_hoy: mensaje += ( f"**Hoy ({datetime.now(TZ_MEXICO).strftime('%d/%m')})**\n" f"• Tubos: {stats_hoy.get('tubos_vendidos', 0)} 🧪\n" f"• Comisión: {formatear_moneda(stats_hoy.get('comision', 0))}\n" f"• Monto: {formatear_moneda(stats_hoy.get('monto_total_dia', 0))}\n" f"• Ventas: {stats_hoy.get('cantidad_ventas', 0)}\n\n" ) # Mes if stats_mes: mensaje += ( f"**Este mes**\n" f"• Tubos totales: {stats_mes.get('tubos_totales', 0)} 🧪\n" f"• Comisión total: {formatear_moneda(stats_mes.get('comision_total', 0))} 💰\n" f"• Monto total: {formatear_moneda(stats_mes.get('monto_total', 0))}\n" f"• Ventas: {stats_mes.get('cantidad_ventas', 0)}\n" f"• Días activos: {stats_mes.get('dias_activos', 0)}\n" f"• Días con meta: {stats_mes.get('dias_meta_cumplida', 0)}\n" f"• Promedio/día: {stats_mes.get('promedio_tubos_dia', 0):.1f} tubos\n" ) mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':bar_chart:') return mensaje except Exception as e: logger.error(f"Error en comando_estadisticas: {str(e)}", exc_info=True) return "❌ Error obteniendo estadísticas"