Files
service_manager/db/schema.sql
2026-02-27 09:16:08 -07:00

505 lines
17 KiB
PL/PgSQL

-- 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: 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_correlation_id ON audit_logs(correlation_id);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
-- Índices compuestos para queries comunes de auditoría
CREATE INDEX idx_audit_logs_tenant_action ON audit_logs(tenant_id, action);
CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id);
CREATE INDEX idx_audit_logs_user_created ON audit_logs(user_id, 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 Argon2id generado con 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$wpjz/t+bM4bQmtM6B6A0pg$ELwnGUL4S1Y6tywp0LS6cre0bvWEoVuJ845spZ9Z9IQ',
'ADMIN',
true,
true
FROM tenants WHERE slug = 'aduanasoft-demo'
ON CONFLICT (tenant_id, email) DO NOTHING;
-- 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';