first commit

This commit is contained in:
Marco Gallegos
2025-12-13 19:06:14 -06:00
commit 1cb382b4ef
6 changed files with 654 additions and 0 deletions

6
.env Normal file
View File

@@ -0,0 +1,6 @@
# Configuración de Telegram
TELEGRAM_TOKEN=TU_TOKEN_NUEVO_AQUI
# Webhooks de n8n (puedes agregar más aquí en el futuro)
WEBHOOK_CONTRATO=https://flows.soul23.cloud/webhook/DuXh9Oi7SCAMf9
# WEBHOOK_VACACIONES=https://... (futuro)

190
Readme.md Normal file
View File

@@ -0,0 +1,190 @@
# 🤖 Vanessa Bot Asistente de RH para Vanity
Vanessa es un bot de Telegram escrito en Python que automatiza procesos internos de Recursos Humanos en Vanity. Su objetivo es eliminar fricción operativa: onboarding, solicitudes de RH e impresión de documentos, todo orquestado desde Telegram y conectado a flujos de n8n.
Este repositorio está pensado como **proyecto Python profesional**, modular y listo para correr 24/7 en producción.
---
## 🧠 ¿Qué hace Vanessa?
Vanessa no es un chatbot genérico: es una interfaz conversacional para procesos reales de negocio.
- Onboarding completo de nuevas socias (/welcome)
- Envío de archivos a impresión (/print)
- Solicitud de vacaciones (/vacaciones)
- Solicitud de permisos por horas (/permiso)
Cada flujo es un módulo independiente y todos los datos se envían a **webhooks de n8n** para su procesamiento posterior.
---
## 📂 Estructura del Proyecto
```
vanity_bot/
├── .env # Variables sensibles (tokens, URLs)
├── main.py # Cerebro principal del bot
├── requirements.txt # Dependencias
├── README.md # Este documento
└── modules/ # Habilidades del bot
├── __init__.py
├── onboarding.py # Flujo /welcome (onboarding RH)
├── printer.py # Flujo /print (impresión)
└── rh_requests.py # /vacaciones y /permiso
```
---
## 🔐 Configuración (.env)
Crea un archivo `.env` en la raíz del proyecto con el siguiente contenido:
```
# --- TELEGRAM ---
TELEGRAM_TOKEN=TU_TOKEN_AQUI
# --- WEBHOOKS N8N ---
WEBHOOK_ONBOARDING=https://flows.soul23.cloud/webhook/contrato
WEBHOOK_PRINT=https://flows.soul23.cloud/webhook/impresion
WEBHOOK_VACACIONES=https://flows.soul23.cloud/webhook/vacaciones
```
Nunca subas este archivo al repositorio.
---
## 📦 Instalación
Se recomienda usar un entorno virtual.
```
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
---
## ▶️ Ejecución Manual
```
python main.py
```
Si el token es válido, verás:
```
🧠 Vanessa Brain iniciada y escuchando...
```
---
## 🧩 Arquitectura Interna
### main.py (El Cerebro)
- Inicializa el bot de Telegram
- Carga variables de entorno
- Registra los handlers de cada módulo
- Define el menú principal (/start, /help)
Nada de lógica de negocio vive aquí. Solo coordinación.
---
### modules/onboarding.py
Flujo conversacional complejo basado en `ConversationHandler`.
- Recolecta información personal, laboral y de emergencia
- Normaliza datos (RFC, CURP, fechas)
- Usa teclados guiados para reducir errores
- Envía un payload estructurado a n8n
El diseño es **estado → pregunta → respuesta → siguiente estado**.
---
### modules/printer.py
- Recibe documentos o imágenes desde Telegram
- Obtiene el enlace temporal de Telegram
- Envía el archivo a una cola de impresión vía webhook
Telegram se usa como interfaz, n8n como backend operativo.
---
### modules/rh_requests.py
- Maneja solicitudes simples de RH
- Vacaciones
- Permisos por horas
El bot solo valida y recopila; la lógica de aprobación vive fuera.
---
## ⚙️ Ejecución Automática con systemd (Linux)
Ejemplo de servicio:
```
[Unit]
Description=Vanessa Bot
After=network.target
[Service]
User=vanity
WorkingDirectory=/opt/vanity_bot
EnvironmentFile=/opt/vanity_bot/.env
ExecStart=/opt/vanity_bot/venv/bin/python main.py
Restart=always
[Install]
WantedBy=multi-user.target
```
Luego:
```
sudo systemctl daemon-reload
sudo systemctl enable vanessa
sudo systemctl start vanessa
```
---
## 🧠 Filosofía del Proyecto
- Telegram como UI
- Python como cerebro
- n8n como sistema nervioso
- Datos estructurados, no mensajes sueltos
- Modularidad total: cada habilidad se enchufa o se quita
Vanessa no reemplaza RH: elimina fricción humana innecesaria.
---
## 🚀 Extensiones Futuras
- Firma digital de contratos
- Finder de documentos
- Reportes automáticos
- Roles y permisos
- Modo administrador
---
## 🧪 Estado del Proyecto
✔ Funcional en producción
✔ Modular
✔ Escalable
✔ Auditable
Vanessa está viva. Y aprende con cada flujo nuevo.

54
main.py Normal file
View File

@@ -0,0 +1,54 @@
import os
import logging
from dotenv import load_dotenv
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import Application, Defaults, CommandHandler, ContextTypes
# --- IMPORTAR HABILIDADES ---
from modules.onboarding import onboarding_handler
from modules.printer import print_handler
from modules.rh_requests import vacaciones_handler, permiso_handler
# from modules.finder import finder_handler (Si lo creas después)
load_dotenv()
TOKEN = os.getenv("TELEGRAM_TOKEN")
logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO)
async def menu_principal(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Muestra el menú de opciones de Vanessa"""
texto = (
"👩‍💼 **Hola, soy Vanessa. ¿En qué puedo ayudarte hoy?**\n\n"
"📝 `/welcome` - Iniciar onboarding/contrato\n"
"🖨️ `/print` - Imprimir o enviar archivo\n"
"🌴 `/vacaciones` - Solicitar días libres\n"
"⏱️ `/permiso` - Solicitar permiso por horas\n"
"🔍 `/socia_finder` - Buscar datos de una compañera\n\n"
"Selecciona un comando para empezar."
)
await update.message.reply_text(texto)
def main():
# Configuración Global
defaults = Defaults(parse_mode=ParseMode.MARKDOWN)
app = Application.builder().token(TOKEN).defaults(defaults).build()
# --- REGISTRO DE HABILIDADES ---
# 1. Comando de Ayuda / Menú
app.add_handler(CommandHandler("start", menu_principal))
app.add_handler(CommandHandler("help", menu_principal))
# 2. Habilidades Complejas (Conversaciones)
app.add_handler(onboarding_handler)
app.add_handler(print_handler)
app.add_handler(vacaciones_handler)
app.add_handler(permiso_handler)
# app.add_handler(finder_handler)
print("🧠 Vanessa Bot Brain iniciada y lista para trabajar en todos los módulos.")
app.run_polling()
if __name__ == "__main__":
main()

343
modules/onboarding.py Normal file
View File

@@ -0,0 +1,343 @@
import logging
import os
import requests
from datetime import datetime
from functools import partial
from dotenv import load_dotenv # pip install python-dotenv
from telegram import Update, ReplyKeyboardRemove, ReplyKeyboardMarkup
from telegram.constants import ParseMode
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
ConversationHandler,
MessageHandler,
filters,
Defaults,
)
# --- 1. CARGA DE ENTORNO ---
load_dotenv() # Carga las variables del archivo .env
TOKEN = os.getenv("TELEGRAM_TOKEN")
# Convertimos la string del webhook en una lista (por si en el futuro hay varios separados por coma)
WEBHOOK_URLS = os.getenv("WEBHOOK_CONTRATO", "").split(",")
# Validación de seguridad
if not TOKEN:
raise ValueError("⚠️ Error: No se encontró TELEGRAM_TOKEN en el archivo .env")
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
# --- 2. ESTADOS DEL FLUJO ---
(
NOMBRE_SALUDO, NOMBRE_COMPLETO, APELLIDO_PATERNO, APELLIDO_MATERNO,
CUMPLE_DIA, CUMPLE_MES, CUMPLE_ANIO, ESTADO_NACIMIENTO,
RFC, CURP,
CORREO, CELULAR,
CALLE, NUM_EXTERIOR, NUM_INTERIOR, COLONIA, CODIGO_POSTAL, CIUDAD_RESIDENCIA,
ROL, SUCURSAL, INICIO_DIA, INICIO_MES, INICIO_ANIO,
REF1_NOMBRE, REF1_TELEFONO, REF1_TIPO,
REF2_NOMBRE, REF2_TELEFONO, REF2_TIPO,
REF3_NOMBRE, REF3_TELEFONO, REF3_TIPO,
EMERGENCIA_NOMBRE, EMERGENCIA_TEL, EMERGENCIA_RELACION
) = range(35)
# --- 3. HELPER: NORMALIZACIÓN Y MAPEOS ---
def normalizar_id(texto: str) -> str:
"""Elimina espacios y convierte a mayúsculas (para RFC y CURP)."""
if not texto: return "N/A"
# Elimina todos los espacios en blanco y pone mayúsculas
limpio = "".join(texto.split()).upper()
return "N/A" if limpio == "0" else limpio
def limpiar_texto_general(texto: str) -> str:
t = texto.strip()
return "N/A" if t == "0" else t
# --- 4. TECLADOS DINÁMICOS ---
# Meses: Texto vs Valor
MAPA_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"
}
# Generamos el teclado de 3 en 3
TECLADO_MESES = ReplyKeyboardMarkup(
[list(MAPA_MESES.keys())[i:i+3] for i in range(0, 12, 3)],
one_time_keyboard=True, resize_keyboard=True
)
# Años: Actual y Siguiente
anio_actual = datetime.now().year
TECLADO_ANIOS_INICIO = ReplyKeyboardMarkup(
[[str(anio_actual), str(anio_actual + 1)]],
one_time_keyboard=True, resize_keyboard=True
)
# Roles
TECLADO_ROLES = ReplyKeyboardMarkup(
[["Partner", "Manager"], ["Staff", "Tech"], ["Marketing"]],
one_time_keyboard=True, resize_keyboard=True
)
# Sucursales (Mapeo Visual -> ID Técnico)
MAPA_SUCURSALES = {
"Plaza Cima (Sur) ⛰️": "plaza_cima",
"Plaza O (Carranza) 🏙️": "plaza_o"
}
TECLADO_SUCURSALES = ReplyKeyboardMarkup(
[["Plaza Cima (Sur) ⛰️", "Plaza O (Carranza) 🏙️"]],
one_time_keyboard=True, resize_keyboard=True
)
TECLADO_CIUDAD = ReplyKeyboardMarkup(
[["Saltillo", "Ramos Arizpe", "Arteaga"]],
one_time_keyboard=True, resize_keyboard=True
)
TECLADO_REF_TIPO = ReplyKeyboardMarkup(
[["Familiar", "Amistad"], ["Trabajo", "Académica", "Otra"]],
one_time_keyboard=True, resize_keyboard=True
)
TECLADO_RELACION_EMERGENCIA = ReplyKeyboardMarkup(
[["Padre/Madre", "Esposo/a", "Hijo/a"], ["Hermano/a", "Amigo/a", "Otro"]],
one_time_keyboard=True, resize_keyboard=True
)
# --- 5. LOGICA DEL BOT (VANESSA) ---
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
user = update.effective_user
context.user_data.clear()
context.user_data["metadata"] = {
"telegram_id": user.id,
"username": user.username or "N/A",
"first_name": user.first_name,
"start_ts": datetime.now().timestamp()
}
context.user_data["respuestas"] = {}
await update.message.reply_text(
f"¡Hola {user.first_name}! 👋\n\n"
"Soy *Vanessa de Recursos Humanos* de Vanity. 👩‍💼\n"
"Bienvenida al equipo Soul. Vamos a dejar listo tu registro en unos minutos.\n\n"
"💡 _Tip: Si te equivocas, escribe /cancelar y empezamos de nuevo._"
)
await update.message.reply_text("Para empezar con el pie derecho, ¿cómo te gusta que te llamemos?")
return NOMBRE_SALUDO
async def manejar_flujo(update: Update, context: ContextTypes.DEFAULT_TYPE, estado_actual: int) -> int:
texto_recibido = update.message.text
respuesta_procesada = limpiar_texto_general(texto_recibido)
# --- LÓGICA DE PROCESAMIENTO ESPECÍFICA POR ESTADO ---
# 1. Normalización de RFC y CURP (Quitar espacios, Mayúsculas)
if estado_actual in [RFC, CURP]:
respuesta_procesada = normalizar_id(texto_recibido)
# 2. Mapeo de Meses (Texto -> Número)
if estado_actual in [CUMPLE_MES, INICIO_MES]:
# Si el usuario seleccionó un botón, buscamos su valor numérico
respuesta_procesada = MAPA_MESES.get(texto_recibido, texto_recibido) # Fallback al texto si no está en mapa
# 3. Mapeo de Sucursales (Texto Bonito -> ID Técnico)
if estado_actual == SUCURSAL:
respuesta_procesada = MAPA_SUCURSALES.get(texto_recibido, "otra_sucursal")
# Guardar en memoria
context.user_data["respuestas"][estado_actual] = respuesta_procesada
# --- GUIÓN DE ENTREVISTA ---
siguiente_estado = estado_actual + 1
preguntas = {
NOMBRE_SALUDO: "¡Lindo nombre! ✨\n\nNecesito tus datos oficiales para el contrato.\n¿Cuál es tu *nombre completo* (nombres) tal cual aparece en tu INE?",
NOMBRE_COMPLETO: "¿Cuál es tu *apellido paterno*?",
APELLIDO_PATERNO: "¿Y tu *apellido materno*?",
# Cumpleaños
APELLIDO_MATERNO: "🎂 Hablemos de ti. ¿Qué *día* es tu cumpleaños? (Escribe el número, ej: 13)",
CUMPLE_DIA: {"texto": "¿De qué *mes*? 🎉", "teclado": TECLADO_MESES},
CUMPLE_MES: "Entendido. ¿Y de qué *año*? 🗓️",
CUMPLE_ANIO: "🇲🇽 ¿En qué *estado de la república* naciste?",
# Identificación
ESTADO_NACIMIENTO: "Pasemos a lo administrativo 📄.\n\nPor favor escribe tu *RFC* (Sin espacios):",
RFC: "Gracias. Ahora tu *CURP*:",
# Contacto
CURP: "¡Súper! 📧 ¿A qué *correo electrónico* te enviamos la info?",
CORREO: "📱 ¿Cuál es tu número de *celular* personal? (10 dígitos)",
# Domicilio
CELULAR: "🏠 Registremos tu domicilio.\n\n¿En qué *calle* vives?",
CALLE: "#️⃣ ¿Cuál es el *número exterior*?",
NUM_EXTERIOR: "🚪 ¿Tienes *número interior*? (Escribe 0 si no aplica)",
NUM_INTERIOR: "🏘️ ¿Cómo se llama la *colonia*?",
COLONIA: "📮 ¿Cuál es el *Código Postal*?",
CODIGO_POSTAL: {"texto": "¿En qué *ciudad* resides actualmente?", "teclado": TECLADO_CIUDAD},
# Laboral
CIUDAD_RESIDENCIA: {"texto": "¡Excelente! Coahuila es territorio Vanity 🌵.\n\n¿Qué *rol* tendrás en el equipo? 💼", "teclado": TECLADO_ROLES},
ROL: {"texto": "¿A qué *sucursal* te vas a integrar? 📍", "teclado": TECLADO_SUCURSALES},
SUCURSAL: "¡Qué emoción! 🎉\n\n¿Qué *día* está programado tu ingreso? (Solo el número, ej: 01)",
INICIO_DIA: {"texto": "¿De qué *mes* será tu ingreso?", "teclado": TECLADO_MESES},
INICIO_MES: {"texto": "¿Y de qué *año*?", "teclado": TECLADO_ANIOS_INICIO},
# Referencias
INICIO_ANIO: "Ya casi acabamos. Necesito 3 referencias.\n\n👤 *Referencia 1*: Nombre completo",
REF1_NOMBRE: "📞 Teléfono de la Referencia 1:",
REF1_TELEFONO: {"texto": "🧑‍🤝‍🧑 ¿Qué relación tienes con ella/él?", "teclado": TECLADO_REF_TIPO},
REF1_TIPO: "Ok. Vamos con la *Referencia 2*.\n\n👤 Nombre completo:",
REF2_NOMBRE: "📞 Teléfono de la Referencia 2:",
REF2_TELEFONO: {"texto": "🧑‍🤝‍🧑 ¿Qué relación tienen?", "teclado": TECLADO_REF_TIPO},
REF2_TIPO: "Última. *Referencia 3*.\n\n👤 Nombre completo:",
REF3_NOMBRE: "📞 Teléfono de la Referencia 3:",
REF3_TELEFONO: {"texto": "🧑‍🤝‍🧑 ¿Qué relación tienen?", "teclado": TECLADO_REF_TIPO},
# Emergencia
REF3_TIPO: "Finalmente, por seguridad 🚑:\n\n¿A quién llamamos en caso de *emergencia*?",
EMERGENCIA_NOMBRE: "☎️ ¿Cuál es el teléfono de esa persona?",
EMERGENCIA_TEL: {"texto": "¿Qué parentesco tiene contigo?", "teclado": TECLADO_RELACION_EMERGENCIA},
}
siguiente = preguntas.get(estado_actual)
if isinstance(siguiente, dict):
await update.message.reply_text(siguiente["texto"], reply_markup=siguiente["teclado"])
else:
await update.message.reply_text(siguiente, reply_markup=ReplyKeyboardRemove())
return siguiente_estado
async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
# Guardar última respuesta (Relación Emergencia)
context.user_data["respuestas"][EMERGENCIA_RELACION] = limpiar_texto_general(update.message.text)
await update.message.reply_text("¡Perfecto! 📝 Guardando tu expediente en el sistema... dame un momento.")
r = context.user_data["respuestas"]
meta = context.user_data["metadata"]
# Construcción segura de fechas
try:
fecha_nac = f"{r[CUMPLE_ANIO]}-{r[CUMPLE_MES]}-{str(r[CUMPLE_DIA]).zfill(2)}"
fecha_ini = f"{r[INICIO_ANIO]}-{r[INICIO_MES]}-{str(r[INICIO_DIA]).zfill(2)}"
except Exception:
fecha_nac = "ERROR_FECHA"
fecha_ini = "ERROR_FECHA"
# PAYLOAD ESTRUCTURADO PARA N8N
payload = {
"candidato": {
"nombre_preferido": r.get(NOMBRE_SALUDO),
"nombre_oficial": r.get(NOMBRE_COMPLETO),
"apellido_paterno": r.get(APELLIDO_PATERNO),
"apellido_materno": r.get(APELLIDO_MATERNO),
"fecha_nacimiento": fecha_nac,
"rfc": r.get(RFC),
"curp": r.get(CURP),
"lugar_nacimiento": r.get(ESTADO_NACIMIENTO)
},
"contacto": {
"email": r.get(CORREO),
"celular": r.get(CELULAR)
},
"domicilio": {
"calle": r.get(CALLE),
"num_ext": r.get(NUM_EXTERIOR),
"num_int": r.get(NUM_INTERIOR),
"colonia": r.get(COLONIA),
"cp": r.get(CODIGO_POSTAL),
"ciudad": r.get(CIUDAD_RESIDENCIA),
"estado": "Coahuila de Zaragoza"
},
"laboral": {
"rol_id": r.get(ROL).lower(), # partner, manager...
"sucursal_id": r.get(SUCURSAL), # plaza_cima, plaza_o
"fecha_inicio": fecha_ini
},
"referencias": [
{"nombre": r.get(REF1_NOMBRE), "telefono": r.get(REF1_TELEFONO), "relacion": r.get(REF1_TIPO)},
{"nombre": r.get(REF2_NOMBRE), "telefono": r.get(REF2_TELEFONO), "relacion": r.get(REF2_TIPO)},
{"nombre": r.get(REF3_NOMBRE), "telefono": r.get(REF3_TELEFONO), "relacion": r.get(REF3_TIPO)}
],
"emergencia": {
"nombre": r.get(EMERGENCIA_NOMBRE),
"telefono": r.get(EMERGENCIA_TEL),
"relacion": r.get(EMERGENCIA_RELACION)
},
"metadata": {
"telegram_user": meta["username"],
"chat_id": meta["telegram_id"],
"bot_version": "welcome2soul_v2",
"fecha_registro": datetime.now().isoformat()
}
}
headers = {"Content-Type": "application/json", "User-Agent": "Welcome2Soul-Bot"}
enviado = False
for url in WEBHOOK_URLS:
if not url: continue
try:
res = requests.post(url.strip(), json=payload, headers=headers, timeout=20)
res.raise_for_status()
enviado = True
logging.info(f"Webhook enviado exitosamente a: {url}")
except Exception as e:
logging.error(f"Error enviando webhook: {e}")
if enviado:
await update.message.reply_text(
"✅ *¡Registro Exitoso!*\n\n"
"Bienvenida a la familia Soul/Vanity. Tu contrato se está generando y te avisaremos pronto.\n"
"¡Nos vemos el primer día! ✨"
)
else:
await update.message.reply_text("⚠️ Se guardaron tus datos pero hubo un error de conexión. RH lo revisará manualmente.")
context.user_data.clear()
return ConversationHandler.END
async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
await update.message.reply_text(
"Proceso cancelado. ⏸️\nCuando quieras retomar, escribe /contrato.",
reply_markup=ReplyKeyboardRemove()
)
context.user_data.clear()
return ConversationHandler.END
def main():
defaults = Defaults(parse_mode=ParseMode.MARKDOWN)
application = Application.builder().token(TOKEN).defaults(defaults).build()
states = {}
for i in range(34):
callback = partial(manejar_flujo, estado_actual=i)
states[i] = [MessageHandler(filters.TEXT & ~filters.COMMAND, callback)]
states[34] = [MessageHandler(filters.TEXT & ~filters.COMMAND, finalizar)]
conv_handler = ConversationHandler(
entry_points=[CommandHandler("contrato", start)],
states=states,
fallbacks=[CommandHandler("cancelar", cancelar)],
)
application.add_handler(conv_handler)
print("🧠 Welcome2Soul Bot (Vanessa) iniciado...")
application.run_polling()
if __name__ == "__main__":
main()

0
modules/printer.py Normal file
View File

61
modules/rh_requests.py Normal file
View File

@@ -0,0 +1,61 @@
import os
import requests
from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
TIPO_SOLICITUD, FECHAS, MOTIVO = range(3)
async def start_vacaciones(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
context.user_data['tipo'] = 'Vacaciones'
await update.message.reply_text("🌴 **Solicitud de Vacaciones**\n\n¿Para qué fechas las necesitas? (Ej: 10 al 15 de Octubre)")
return FECHAS
async def start_permiso(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
context.user_data['tipo'] = 'Permiso Especial'
await update.message.reply_text("⏱️ **Solicitud de Permiso**\n\n¿Para qué día y horario lo necesitas?")
return FECHAS
async def recibir_fechas(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
context.user_data['fechas'] = update.message.text
await update.message.reply_text("Entendido. ¿Cuál es el motivo o comentario adicional?")
return MOTIVO
async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
motivo = update.message.text
datos = context.user_data
user = update.effective_user
# Payload para n8n
payload = {
"solicitante": user.full_name,
"id_telegram": user.id,
"tipo_solicitud": datos['tipo'],
"fechas": datos['fechas'],
"motivo": motivo
}
webhook = os.getenv("WEBHOOK_VACACIONES")
try:
requests.post(webhook, json=payload)
await update.message.reply_text(f"✅ Solicitud de *{datos['tipo']}* enviada a tu Manager.")
except:
await update.message.reply_text("⚠️ Error enviando la solicitud.")
return ConversationHandler.END
async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
await update.message.reply_text("Solicitud cancelada.")
return ConversationHandler.END
# Handlers separados pero comparten lógica
vacaciones_handler = ConversationHandler(
entry_points=[CommandHandler("vacaciones", start_vacaciones)],
states={FECHAS: [MessageHandler(filters.TEXT, recibir_fechas)], MOTIVO: [MessageHandler(filters.TEXT, recibir_motivo_fin)]},
fallbacks=[CommandHandler("cancelar", cancelar)]
)
permiso_handler = ConversationHandler(
entry_points=[CommandHandler("permiso", start_permiso)],
states={FECHAS: [MessageHandler(filters.TEXT, recibir_fechas)], MOTIVO: [MessageHandler(filters.TEXT, recibir_motivo_fin)]},
fallbacks=[CommandHandler("cancelar", cancelar)]
)