-- ServiceManagerWeb - Esquema de Base de Datos -- Sistema multi-tenant de Mesa de Ayuda B2B -- PostgreSQL 15+ -- =================================== -- DOMINIO: TENANTS (Multi-tenancy) -- =================================== CREATE TABLE tenants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, slug VARCHAR(100) UNIQUE NOT NULL, domain VARCHAR(255), logo_url VARCHAR(500), contact_email VARCHAR(320), contact_phone VARCHAR(20), address TEXT, timezone VARCHAR(50) DEFAULT 'UTC', locale VARCHAR(10) DEFAULT 'es-ES', -- Settings max_users INTEGER DEFAULT 50, max_storage_mb INTEGER DEFAULT 1024, allowed_file_types TEXT[] DEFAULT ARRAY['pdf','jpg','jpeg','png','doc','docx','xls','xlsx','txt'], -- Status status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'inactive')), -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), CONSTRAINT tenants_slug_format CHECK (slug ~ '^[a-z0-9-]+$') ); -- Índices para tenants CREATE INDEX idx_tenants_slug ON tenants(slug); CREATE INDEX idx_tenants_status ON tenants(status); CREATE INDEX idx_tenants_domain ON tenants(domain); -- =================================== -- DOMINIO: CLIENTS (Cartera de Clientes) -- =================================== -- Enum para los radio buttons de estatus CREATE TYPE client_status_enum AS ENUM ( 'prospect', 'active', 'suspended', 'cancelled' ); CREATE TABLE clients ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- Identificación (Sección General) code VARCHAR(50), -- Clave del cliente name VARCHAR(200) NOT NULL, -- Nombre / Razón Social tax_id VARCHAR(20), -- RFC client_type VARCHAR(50), -- Tipo de Cliente account_manager VARCHAR(100), -- Encargado Cliente (Texto simple por ahora) -- Dirección (Desglosada) address_street TEXT, -- Dirección / Calle address_ext_num VARCHAR(20), -- Núm. Ext neighborhood VARCHAR(100), -- Colonia zip_code VARCHAR(10), -- CP city VARCHAR(100), -- Ciudad state VARCHAR(100), -- Estado country VARCHAR(100) DEFAULT 'Mexico', -- Contacto phone_primary VARCHAR(20), -- Teléfono 1 phone_secondary VARCHAR(20),-- Teléfono 2 fax VARCHAR(20), email VARCHAR(320), -- Correo website VARCHAR(255), -- Página Web -- Configuración y Flexibilidad status client_status_enum DEFAULT 'prospect', logo_url VARCHAR(500), -- Ruta al archivo PDF/Imagen del logo -- Campo JSONB para lo que sobre (Horario, Nacionalidad, Medio Publicidad, Lada, etc.) properties JSONB DEFAULT '{}'::jsonb, -- Auditoría estándar is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- Restricciones: No repetir RFC ni Clave dentro del mismo Tenant UNIQUE(tenant_id, code), UNIQUE(tenant_id, tax_id) ); -- Índices para búsqueda rápida CREATE INDEX idx_clients_tenant_id ON clients(tenant_id); CREATE INDEX idx_clients_tax_id ON clients(tax_id); CREATE INDEX idx_clients_name ON clients(name); CREATE INDEX idx_clients_properties ON clients USING gin (properties); -- Trigger para mantener actualizado el campo updated_at -- (Usa la función que ya definiste al final de tu script) CREATE TRIGGER update_clients_updated_at BEFORE UPDATE ON clients FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- =================================== -- DOMINIO: AUTH (Autenticación) -- =================================== CREATE TYPE user_role_enum AS ENUM ( 'ADMIN', -- Control total (staff interno) 'SUPPORT_MANAGER', -- Gestión de equipos y SLAs 'AGENT', -- Atención de tickets 'AUDITOR', -- Solo lectura para auditoría 'CLIENT_ADMIN', -- Admin de organización cliente 'CLIENT_USER' -- Usuario final cliente ); CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- Básicos email VARCHAR(320) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, avatar_url VARCHAR(500), -- Auth password_hash VARCHAR(255) NOT NULL, role user_role_enum NOT NULL, -- 2FA (opcional para staff interno) totp_secret VARCHAR(32), totp_enabled BOOLEAN DEFAULT FALSE, backup_codes TEXT[], -- códigos de recuperación -- Status is_active BOOLEAN DEFAULT TRUE, email_verified BOOLEAN DEFAULT FALSE, last_login TIMESTAMP WITH TIME ZONE, last_activity TIMESTAMP WITH TIME ZONE, -- Preferences language VARCHAR(10) DEFAULT 'es', timezone VARCHAR(50) DEFAULT 'UTC', notifications_email BOOLEAN DEFAULT TRUE, -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(tenant_id, email) ); -- Índices para users CREATE INDEX idx_users_tenant_id ON users(tenant_id); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_role ON users(role); CREATE INDEX idx_users_active ON users(is_active); CREATE INDEX idx_users_tenant_email ON users(tenant_id, email); -- Tabla para refresh tokens CREATE TABLE refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash VARCHAR(255) NOT NULL, device_info VARCHAR(500), ip_address INET, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, revoked BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Índices para refresh_tokens CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id); CREATE INDEX idx_refresh_tokens_hash ON refresh_tokens(token_hash); CREATE INDEX idx_refresh_tokens_expires ON refresh_tokens(expires_at); -- =================================== -- DOMINIO: TICKETS (Core del negocio) -- =================================== CREATE TYPE ticket_status_enum AS ENUM ( 'NEW', 'TRIAGE', 'IN_PROGRESS', 'WAITING_CUSTOMER', 'RESOLVED', 'CLOSED', 'REOPENED' ); CREATE TYPE ticket_priority_enum AS ENUM ( 'LOW', 'MEDIUM', 'HIGH', 'URGENT' ); -- Categorías de tickets por tenant CREATE TABLE ticket_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, description TEXT, color VARCHAR(7), -- hex color sla_response_hours INTEGER DEFAULT 24, sla_resolution_hours INTEGER DEFAULT 72, auto_assign_to UUID REFERENCES users(id), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(tenant_id, name) ); -- Sistemas afectados por tenant CREATE TABLE affected_systems ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, description TEXT, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(tenant_id, name) ); -- Tickets principales CREATE TABLE tickets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, ticket_number VARCHAR(20) NOT NULL, -- formato: TKT-2024-000001 -- Básicos subject VARCHAR(255) NOT NULL, description TEXT NOT NULL, -- Clasificación category_id UUID REFERENCES ticket_categories(id), priority ticket_priority_enum DEFAULT 'MEDIUM', affected_system_id UUID REFERENCES affected_systems(id), -- Asignación created_by UUID NOT NULL REFERENCES users(id), assigned_to UUID REFERENCES users(id), -- Estado status ticket_status_enum DEFAULT 'NEW', -- SLA tracking sla_response_due TIMESTAMP WITH TIME ZONE, sla_resolution_due TIMESTAMP WITH TIME ZONE, first_response_at TIMESTAMP WITH TIME ZONE, resolved_at TIMESTAMP WITH TIME ZONE, -- CSAT (Customer Satisfaction) rating INTEGER CHECK (rating >= 1 AND rating <= 5), rating_comment TEXT, rated_at TIMESTAMP WITH TIME ZONE, -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(tenant_id, ticket_number) ); -- Índices para tickets CREATE INDEX idx_tickets_tenant_id ON tickets(tenant_id); CREATE INDEX idx_tickets_number ON tickets(ticket_number); CREATE INDEX idx_tickets_created_by ON tickets(created_by); CREATE INDEX idx_tickets_assigned_to ON tickets(assigned_to); CREATE INDEX idx_tickets_status ON tickets(status); CREATE INDEX idx_tickets_priority ON tickets(priority); CREATE INDEX idx_tickets_category ON tickets(category_id); CREATE INDEX idx_tickets_sla_response ON tickets(sla_response_due); CREATE INDEX idx_tickets_sla_resolution ON tickets(sla_resolution_due); CREATE INDEX idx_tickets_created_at ON tickets(created_at); -- Comentarios en tickets CREATE TABLE ticket_comments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, author_id UUID NOT NULL REFERENCES users(id), content TEXT NOT NULL, is_internal BOOLEAN DEFAULT FALSE, -- solo visible para staff interno created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Índices para comentarios CREATE INDEX idx_ticket_comments_ticket_id ON ticket_comments(ticket_id); CREATE INDEX idx_ticket_comments_author_id ON ticket_comments(author_id); CREATE INDEX idx_ticket_comments_created_at ON ticket_comments(created_at); -- Adjuntos en tickets/comentarios CREATE TABLE ticket_attachments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, comment_id UUID REFERENCES ticket_comments(id) ON DELETE CASCADE, uploaded_by UUID NOT NULL REFERENCES users(id), -- Archivo filename VARCHAR(255) NOT NULL, original_filename VARCHAR(255) NOT NULL, mime_type VARCHAR(100) NOT NULL, file_size INTEGER NOT NULL, -- bytes file_path VARCHAR(500) NOT NULL, -- path en storage -- Checksums para integridad md5_hash VARCHAR(32), sha256_hash VARCHAR(64), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Índices para adjuntos CREATE INDEX idx_ticket_attachments_ticket_id ON ticket_attachments(ticket_id); CREATE INDEX idx_ticket_attachments_comment_id ON ticket_attachments(comment_id); CREATE INDEX idx_ticket_attachments_uploaded_by ON ticket_attachments(uploaded_by); -- Historial de cambios de estado CREATE TABLE ticket_status_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, changed_by UUID NOT NULL REFERENCES users(id), old_status ticket_status_enum, new_status ticket_status_enum NOT NULL, old_assigned_to UUID REFERENCES users(id), new_assigned_to UUID REFERENCES users(id), comment TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Índices para historial de estados CREATE INDEX idx_ticket_status_history_ticket_id ON ticket_status_history(ticket_id); CREATE INDEX idx_ticket_status_history_changed_by ON ticket_status_history(changed_by); CREATE INDEX idx_ticket_status_history_created_at ON ticket_status_history(created_at); -- =================================== -- DOMINIO: NOTIFICATIONS (Comunicaciones) -- =================================== -- Templates de email CREATE TABLE email_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES tenants(id), -- NULL = template global name VARCHAR(100) NOT NULL, subject_template TEXT NOT NULL, body_template TEXT NOT NULL, template_type VARCHAR(50) NOT NULL, -- ticket_created, ticket_assigned, etc. -- Variables disponibles en formato JSON available_variables JSONB, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Índices para templates CREATE INDEX idx_email_templates_tenant_id ON email_templates(tenant_id); CREATE INDEX idx_email_templates_type ON email_templates(template_type); -- Log de notificaciones enviadas CREATE TABLE notification_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id), recipient_email VARCHAR(320) NOT NULL, subject VARCHAR(500) NOT NULL, template_type VARCHAR(50), -- Metadata ticket_id UUID REFERENCES tickets(id), user_id UUID REFERENCES users(id), -- Estado del envío status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed', 'bounced')), error_message TEXT, -- Proveedor (ej: sendgrid, ses, smtp) provider VARCHAR(50), external_id VARCHAR(255), -- ID del proveedor sent_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Índices para logs de notificaciones CREATE INDEX idx_notification_logs_tenant_id ON notification_logs(tenant_id); CREATE INDEX idx_notification_logs_recipient ON notification_logs(recipient_email); CREATE INDEX idx_notification_logs_ticket_id ON notification_logs(ticket_id); CREATE INDEX idx_notification_logs_status ON notification_logs(status); CREATE INDEX idx_notification_logs_created_at ON notification_logs(created_at); -- =================================== -- DOMINIO: AUDIT (Bitácora) -- =================================== CREATE TABLE audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id), user_id UUID REFERENCES users(id), -- NULL para acciones del sistema -- Acción action VARCHAR(100) NOT NULL, -- ej: user.login, ticket.create, ticket.assign resource_type VARCHAR(50) NOT NULL, -- user, ticket, comment, etc. resource_id UUID, -- Contexto ip_address INET, user_agent TEXT, correlation_id UUID, -- para rastrear requests -- Cambios (antes/después en JSON) old_values JSONB, new_values JSONB, -- Metadata adicional metadata JSONB, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Índices para audit logs CREATE INDEX idx_audit_logs_tenant_id ON audit_logs(tenant_id); CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id); CREATE INDEX idx_audit_logs_action ON audit_logs(action); CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id); CREATE INDEX idx_audit_logs_correlation_id ON audit_logs(correlation_id); CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at); -- =================================== -- FUNCIONES Y TRIGGERS -- =================================== -- Función para actualizar updated_at automáticamente CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ language 'plpgsql'; -- Triggers para updated_at CREATE TRIGGER update_tenants_updated_at BEFORE UPDATE ON tenants FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_tickets_updated_at BEFORE UPDATE ON tickets FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_ticket_comments_updated_at BEFORE UPDATE ON ticket_comments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_email_templates_updated_at BEFORE UPDATE ON email_templates FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- =================================== -- DATOS INICIALES (SEEDS) -- =================================== -- Tenant de ejemplo para desarrollo INSERT INTO tenants (name, slug, contact_email) VALUES ('Aduanasoft Demo', 'aduanasoft-demo', 'demo@aduanasoft.com'); -- Usuario admin por defecto (password: admin123) -- Hash generado con Argon2: $argon2id$v=19$m=65536,t=3,p=4$... INSERT INTO users (tenant_id, email, first_name, last_name, password_hash, role, is_active, email_verified) SELECT id, 'admin@aduanasoft.com', 'Admin', 'Sistema', '$argon2id$v=19$m=65536,t=3,p=4$example_hash_here', 'ADMIN', true, true FROM tenants WHERE slug = 'aduanasoft-demo'; -- Categorías por defecto INSERT INTO ticket_categories (tenant_id, name, description, sla_response_hours, sla_resolution_hours) SELECT id, 'Soporte Técnico', 'Problemas técnicos generales', 2, 24 FROM tenants WHERE slug = 'aduanasoft-demo' UNION ALL SELECT id, 'Consulta Comercial', 'Preguntas sobre productos y servicios', 4, 48 FROM tenants WHERE slug = 'aduanasoft-demo' UNION ALL SELECT id, 'Incidente Crítico', 'Problemas que afectan operaciones', 1, 8 FROM tenants WHERE slug = 'aduanasoft-demo'; -- Sistemas afectados por defecto INSERT INTO affected_systems (tenant_id, name, description) SELECT id, 'Plataforma Web', 'Sistema web principal' FROM tenants WHERE slug = 'aduanasoft-demo' UNION ALL SELECT id, 'API', 'Servicios de API' FROM tenants WHERE slug = 'aduanasoft-demo' UNION ALL SELECT id, 'Base de Datos', 'Sistemas de almacenamiento' FROM tenants WHERE slug = 'aduanasoft-demo'; -- Templates de email básicos INSERT INTO email_templates (name, subject_template, body_template, template_type, available_variables) VALUES ( 'Ticket Creado', 'Nuevo ticket #{{ticket_number}}: {{subject}}', 'Hola {{user_name}},\n\nSe ha creado un nuevo ticket:\n\nNúmero: #{{ticket_number}}\nAsunto: {{subject}}\nPrioridad: {{priority}}\n\nPuedes ver los detalles en: {{ticket_url}}\n\nSaludos,\nEquipo de Soporte', 'ticket_created', '{"ticket_number": "string", "subject": "string", "priority": "string", "user_name": "string", "ticket_url": "string"}' ), ( 'Ticket Asignado', 'Ticket #{{ticket_number}} asignado a ti', 'Hola {{agent_name}},\n\nSe te ha asignado el ticket:\n\nNúmero: #{{ticket_number}}\nAsunto: {{subject}}\nCliente: {{customer_name}}\nPrioridad: {{priority}}\n\nPuedes verlo en: {{ticket_url}}\n\nSaludos,\nSistema de Tickets', 'ticket_assigned', '{"ticket_number": "string", "subject": "string", "agent_name": "string", "customer_name": "string", "priority": "string", "ticket_url": "string"}' ); -- =================================== -- COMENTARIOS Y DOCUMENTACIÓN -- =================================== COMMENT ON TABLE tenants IS 'Organizaciones cliente en el sistema multi-tenant'; COMMENT ON TABLE users IS 'Usuarios del sistema (internos y clientes)'; COMMENT ON TABLE tickets IS 'Tickets de soporte - core del negocio'; COMMENT ON TABLE ticket_comments IS 'Comentarios en tickets'; COMMENT ON TABLE ticket_attachments IS 'Archivos adjuntos en tickets'; COMMENT ON TABLE audit_logs IS 'Bitácora de acciones para auditoría y compliance';