mirror of
https://github.com/marcogll/telegram_new_socias.git
synced 2026-01-13 21:25:16 +00:00
feat: Implement multi-database architecture
This commit introduces a three-database architecture to the application, as specified in the `db_logic.md` file. The changes include: - A SQL initialization script (`db/init/init.sql`) to create the `USERS_ALMA`, `vanity_hr`, and `vanity_attendance` databases and their respective tables. - SQLAlchemy models for all tables, organized into separate files within the `models` directory. - Refactoring of the database connection logic in `modules/database.py` to support connections to all three databases. - Creation of a `modules/logger.py` to handle request logging to the `USERS_ALMA` database. - Updates to `docker-compose.yml` to mount the initialization script and build the bot image locally. - Updates to `.env.example` to include the new database environment variables. - Restoration of the data dictionary to `db_tasks.md`.
This commit is contained in:
@@ -32,7 +32,9 @@ WEBHOOK_PERMISOS=url
|
|||||||
|
|
||||||
# --- DATABASE ---
|
# --- DATABASE ---
|
||||||
# Usado por el servicio de la base de datos en docker-compose.yml
|
# Usado por el servicio de la base de datos en docker-compose.yml
|
||||||
MYSQL_DATABASE=vanessa_logs
|
MYSQL_DATABASE_USERS_ALMA=USERS_ALMA
|
||||||
|
MYSQL_DATABASE_VANITY_HR=vanity_hr
|
||||||
|
MYSQL_DATABASE_VANITY_ATTENDANCE=vanity_attendance
|
||||||
MYSQL_USER=user
|
MYSQL_USER=user
|
||||||
MYSQL_PASSWORD=password
|
MYSQL_PASSWORD=password
|
||||||
MYSQL_ROOT_PASSWORD=rootpassword
|
MYSQL_ROOT_PASSWORD=rootpassword
|
||||||
|
|||||||
136
db/init/init.sql
Normal file
136
db/init/init.sql
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
CREATE DATABASE IF NOT EXISTS USERS_ALMA;
|
||||||
|
CREATE DATABASE IF NOT EXISTS vanity_hr;
|
||||||
|
CREATE DATABASE IF NOT EXISTS vanity_attendance;
|
||||||
|
|
||||||
|
GRANT ALL PRIVILEGES ON USERS_ALMA.* TO 'user'@'%';
|
||||||
|
GRANT ALL PRIVILEGES ON vanity_hr.* TO 'user'@'%';
|
||||||
|
GRANT ALL PRIVILEGES ON vanity_attendance.* TO 'user'@'%';
|
||||||
|
|
||||||
|
USE USERS_ALMA;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE,
|
||||||
|
role ENUM('admin', 'manager', 'user'),
|
||||||
|
first_name VARCHAR(100),
|
||||||
|
last_name VARCHAR(100),
|
||||||
|
email VARCHAR(100) UNIQUE,
|
||||||
|
cell_phone VARCHAR(20),
|
||||||
|
telegram_id VARCHAR(50) UNIQUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS request_logs (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
telegram_id VARCHAR(50),
|
||||||
|
username VARCHAR(100),
|
||||||
|
command VARCHAR(100),
|
||||||
|
message VARCHAR(500),
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
USE vanity_hr;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS data_empleadas (
|
||||||
|
numero_empleado VARCHAR(15) PRIMARY KEY,
|
||||||
|
puesto VARCHAR(50),
|
||||||
|
sucursal VARCHAR(50),
|
||||||
|
fecha_ingreso DATE,
|
||||||
|
estatus VARCHAR(15),
|
||||||
|
nombre_completo VARCHAR(150),
|
||||||
|
nombre VARCHAR(50),
|
||||||
|
nombre_preferido VARCHAR(50),
|
||||||
|
apellido_paterno VARCHAR(50),
|
||||||
|
apellido_materno VARCHAR(50),
|
||||||
|
fecha_nacimiento DATE,
|
||||||
|
lugar_nacimiento VARCHAR(50),
|
||||||
|
rfc VARCHAR(13) UNIQUE,
|
||||||
|
curp VARCHAR(18) UNIQUE,
|
||||||
|
email VARCHAR(100),
|
||||||
|
telefono_celular VARCHAR(15),
|
||||||
|
domicilio_calle VARCHAR(255),
|
||||||
|
domicilio_numero_exterior VARCHAR(10),
|
||||||
|
domicilio_numero_interior VARCHAR(10),
|
||||||
|
domicilio_numero_texto VARCHAR(50),
|
||||||
|
domicilio_colonia VARCHAR(255),
|
||||||
|
domicilio_codigo_postal VARCHAR(10),
|
||||||
|
domicilio_ciudad VARCHAR(100),
|
||||||
|
domicilio_estado VARCHAR(50),
|
||||||
|
domicilio_completo VARCHAR(255),
|
||||||
|
emergencia_nombre VARCHAR(100),
|
||||||
|
emergencia_telefono VARCHAR(15),
|
||||||
|
emergencia_parentesco VARCHAR(50),
|
||||||
|
referencia_1_nombre VARCHAR(100),
|
||||||
|
referencia_1_telefono VARCHAR(15),
|
||||||
|
referencia_1_tipo VARCHAR(20),
|
||||||
|
referencia_2_nombre VARCHAR(100),
|
||||||
|
referencia_2_telefono VARCHAR(15),
|
||||||
|
referencia_2_tipo VARCHAR(20),
|
||||||
|
referencia_3_nombre VARCHAR(100),
|
||||||
|
referencia_3_telefono VARCHAR(15),
|
||||||
|
referencia_3_tipo VARCHAR(20),
|
||||||
|
origen_registro VARCHAR(50),
|
||||||
|
telegram_usuario VARCHAR(50),
|
||||||
|
telegram_chat_id BIGINT,
|
||||||
|
bot_version VARCHAR(20),
|
||||||
|
fecha_registro DATETIME,
|
||||||
|
tiempo_registro_minutos INT,
|
||||||
|
fecha_procesamiento DATETIME(3)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS vacaciones (
|
||||||
|
vacaciones_id VARCHAR(50) PRIMARY KEY,
|
||||||
|
numero_empleado VARCHAR(15),
|
||||||
|
tipo_solicitud VARCHAR(20),
|
||||||
|
estatus ENUM('pendiente', 'aprobado', 'rechazado', 'cancelado'),
|
||||||
|
fecha_inicio DATE,
|
||||||
|
fecha_fin DATE,
|
||||||
|
dias_solicitados INT,
|
||||||
|
dias_habiles INT,
|
||||||
|
motivo TEXT,
|
||||||
|
con_goce_sueldo TINYINT(1),
|
||||||
|
fecha_solicitud DATETIME,
|
||||||
|
fecha_procesamiento DATETIME(3),
|
||||||
|
origen VARCHAR(20),
|
||||||
|
afecta_nomina TINYINT(1),
|
||||||
|
FOREIGN KEY (numero_empleado) REFERENCES data_empleadas(numero_empleado)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS permisos (
|
||||||
|
permiso_id VARCHAR(50) PRIMARY KEY,
|
||||||
|
numero_empleado VARCHAR(15),
|
||||||
|
categoria ENUM('PERSONAL', 'MEDICO', 'OFICIAL', 'OTRO'),
|
||||||
|
estatus ENUM('pendiente', 'aprobado', 'rechazado', 'cancelado'),
|
||||||
|
fecha_inicio DATE,
|
||||||
|
horario_especifico VARCHAR(50),
|
||||||
|
motivo TEXT,
|
||||||
|
con_goce_sueldo TINYINT(1),
|
||||||
|
afecta_nomina TINYINT(1),
|
||||||
|
FOREIGN KEY (numero_empleado) REFERENCES data_empleadas(numero_empleado)
|
||||||
|
);
|
||||||
|
|
||||||
|
USE vanity_attendance;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS asistencia_registros (
|
||||||
|
id_asistencia INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
numero_empleado VARCHAR(15),
|
||||||
|
fecha DATE,
|
||||||
|
hora_entrada_real TIME,
|
||||||
|
hora_salida_real TIME,
|
||||||
|
minutos_retraso INT,
|
||||||
|
minutos_extra INT,
|
||||||
|
sucursal_registro VARCHAR(50),
|
||||||
|
telegram_id_usado BIGINT,
|
||||||
|
FOREIGN KEY (numero_empleado) REFERENCES vanity_hr.data_empleadas(numero_empleado)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS horario_empleadas (
|
||||||
|
id_horario INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
numero_empleado VARCHAR(15),
|
||||||
|
telegram_id BIGINT,
|
||||||
|
dia_semana VARCHAR(20),
|
||||||
|
hora_entrada_teorica TIME,
|
||||||
|
hora_salida_teorica TIME,
|
||||||
|
FOREIGN KEY (numero_empleado) REFERENCES vanity_hr.data_empleadas(numero_empleado)
|
||||||
|
);
|
||||||
286
db_tasks.md
286
db_tasks.md
@@ -1,101 +1,219 @@
|
|||||||
# Definición de Tablas y Campos para la Base de Datos
|
# Sistema Integrado de Gestión (Vanity)
|
||||||
|
|
||||||
Este documento describe la estructura de la base de datos para el bot Vanessa, diseñada para gestionar la información de empleadas, solicitudes y permisos de forma detallada.
|
Este repositorio documenta la **especificación técnica completa** del ecosistema Vanity: infraestructura de datos, diccionarios de campos sin truncar, relaciones entre entidades y reglas de negocio que gobiernan los Bots, Recursos Humanos y el sistema de Asistencia.
|
||||||
|
|
||||||
## 1. Tabla de Usuarias (`users`)
|
El objetivo es servir como **fuente de verdad técnica** para desarrollo, mantenimiento, auditoría y escalamiento.
|
||||||
|
|
||||||
Almacena la información central, el estado y el balance de vacaciones de cada empleada.
|
---
|
||||||
|
|
||||||
| Campo | Tipo | Descripción |
|
## 1. Arquitectura de Datos
|
||||||
|--------------------------|-------------------------------|--------------------------------------------------------------------------|
|
|
||||||
| `id` | `INT` (PK) | Identificador único de la usuaria. |
|
|
||||||
| `employee_number` | `VARCHAR(50)` (Unique) | Número de empleada único. |
|
|
||||||
| `telegram_id` | `BIGINT` (Unique) | ID de Telegram de la usuaria. |
|
|
||||||
| `telegram_username` | `VARCHAR(255)` | Nombre de usuario de Telegram. |
|
|
||||||
| `full_name` | `VARCHAR(255)` | Nombre completo. |
|
|
||||||
| `preferred_name` | `VARCHAR(100)` | Nombre preferido de la empleada. |
|
|
||||||
| `email` | `VARCHAR(255)` (Unique) | Correo electrónico. |
|
|
||||||
| `phone_number` | `VARCHAR(20)` | Teléfono celular. |
|
|
||||||
| `position` | `VARCHAR(100)` | Puesto que desempeña. |
|
|
||||||
| `branch` | `VARCHAR(100)` | Sucursal a la que pertenece. |
|
|
||||||
| `hire_date` | `DATE` | Fecha de ingreso a la empresa. |
|
|
||||||
| `status` | `ENUM('activo', 'inactivo')` | Estatus actual de la empleada. |
|
|
||||||
| `role` | `ENUM('user', 'manager', 'admin')` | Nivel de permisos en el sistema (por defecto: 'user'). |
|
|
||||||
| `vacation_days_assigned` | `INT` | Días de vacaciones asignados para el periodo actual. |
|
|
||||||
| `vacation_days_taken` | `INT` (Default: 0) | Suma de días de vacaciones aprobados y tomados. |
|
|
||||||
| `created_at` | `DATETIME` | Fecha de creación del registro. |
|
|
||||||
| `updated_at` | `DATETIME` | Fecha de última actualización. |
|
|
||||||
|
|
||||||
## 2. Tabla de Solicitudes de Vacaciones (`vacations`)
|
El sistema se distribuye en **tres bases de datos** dentro del mismo servidor, permitiendo integridad referencial y consultas cruzadas controladas:
|
||||||
|
|
||||||
Registro detallado de las solicitudes de vacaciones.
|
* **USERS_ALMA** → Seguridad, autenticación y control de acceso.
|
||||||
|
* **vanity_hr** → Gestión de personal, vacaciones, permisos y reglas laborales.
|
||||||
|
* **vanity_attendance** → Control de asistencia y programación de horarios.
|
||||||
|
|
||||||
| Campo | Tipo | Descripción |
|
---
|
||||||
|---------------------|------------------------------------|--------------------------------------------------------------------------|
|
|
||||||
| `id` | `INT` (PK) | Identificador numérico de la solicitud. |
|
|
||||||
| `request_id` | `VARCHAR(50)` (Unique) | Identificador alfanumérico único de la solicitud (e.g., "7c32a085..."). |
|
|
||||||
| `user_id` | `INT` (FK) | Empleada que solicita (`users.id`). |
|
|
||||||
| `status` | `ENUM('pendiente', 'aprobado', 'rechazado')` | Estado actual de la solicitud. |
|
|
||||||
| `start_date` | `DATE` | Fecha de inicio de las vacaciones. |
|
|
||||||
| `end_date` | `DATE` | Fecha de fin de las vacaciones. |
|
|
||||||
| `requested_days` | `INT` | Número de días naturales solicitados. |
|
|
||||||
| `business_days` | `INT` | Número de días hábiles que abarca la solicitud. |
|
|
||||||
| `reason` | `TEXT` | Motivo de la solicitud. |
|
|
||||||
| `with_pay` | `BOOLEAN` | `TRUE` si es con goce de sueldo. |
|
|
||||||
| `leave_type` | `ENUM('con_goce', 'sin_goce')` | Clasificación del tipo de permiso. |
|
|
||||||
| `request_date` | `DATETIME` | Fecha y hora en que se creó la solicitud. |
|
|
||||||
| `processed_date` | `DATETIME` | Fecha y hora en que se procesó en el sistema (e.g., envío a webhook). |
|
|
||||||
| `source` | `VARCHAR(50)` | Origen de la solicitud (e.g., "telegram_bot"). |
|
|
||||||
| `approver_id` | `INT` (FK, nullable) | Usuario (`users.id`) que aprobó o rechazó. |
|
|
||||||
| `approval_date` | `DATETIME` (nullable) | Fecha y hora de la aprobación o rechazo. |
|
|
||||||
| `approver_comments` | `TEXT` (nullable) | Comentarios del aprobador. |
|
|
||||||
| `affects_payroll` | `BOOLEAN` | `TRUE` si la solicitud tiene implicaciones en la nómina. |
|
|
||||||
|
|
||||||
## 3. Tabla de Solicitudes de Permisos por Horas (`permission_requests`)
|
## 2. Diccionario de Datos
|
||||||
|
|
||||||
Registro detallado de permisos especiales por horas.
|
### 2.1 Base de Datos: `vanity_hr`
|
||||||
|
|
||||||
| Campo | Tipo | Descripción |
|
#### Tabla: `data_empleadas` (Maestra — 44 campos)
|
||||||
|---------------------|------------------------------------|--------------------------------------------------------------------------|
|
|
||||||
| `id` | `INT` (PK) | Identificador numérico del permiso. |
|
|
||||||
| `request_id` | `VARCHAR(50)` (Unique) | Identificador alfanumérico único (e.g., "1LSRADeDNfY"). |
|
|
||||||
| `user_id` | `INT` (FK) | Empleada que solicita (`users.id`). |
|
|
||||||
| `category` | `VARCHAR(100)` | Categoría del permiso (e.g., "PERSONAL", "MÉDICO"). |
|
|
||||||
| `status` | `ENUM('pendiente', 'aprobado', 'rechazado')` | Estado actual del permiso. |
|
|
||||||
| `permission_date` | `DATE` | Fecha para la cual se solicita el permiso. |
|
|
||||||
| `start_time` | `TIME` | Hora de inicio del permiso. |
|
|
||||||
| `end_time` | `TIME` | Hora de fin del permiso. |
|
|
||||||
| `reason` | `TEXT` | Motivo detallado del permiso. |
|
|
||||||
| `with_pay` | `BOOLEAN` | `TRUE` si es con goce de sueldo. |
|
|
||||||
| `leave_type` | `ENUM('con_goce', 'sin_goce')` | Clasificación del tipo de permiso. |
|
|
||||||
| `request_date` | `DATETIME` | Fecha y hora en que se creó la solicitud. |
|
|
||||||
| `processed_date` | `DATETIME` | Fecha y hora en que se procesó. |
|
|
||||||
| `source` | `VARCHAR(50)` | Origen de la solicitud. |
|
|
||||||
| `approver_id` | `INT` (FK, nullable) | Usuario (`users.id`) que gestionó el permiso. |
|
|
||||||
| `approval_date` | `DATETIME` (nullable) | Fecha y hora de la gestión. |
|
|
||||||
| `approver_comments` | `TEXT` (nullable) | Comentarios del aprobador. |
|
|
||||||
| `affects_payroll` | `BOOLEAN` | `TRUE` si el permiso tiene implicaciones en la nómina. |
|
|
||||||
|
|
||||||
## 4. Interacción con la Base de Datos y Lógica de Negocio
|
Tabla central de Recursos Humanos. Contiene información contractual, personal, de contacto y metadatos de registro.
|
||||||
|
|
||||||
### a. Sistema de Roles
|
| Campo | Tipo | Key | Descripción |
|
||||||
|
| ------------------------- | ------------ | --- | ---------------------------------- |
|
||||||
|
| numero_empleado | varchar(15) | PRI | ID único de nómina |
|
||||||
|
| puesto | varchar(50) | | Cargo / Puesto |
|
||||||
|
| sucursal | varchar(50) | | Sucursal asignada |
|
||||||
|
| fecha_ingreso | date | | Fecha de alta (base de antigüedad) |
|
||||||
|
| estatus | varchar(15) | | Activo / Baja |
|
||||||
|
| nombre_completo | varchar(150) | | Nombre completo concatenado |
|
||||||
|
| nombre | varchar(50) | | Nombre(s) |
|
||||||
|
| nombre_preferido | varchar(50) | | Apodo o nombre de preferencia |
|
||||||
|
| apellido_paterno | varchar(50) | | Primer apellido |
|
||||||
|
| apellido_materno | varchar(50) | | Segundo apellido |
|
||||||
|
| fecha_nacimiento | date | | Fecha de nacimiento |
|
||||||
|
| lugar_nacimiento | varchar(50) | | Ciudad / Estado |
|
||||||
|
| rfc | varchar(13) | UNI | RFC |
|
||||||
|
| curp | varchar(18) | UNI | CURP |
|
||||||
|
| email | varchar(100) | | Correo electrónico |
|
||||||
|
| telefono_celular | varchar(15) | | Teléfono móvil |
|
||||||
|
| domicilio_calle | varchar(255) | | Calle |
|
||||||
|
| domicilio_numero_exterior | varchar(10) | | Número exterior |
|
||||||
|
| domicilio_numero_interior | varchar(10) | | Número interior |
|
||||||
|
| domicilio_numero_texto | varchar(50) | | Referencias |
|
||||||
|
| domicilio_colonia | varchar(255) | | Colonia |
|
||||||
|
| domicilio_codigo_postal | varchar(10) | | CP |
|
||||||
|
| domicilio_ciudad | varchar(100) | | Ciudad |
|
||||||
|
| domicilio_estado | varchar(50) | | Estado |
|
||||||
|
| domicilio_completo | varchar(255) | | Dirección formateada |
|
||||||
|
| emergencia_nombre | varchar(100) | | Contacto de emergencia |
|
||||||
|
| emergencia_telefono | varchar(15) | | Teléfono de emergencia |
|
||||||
|
| emergencia_parentesco | varchar(50) | | Parentesco |
|
||||||
|
| referencia_1_nombre | varchar(100) | | Referencia 1 |
|
||||||
|
| referencia_1_telefono | varchar(15) | | Teléfono ref 1 |
|
||||||
|
| referencia_1_tipo | varchar(20) | | Tipo ref 1 |
|
||||||
|
| referencia_2_nombre | varchar(100) | | Referencia 2 |
|
||||||
|
| referencia_2_telefono | varchar(15) | | Teléfono ref 2 |
|
||||||
|
| referencia_2_tipo | varchar(20) | | Tipo ref 2 |
|
||||||
|
| referencia_3_nombre | varchar(100) | | Referencia 3 |
|
||||||
|
| referencia_3_telefono | varchar(15) | | Teléfono ref 3 |
|
||||||
|
| referencia_3_tipo | varchar(20) | | Tipo ref 3 |
|
||||||
|
| origen_registro | varchar(50) | | Web / Bot |
|
||||||
|
| telegram_usuario | varchar(50) | | Username Telegram |
|
||||||
|
| telegram_chat_id | bigint | | ID de chat Telegram |
|
||||||
|
| bot_version | varchar(20) | | Versión del bot |
|
||||||
|
| fecha_registro | datetime | | Timestamp creación |
|
||||||
|
| tiempo_registro_minutos | int | | Duración del registro |
|
||||||
|
| fecha_procesamiento | datetime(3) | | Timestamp procesado |
|
||||||
|
|
||||||
- **`user`**: Rol estándar para todas las empleadas. Pueden solicitar vacaciones y permisos, y consultar su propio estado.
|
---
|
||||||
- **`manager`**: Puede realizar las mismas acciones que un `user`, y adicionalmente, aprobar/rechazar solicitudes y consultar datos de las usuarias a su cargo.
|
|
||||||
- **`admin`**: Acceso total. Puede gestionar todos los datos de todas las usuarias y solicitudes.
|
|
||||||
|
|
||||||
### b. Flujo de Solicitudes (Vacaciones y Permisos)
|
#### Tabla: `vacaciones` (14 campos)
|
||||||
|
|
||||||
1. **Creación**: Una usuaria crea una solicitud. El sistema la inserta en la tabla `vacations` o `permission_requests` con estado `'pendiente'`.
|
| Campo | Tipo | Key | Descripción |
|
||||||
2. **Aprobación**: Un `manager` o `admin` revisa la solicitud. Al aprobarla:
|
| ------------------- | ----------- | --- | -------------------------------------------- |
|
||||||
* El `status` de la solicitud cambia a `'aprobado'`.
|
| vacaciones_id | varchar(50) | PRI | ID de solicitud |
|
||||||
* Se registran el `approver_id`, `approval_date` y `approver_comments`.
|
| numero_empleado | varchar(15) | MUL | Relación con empleada |
|
||||||
3. **Actualización de Balance de Vacaciones**:
|
| tipo_solicitud | varchar(20) | | VACACIONES |
|
||||||
* **Si la solicitud es de vacaciones y fue aprobada**: El sistema debe sumar los `business_days` de la solicitud al campo `vacation_days_taken` de la usuaria en la tabla `users`.
|
| estatus | enum | | pendiente / aprobado / rechazado / cancelado |
|
||||||
* Esta lógica asegura que el balance de días tomados siempre esté sincronizado con los registros aprobados.
|
| fecha_inicio | date | | Inicio |
|
||||||
|
| fecha_fin | date | | Fin |
|
||||||
|
| dias_solicitados | int | | Total días |
|
||||||
|
| dias_habiles | int | | Días descontados |
|
||||||
|
| motivo | text | | Observaciones |
|
||||||
|
| con_goce_sueldo | tinyint(1) | | 1 = Sí |
|
||||||
|
| fecha_solicitud | datetime | | Creación |
|
||||||
|
| fecha_procesamiento | datetime(3) | | Cambio de estatus |
|
||||||
|
| origen | varchar(20) | | telegram_bot / web |
|
||||||
|
| afecta_nomina | tinyint(1) | | Impacto en pago |
|
||||||
|
|
||||||
### c. Lógica de Negocio Clave
|
---
|
||||||
|
|
||||||
|
#### Tabla: `permisos` (9 campos)
|
||||||
|
|
||||||
|
| Campo | Tipo | Key | Descripción |
|
||||||
|
| ------------------ | ----------- | --- | -------------------------------------------- |
|
||||||
|
| permiso_id | varchar(50) | PRI | ID de permiso |
|
||||||
|
| numero_empleado | varchar(15) | MUL | Relación RH |
|
||||||
|
| categoria | enum | | PERSONAL / MEDICO / OFICIAL / OTRO |
|
||||||
|
| estatus | enum | | pendiente / aprobado / rechazado / cancelado |
|
||||||
|
| fecha_inicio | date | | Fecha |
|
||||||
|
| horario_especifico | varchar(50) | | Rango horario |
|
||||||
|
| motivo | text | | Razón |
|
||||||
|
| con_goce_sueldo | tinyint(1) | | 0 / 1 |
|
||||||
|
| afecta_nomina | tinyint(1) | | Impacto |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Base de Datos: `vanity_attendance`
|
||||||
|
|
||||||
|
#### Tabla: `asistencia_registros` (9 campos)
|
||||||
|
|
||||||
|
| Campo | Tipo | Key | Descripción |
|
||||||
|
| ----------------- | ----------- | --- | -------------- |
|
||||||
|
| id_asistencia | int | PRI | Auto-increment |
|
||||||
|
| numero_empleado | varchar(15) | MUL | Relación RH |
|
||||||
|
| fecha | date | | Día |
|
||||||
|
| hora_entrada_real | time | | Entrada |
|
||||||
|
| hora_salida_real | time | | Salida |
|
||||||
|
| minutos_retraso | int | | Calculado |
|
||||||
|
| minutos_extra | int | | Excedente |
|
||||||
|
| sucursal_registro | varchar(50) | | Sucursal |
|
||||||
|
| telegram_id_usado | bigint | | ID Telegram |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Tabla: `horario_empleadas` (Diccionario de turnos)
|
||||||
|
|
||||||
|
| Campo | Tipo | Key | Descripción |
|
||||||
|
| -------------------- | ----------- | --- | ---------------- |
|
||||||
|
| id_horario | int | PRI | ID |
|
||||||
|
| numero_empleado | varchar(15) | MUL | Relación RH |
|
||||||
|
| telegram_id | bigint | | Llave webhook |
|
||||||
|
| dia_semana | varchar(20) | | monday, tuesday… |
|
||||||
|
| hora_entrada_teorica | time | | Entrada |
|
||||||
|
| hora_salida_teorica | time | | Salida |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 Base de Datos: `USERS_ALMA`
|
||||||
|
|
||||||
|
#### Tabla: `users` (10 campos)
|
||||||
|
|
||||||
|
| Campo | Tipo | Key | Descripción |
|
||||||
|
| ----------- | ------------ | --- | ---------------------- |
|
||||||
|
| id | int | PRI | ID interno |
|
||||||
|
| username | varchar(50) | UNI | Usuario |
|
||||||
|
| role | enum | | admin / manager / user |
|
||||||
|
| first_name | varchar(100) | | Nombre |
|
||||||
|
| last_name | varchar(100) | | Apellidos |
|
||||||
|
| email | varchar(100) | UNI | Correo |
|
||||||
|
| cell_phone | varchar(20) | | Teléfono |
|
||||||
|
| telegram_id | varchar(50) | UNI | Auth bot |
|
||||||
|
| created_at | timestamp | | Creación |
|
||||||
|
| updated_at | timestamp | | Actualización |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Reglas de Negocio
|
||||||
|
|
||||||
|
### 3.1 Vacaciones
|
||||||
|
|
||||||
|
* **Antigüedad**: `FLOOR(DATEDIFF(fecha_inicio, fecha_ingreso) / 365.25)`
|
||||||
|
* **Ventana**: mínimo 12 días, máximo 45 días de anticipación
|
||||||
|
* **Validación**: no se permite solicitar periodos no cumplidos
|
||||||
|
|
||||||
|
### 3.2 Asistencia
|
||||||
|
|
||||||
|
* Identificación por `telegram_id`
|
||||||
|
* Cruce con `horario_empleadas` según día
|
||||||
|
* Cálculo de retraso contra horario teórico
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Integración Webhook (Horarios)
|
||||||
|
|
||||||
|
* **Identificación**: `body.telegram.user_id`
|
||||||
|
* **Operación**: Upsert por día
|
||||||
|
* **Formato**: conversión de `10:00 AM` → `10:00:00`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Consultas Operativas
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Saldo actual de vacaciones
|
||||||
|
SELECT * FROM vanity_hr.vista_saldos_vacaciones
|
||||||
|
WHERE numero_empleado = ?;
|
||||||
|
|
||||||
|
-- Última solicitud
|
||||||
|
SELECT * FROM vanity_hr.vacaciones
|
||||||
|
WHERE numero_empleado = ?
|
||||||
|
ORDER BY fecha_solicitud DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- Horario del día
|
||||||
|
SELECT hora_entrada_teorica
|
||||||
|
FROM vanity_attendance.horario_empleadas
|
||||||
|
WHERE telegram_id = ? AND dia_semana = 'monday';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Este documento define el **contrato técnico** del sistema Vanity. Cualquier cambio estructural debe reflejarse aquí antes de pasar a producción.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# DB Implementation Tasks
|
||||||
|
|
||||||
|
1. **Create initialization script:** Write a SQL script to create the databases and tables.
|
||||||
|
2. **Modify `docker-compose.yml`:** Mount the initialization script.
|
||||||
|
3. **Update `.env.example`:** Add new environment variables.
|
||||||
|
4. **Implement SQLAlchemy models:** Create Python classes for each table.
|
||||||
|
5. **Refactor database logic:** Use the new models in the application.
|
||||||
|
|
||||||
- **Balance de Vacaciones**: El balance de días disponibles de una empleada se calcula en tiempo real como `vacation_days_assigned` - `vacation_days_taken`.
|
|
||||||
- **Nómina**: El campo `affects_payroll` sirve como una bandera para que los sistemas externos de RRHH sepan que una solicitud específica (e.g., un permiso sin goce de sueldo) requiere un ajuste en la nómina.
|
|
||||||
- **Auditoría**: Todas las fechas (`request_date`, `approval_date`) y IDs (`user_id`, `approver_id`) permiten una auditoría completa de cada solicitud.
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
bot:
|
bot:
|
||||||
image: marcogll/vanessa-bot:1.2
|
build: .
|
||||||
container_name: vanessa_bot
|
container_name: vanessa_bot
|
||||||
restart: always
|
restart: always
|
||||||
env_file:
|
env_file:
|
||||||
@@ -11,7 +11,6 @@ services:
|
|||||||
- MYSQL_HOST=db
|
- MYSQL_HOST=db
|
||||||
- MYSQL_USER=${MYSQL_USER}
|
- MYSQL_USER=${MYSQL_USER}
|
||||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
||||||
- MYSQL_DATABASE=${MYSQL_DATABASE}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -23,7 +22,6 @@ services:
|
|||||||
container_name: vanessa_db
|
container_name: vanessa_db
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
|
||||||
MYSQL_USER: ${MYSQL_USER}
|
MYSQL_USER: ${MYSQL_USER}
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||||
@@ -34,6 +32,7 @@ services:
|
|||||||
retries: 6
|
retries: 6
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./db/init:/docker-entrypoint-initdb.d
|
||||||
- mysql_data:/var/lib/mysql
|
- mysql_data:/var/lib/mysql
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
|
|||||||
3
main.py
3
main.py
@@ -18,7 +18,8 @@ from telegram.ext import Application, Defaults, CommandHandler, ContextTypes
|
|||||||
# --- IMPORTAR HABILIDADES ---
|
# --- IMPORTAR HABILIDADES ---
|
||||||
from modules.onboarding import onboarding_handler
|
from modules.onboarding import onboarding_handler
|
||||||
from modules.rh_requests import vacaciones_handler, permiso_handler
|
from modules.rh_requests import vacaciones_handler, permiso_handler
|
||||||
from modules.database import log_request, chat_id_exists # Importar chat_id_exists
|
from modules.logger import log_request
|
||||||
|
from modules.database import chat_id_exists # Importar chat_id_exists
|
||||||
from modules.ui import main_actions_keyboard
|
from modules.ui import main_actions_keyboard
|
||||||
# from modules.finder import finder_handler (Si lo creas después)
|
# from modules.finder import finder_handler (Si lo creas después)
|
||||||
|
|
||||||
|
|||||||
31
models/users_alma_models.py
Normal file
31
models/users_alma_models.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Enum, TIMESTAMP
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class RequestLog(Base):
|
||||||
|
__tablename__ = 'request_logs'
|
||||||
|
__table_args__ = {'schema': 'USERS_ALMA'}
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
telegram_id = Column(String(50))
|
||||||
|
username = Column(String(100))
|
||||||
|
command = Column(String(100))
|
||||||
|
message = Column(String(500))
|
||||||
|
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
__table_args__ = {'schema': 'USERS_ALMA'}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
username = Column(String(50), unique=True)
|
||||||
|
role = Column(Enum('admin', 'manager', 'user'))
|
||||||
|
first_name = Column(String(100))
|
||||||
|
last_name = Column(String(100))
|
||||||
|
email = Column(String(100), unique=True)
|
||||||
|
cell_phone = Column(String(20))
|
||||||
|
telegram_id = Column(String(50), unique=True)
|
||||||
|
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||||
|
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
||||||
32
models/vanity_attendance_models.py
Normal file
32
models/vanity_attendance_models.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Time, BigInteger, ForeignKey
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class AsistenciaRegistros(Base):
|
||||||
|
__tablename__ = 'asistencia_registros'
|
||||||
|
__table_args__ = {'schema': 'vanity_attendance'}
|
||||||
|
|
||||||
|
id_asistencia = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
numero_empleado = Column(String(15), ForeignKey('vanity_hr.data_empleadas.numero_empleado'))
|
||||||
|
fecha = Column(Date)
|
||||||
|
hora_entrada_real = Column(Time)
|
||||||
|
hora_salida_real = Column(Time)
|
||||||
|
minutos_retraso = Column(Integer)
|
||||||
|
minutos_extra = Column(Integer)
|
||||||
|
sucursal_registro = Column(String(50))
|
||||||
|
telegram_id_usado = Column(BigInteger)
|
||||||
|
empleada = relationship("DataEmpleadas", backref="asistencia_registros")
|
||||||
|
|
||||||
|
class HorarioEmpleadas(Base):
|
||||||
|
__tablename__ = 'horario_empleadas'
|
||||||
|
__table_args__ = {'schema': 'vanity_attendance'}
|
||||||
|
|
||||||
|
id_horario = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
numero_empleado = Column(String(15), ForeignKey('vanity_hr.data_empleadas.numero_empleado'))
|
||||||
|
telegram_id = Column(BigInteger)
|
||||||
|
dia_semana = Column(String(20))
|
||||||
|
hora_entrada_teorica = Column(Time)
|
||||||
|
hora_salida_teorica = Column(Time)
|
||||||
|
empleada = relationship("DataEmpleadas", backref="horario_empleadas")
|
||||||
92
models/vanity_hr_models.py
Normal file
92
models/vanity_hr_models.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Enum, TIMESTAMP, Date, Text, BigInteger, DateTime
|
||||||
|
from sqlalchemy.dialects.mysql import TINYINT
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from sqlalchemy import ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class DataEmpleadas(Base):
|
||||||
|
__tablename__ = 'data_empleadas'
|
||||||
|
__table_args__ = {'schema': 'vanity_hr'}
|
||||||
|
|
||||||
|
numero_empleado = Column(String(15), primary_key=True)
|
||||||
|
puesto = Column(String(50))
|
||||||
|
sucursal = Column(String(50))
|
||||||
|
fecha_ingreso = Column(Date)
|
||||||
|
estatus = Column(String(15))
|
||||||
|
nombre_completo = Column(String(150))
|
||||||
|
nombre = Column(String(50))
|
||||||
|
nombre_preferido = Column(String(50))
|
||||||
|
apellido_paterno = Column(String(50))
|
||||||
|
apellido_materno = Column(String(50))
|
||||||
|
fecha_nacimiento = Column(Date)
|
||||||
|
lugar_nacimiento = Column(String(50))
|
||||||
|
rfc = Column(String(13), unique=True)
|
||||||
|
curp = Column(String(18), unique=True)
|
||||||
|
email = Column(String(100))
|
||||||
|
telefono_celular = Column(String(15))
|
||||||
|
domicilio_calle = Column(String(255))
|
||||||
|
domicilio_numero_exterior = Column(String(10))
|
||||||
|
domicilio_numero_interior = Column(String(10))
|
||||||
|
domicilio_numero_texto = Column(String(50))
|
||||||
|
domicilio_colonia = Column(String(255))
|
||||||
|
domicilio_codigo_postal = Column(String(10))
|
||||||
|
domicilio_ciudad = Column(String(100))
|
||||||
|
domicilio_estado = Column(String(50))
|
||||||
|
domicilio_completo = Column(String(255))
|
||||||
|
emergencia_nombre = Column(String(100))
|
||||||
|
emergencia_telefono = Column(String(15))
|
||||||
|
emergencia_parentesco = Column(String(50))
|
||||||
|
referencia_1_nombre = Column(String(100))
|
||||||
|
referencia_1_telefono = Column(String(15))
|
||||||
|
referencia_1_tipo = Column(String(20))
|
||||||
|
referencia_2_nombre = Column(String(100))
|
||||||
|
referencia_2_telefono = Column(String(15))
|
||||||
|
referencia_2_tipo = Column(String(20))
|
||||||
|
referencia_3_nombre = Column(String(100))
|
||||||
|
referencia_3_telefono = Column(String(15))
|
||||||
|
referencia_3_tipo = Column(String(20))
|
||||||
|
origen_registro = Column(String(50))
|
||||||
|
telegram_usuario = Column(String(50))
|
||||||
|
telegram_chat_id = Column(BigInteger)
|
||||||
|
bot_version = Column(String(20))
|
||||||
|
fecha_registro = Column(DateTime)
|
||||||
|
tiempo_registro_minutos = Column(Integer)
|
||||||
|
fecha_procesamiento = Column(DateTime)
|
||||||
|
|
||||||
|
class Vacaciones(Base):
|
||||||
|
__tablename__ = 'vacaciones'
|
||||||
|
__table_args__ = {'schema': 'vanity_hr'}
|
||||||
|
|
||||||
|
vacaciones_id = Column(String(50), primary_key=True)
|
||||||
|
numero_empleado = Column(String(15), ForeignKey('vanity_hr.data_empleadas.numero_empleado'))
|
||||||
|
tipo_solicitud = Column(String(20))
|
||||||
|
estatus = Column(Enum('pendiente', 'aprobado', 'rechazado', 'cancelado'))
|
||||||
|
fecha_inicio = Column(Date)
|
||||||
|
fecha_fin = Column(Date)
|
||||||
|
dias_solicitados = Column(Integer)
|
||||||
|
dias_habiles = Column(Integer)
|
||||||
|
motivo = Column(Text)
|
||||||
|
con_goce_sueldo = Column(TINYINT)
|
||||||
|
fecha_solicitud = Column(DateTime)
|
||||||
|
fecha_procesamiento = Column(DateTime)
|
||||||
|
origen = Column(String(20))
|
||||||
|
afecta_nomina = Column(TINYINT)
|
||||||
|
empleada = relationship("DataEmpleadas")
|
||||||
|
|
||||||
|
class Permisos(Base):
|
||||||
|
__tablename__ = 'permisos'
|
||||||
|
__table_args__ = {'schema': 'vanity_hr'}
|
||||||
|
|
||||||
|
permiso_id = Column(String(50), primary_key=True)
|
||||||
|
numero_empleado = Column(String(15), ForeignKey('vanity_hr.data_empleadas.numero_empleado'))
|
||||||
|
categoria = Column(Enum('PERSONAL', 'MEDICO', 'OFICIAL', 'OTRO'))
|
||||||
|
estatus = Column(Enum('pendiente', 'aprobado', 'rechazado', 'cancelado'))
|
||||||
|
fecha_inicio = Column(Date)
|
||||||
|
horario_especifico = Column(String(50))
|
||||||
|
motivo = Column(Text)
|
||||||
|
con_goce_sueldo = Column(TINYINT)
|
||||||
|
afecta_nomina = Column(TINYINT)
|
||||||
|
empleada = relationship("DataEmpleadas")
|
||||||
@@ -1,14 +1,40 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy import Column, DateTime, Integer, MetaData, String, create_engine
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from models.users_alma_models import Base as BaseUsersAlma, User
|
||||||
|
from models.vanity_hr_models import Base as BaseVanityHr, DataEmpleadas, Vacaciones, Permisos
|
||||||
|
from models.vanity_attendance_models import Base as BaseVanityAttendance, AsistenciaRegistros, HorarioEmpleadas
|
||||||
import gspread
|
import gspread
|
||||||
from google.oauth2.service_account import Credentials
|
from google.oauth2.service_account import Credentials
|
||||||
|
|
||||||
# Configuración de logging
|
# --- DATABASE (MySQL) SETUP ---
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
def _build_engine(db_name_env_var):
|
||||||
|
user = os.getenv("MYSQL_USER")
|
||||||
|
password = os.getenv("MYSQL_PASSWORD")
|
||||||
|
db_name = os.getenv(db_name_env_var)
|
||||||
|
host = os.getenv("MYSQL_HOST", "db")
|
||||||
|
|
||||||
|
if not all([user, password, db_name]):
|
||||||
|
logging.warning(f"Database connection disabled: missing environment variables for {db_name_env_var}.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
db_url = f"mysql+mysqlconnector://{user}:{password}@{host}:3306/{db_name}"
|
||||||
|
return create_engine(db_url, pool_pre_ping=True)
|
||||||
|
except Exception as exc:
|
||||||
|
logging.error(f"Could not create database engine for {db_name}: {exc}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create engines for each database
|
||||||
|
engine_users_alma = _build_engine("MYSQL_DATABASE_USERS_ALMA")
|
||||||
|
engine_vanity_hr = _build_engine("MYSQL_DATABASE_VANITY_HR")
|
||||||
|
engine_vanity_attendance = _build_engine("MYSQL_DATABASE_VANITY_ATTENDANCE")
|
||||||
|
|
||||||
|
# Create sessions for each database
|
||||||
|
SessionUsersAlma = sessionmaker(autocommit=False, autoflush=False, bind=engine_users_alma) if engine_users_alma else None
|
||||||
|
SessionVanityHr = sessionmaker(autocommit=False, autoflush=False, bind=engine_vanity_hr) if engine_vanity_hr else None
|
||||||
|
SessionVanityAttendance = sessionmaker(autocommit=False, autoflush=False, bind=engine_vanity_attendance) if engine_vanity_attendance else None
|
||||||
|
|
||||||
# --- GOOGLE SHEETS SETUP ---
|
# --- GOOGLE SHEETS SETUP ---
|
||||||
GSHEET_URL = os.getenv("GOOGLE_SHEET_URL")
|
GSHEET_URL = os.getenv("GOOGLE_SHEET_URL")
|
||||||
@@ -16,15 +42,14 @@ GOOGLE_CREDENTIALS_FILE = os.getenv("GOOGLE_CREDENTIALS_FILE", "google_credentia
|
|||||||
SHEET_COLUMN_INDEX = 40 # AN is the 40th column
|
SHEET_COLUMN_INDEX = 40 # AN is the 40th column
|
||||||
|
|
||||||
def get_gsheet_client():
|
def get_gsheet_client():
|
||||||
"""Retorna un cliente de gspread autenticado o None si falla."""
|
"""Returns an authenticated gspread client or None if it fails."""
|
||||||
if not GSHEET_URL:
|
if not GSHEET_URL:
|
||||||
logging.warning("GOOGLE_SHEET_URL no está configurada. La verificación de duplicados está deshabilitada.")
|
logging.warning("GOOGLE_SHEET_URL is not configured. Duplicate checking is disabled.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
creds = None
|
creds = None
|
||||||
scopes = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
|
scopes = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
|
||||||
|
|
||||||
# Prioridad 1: Cargar desde variables de entorno
|
|
||||||
gsa_creds_dict = {
|
gsa_creds_dict = {
|
||||||
"type": os.getenv("GSA_TYPE"),
|
"type": os.getenv("GSA_TYPE"),
|
||||||
"project_id": os.getenv("GSA_PROJECT_ID"),
|
"project_id": os.getenv("GSA_PROJECT_ID"),
|
||||||
@@ -41,139 +66,39 @@ def get_gsheet_client():
|
|||||||
if all(gsa_creds_dict.values()):
|
if all(gsa_creds_dict.values()):
|
||||||
try:
|
try:
|
||||||
creds = Credentials.from_service_account_info(gsa_creds_dict, scopes=scopes)
|
creds = Credentials.from_service_account_info(gsa_creds_dict, scopes=scopes)
|
||||||
logging.info("Autenticando con Google Sheets usando variables de entorno.")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error al procesar credenciales de entorno de Google: {e}")
|
logging.error(f"Error processing Google credentials from environment: {e}")
|
||||||
return None
|
return None
|
||||||
# Prioridad 2: Cargar desde archivo JSON
|
|
||||||
elif os.path.exists(GOOGLE_CREDENTIALS_FILE):
|
elif os.path.exists(GOOGLE_CREDENTIALS_FILE):
|
||||||
try:
|
try:
|
||||||
creds = Credentials.from_service_account_file(GOOGLE_CREDENTIALS_FILE, scopes=scopes)
|
creds = Credentials.from_service_account_file(GOOGLE_CREDENTIALS_FILE, scopes=scopes)
|
||||||
logging.info(f"Autenticando con Google Sheets usando el archivo '{GOOGLE_CREDENTIALS_FILE}'.")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error al procesar el archivo de credenciales '{GOOGLE_CREDENTIALS_FILE}': {e}")
|
logging.error(f"Error processing credentials file '{GOOGLE_CREDENTIALS_FILE}': {e}")
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
logging.warning("No se encontraron credenciales de Google (ni por variables de entorno ni por archivo). La verificación de duplicados está deshabilitada.")
|
logging.warning("Google credentials not found (neither environment variables nor file). Duplicate checking is disabled.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = gspread.authorize(creds)
|
return gspread.authorize(creds)
|
||||||
return client
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error al autorizar cliente de gspread: {e}")
|
logging.error(f"Error authorizing gspread client: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def chat_id_exists(chat_id: int) -> bool:
|
def chat_id_exists(chat_id: int) -> bool:
|
||||||
"""Verifica si un chat_id de Telegram ya existe en la columna AN de la hoja de cálculo."""
|
"""Checks if a Telegram chat_id already exists in the Google Sheet."""
|
||||||
client = get_gsheet_client()
|
client = get_gsheet_client()
|
||||||
if not client:
|
if not client:
|
||||||
return False # Si no hay cliente, no podemos verificar, así que asumimos que no existe.
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
spreadsheet = client.open_by_url(GSHEET_URL)
|
spreadsheet = client.open_by_url(GSHEET_URL)
|
||||||
worksheet = spreadsheet.get_worksheet(0) # Primera hoja
|
worksheet = spreadsheet.get_worksheet(0)
|
||||||
|
|
||||||
# Obtener todos los valores de la columna AN (índice 40)
|
|
||||||
chat_ids_in_sheet = worksheet.col_values(SHEET_COLUMN_INDEX)
|
chat_ids_in_sheet = worksheet.col_values(SHEET_COLUMN_INDEX)
|
||||||
|
|
||||||
# El ID de chat puede venir como número o texto, así que comparamos como string
|
|
||||||
return str(chat_id) in chat_ids_in_sheet
|
return str(chat_id) in chat_ids_in_sheet
|
||||||
|
|
||||||
except gspread.exceptions.SpreadsheetNotFound:
|
except gspread.exceptions.SpreadsheetNotFound:
|
||||||
logging.error(f"No se pudo encontrar la hoja de cálculo en la URL proporcionada.")
|
logging.error("Could not find the spreadsheet at the provided URL.")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error al leer la hoja de cálculo: {e}")
|
logging.error(f"Error reading the spreadsheet: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# --- DATABASE (MySQL) SETUP ---
|
|
||||||
|
|
||||||
# Base para los modelos declarativos
|
|
||||||
Base = declarative_base()
|
|
||||||
|
|
||||||
# Clase que mapea a la tabla de logs
|
|
||||||
class RequestLog(Base):
|
|
||||||
__tablename__ = 'request_logs'
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
telegram_id = Column(String(50))
|
|
||||||
username = Column(String(100))
|
|
||||||
command = Column(String(100))
|
|
||||||
message = Column(String(500))
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
|
||||||
|
|
||||||
def _build_engine():
|
|
||||||
"""Crea un engine de SQLAlchemy si hay variables de entorno suficientes."""
|
|
||||||
user = os.getenv("MYSQL_USER")
|
|
||||||
password = os.getenv("MYSQL_PASSWORD")
|
|
||||||
database = os.getenv("MYSQL_DATABASE")
|
|
||||||
host = os.getenv("MYSQL_HOST") or "db" # Permitimos override para uso local
|
|
||||||
|
|
||||||
if not all([user, password, database]):
|
|
||||||
logging.warning("DB logging deshabilitado: faltan MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
db_url = f"mysql+mysqlconnector://{user}:{password}@{host}:3306/{database}"
|
|
||||||
return create_engine(db_url, pool_pre_ping=True)
|
|
||||||
except Exception as exc:
|
|
||||||
logging.error(f"No se pudo crear el engine de base de datos: {exc}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _disable_db_logging(reason: str):
|
|
||||||
"""Deshabilita el logging a DB después de un error para evitar spam."""
|
|
||||||
global engine, SessionLocal
|
|
||||||
engine = None
|
|
||||||
SessionLocal = None
|
|
||||||
logging.warning(f"DB logging deshabilitado: {reason}")
|
|
||||||
|
|
||||||
# Crear el engine y sesión si es posible
|
|
||||||
engine = _build_engine()
|
|
||||||
metadata = MetaData() if engine else None
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) if engine else None
|
|
||||||
|
|
||||||
# Función para inicializar la base de datos
|
|
||||||
def init_db():
|
|
||||||
global engine, SessionLocal
|
|
||||||
if not engine:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
logging.info("Inicializando la base de datos y creando tablas si no existen...")
|
|
||||||
Base.metadata.create_all(bind=engine)
|
|
||||||
logging.info("Tablas verificadas/creadas correctamente.")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error al inicializar la base de datos: {e}")
|
|
||||||
_disable_db_logging("no se pudo inicializar la base de datos (se omitirán logs).")
|
|
||||||
# No propagamos para que el bot pueda seguir levantando aunque no haya DB
|
|
||||||
|
|
||||||
# Función para registrar una solicitud en la base de datos
|
|
||||||
def log_request(telegram_id, username, command, message):
|
|
||||||
if not SessionLocal:
|
|
||||||
logging.debug("Log de DB omitido (DB no configurada).")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
db_session = SessionLocal()
|
|
||||||
except Exception as exc:
|
|
||||||
logging.error(f"No se pudo crear sesión DB, se deshabilita el log: {exc}")
|
|
||||||
_disable_db_logging("no se pudo abrir sesión")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
log_entry = RequestLog(
|
|
||||||
telegram_id=str(telegram_id),
|
|
||||||
username=username,
|
|
||||||
command=command,
|
|
||||||
message=message
|
|
||||||
)
|
|
||||||
db_session.add(log_entry)
|
|
||||||
db_session.commit()
|
|
||||||
logging.info(f"Log guardado: {command} de {username}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error al guardar el log: {e}")
|
|
||||||
db_session.rollback()
|
|
||||||
finally:
|
|
||||||
db_session.close()
|
|
||||||
|
|
||||||
# Inicializar la base de datos al arrancar el módulo
|
|
||||||
init_db()
|
|
||||||
|
|||||||
29
modules/logger.py
Normal file
29
modules/logger.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import logging
|
||||||
|
from modules.database import SessionUsersAlma
|
||||||
|
from models.users_alma_models import RequestLog
|
||||||
|
|
||||||
|
def log_request(telegram_id, username, command, message):
|
||||||
|
if not SessionUsersAlma:
|
||||||
|
logging.debug("DB log omitted (DB not configured).")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
db_session = SessionUsersAlma()
|
||||||
|
except Exception as exc:
|
||||||
|
logging.error(f"Could not create DB session, logging is disabled: {exc}")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
log_entry = RequestLog(
|
||||||
|
telegram_id=str(telegram_id),
|
||||||
|
username=username,
|
||||||
|
command=command,
|
||||||
|
message=message
|
||||||
|
)
|
||||||
|
db_session.add(log_entry)
|
||||||
|
db_session.commit()
|
||||||
|
logging.info(f"Log saved: {command} from {username}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error saving log: {e}")
|
||||||
|
db_session.rollback()
|
||||||
|
finally:
|
||||||
|
db_session.close()
|
||||||
@@ -17,7 +17,8 @@ from telegram.ext import (
|
|||||||
Defaults,
|
Defaults,
|
||||||
)
|
)
|
||||||
|
|
||||||
from modules.database import log_request, chat_id_exists
|
from modules.logger import log_request
|
||||||
|
from modules.database import chat_id_exists
|
||||||
from modules.ui import main_actions_keyboard
|
from modules.ui import main_actions_keyboard
|
||||||
|
|
||||||
# --- 1. CARGA DE ENTORNO ---
|
# --- 1. CARGA DE ENTORNO ---
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import string
|
|||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
|
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
|
||||||
from telegram.ext import CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters
|
from telegram.ext import CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters
|
||||||
from modules.database import log_request
|
from modules.logger import log_request
|
||||||
from modules.ui import main_actions_keyboard
|
from modules.ui import main_actions_keyboard
|
||||||
from modules.ai import classify_reason
|
from modules.ai import classify_reason
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user