854 lines
28 KiB
PL/PgSQL
854 lines
28 KiB
PL/PgSQL
-- =====================================================
|
|
-- SCHEMA: system
|
|
-- PROPÓSITO: Mensajería, notificaciones, logs, reportes
|
|
-- MÓDULOS: MGN-012 (Reportes), MGN-014 (Mensajería)
|
|
-- FECHA: 2025-11-24
|
|
-- =====================================================
|
|
|
|
-- Crear schema
|
|
CREATE SCHEMA IF NOT EXISTS system;
|
|
|
|
-- =====================================================
|
|
-- TYPES (ENUMs)
|
|
-- =====================================================
|
|
|
|
CREATE TYPE system.message_type AS ENUM (
|
|
'comment',
|
|
'note',
|
|
'email',
|
|
'notification',
|
|
'system'
|
|
);
|
|
|
|
CREATE TYPE system.notification_status AS ENUM (
|
|
'pending',
|
|
'sent',
|
|
'read',
|
|
'failed'
|
|
);
|
|
|
|
CREATE TYPE system.activity_type AS ENUM (
|
|
'call',
|
|
'meeting',
|
|
'email',
|
|
'todo',
|
|
'follow_up',
|
|
'custom'
|
|
);
|
|
|
|
CREATE TYPE system.activity_status AS ENUM (
|
|
'planned',
|
|
'done',
|
|
'cancelled',
|
|
'overdue'
|
|
);
|
|
|
|
CREATE TYPE system.email_status AS ENUM (
|
|
'draft',
|
|
'queued',
|
|
'sending',
|
|
'sent',
|
|
'failed',
|
|
'bounced'
|
|
);
|
|
|
|
CREATE TYPE system.log_level AS ENUM (
|
|
'debug',
|
|
'info',
|
|
'warning',
|
|
'error',
|
|
'critical'
|
|
);
|
|
|
|
CREATE TYPE system.report_format AS ENUM (
|
|
'pdf',
|
|
'excel',
|
|
'csv',
|
|
'html'
|
|
);
|
|
|
|
-- =====================================================
|
|
-- TABLES
|
|
-- =====================================================
|
|
|
|
-- Tabla: messages (Chatter - mensajes en registros)
|
|
CREATE TABLE system.messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Referencia polimórfica (a qué registro pertenece)
|
|
model VARCHAR(100) NOT NULL, -- 'SaleOrder', 'Task', 'Invoice', etc.
|
|
record_id UUID NOT NULL,
|
|
|
|
-- Tipo y contenido
|
|
message_type system.message_type NOT NULL DEFAULT 'comment',
|
|
subject VARCHAR(255),
|
|
body TEXT NOT NULL,
|
|
|
|
-- Autor
|
|
author_id UUID REFERENCES auth.users(id),
|
|
author_name VARCHAR(255),
|
|
author_email VARCHAR(255),
|
|
|
|
-- Email tracking
|
|
email_from VARCHAR(255),
|
|
reply_to VARCHAR(255),
|
|
message_id VARCHAR(500), -- Message-ID para threading
|
|
|
|
-- Relación (respuesta a mensaje)
|
|
parent_id UUID REFERENCES system.messages(id),
|
|
|
|
-- Attachments
|
|
attachment_ids UUID[] DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
);
|
|
|
|
-- Tabla: message_followers (Seguidores de registros)
|
|
CREATE TABLE system.message_followers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
-- Referencia polimórfica
|
|
model VARCHAR(100) NOT NULL,
|
|
record_id UUID NOT NULL,
|
|
|
|
-- Seguidor
|
|
partner_id UUID REFERENCES core.partners(id),
|
|
user_id UUID REFERENCES auth.users(id),
|
|
|
|
-- Configuración
|
|
email_notifications BOOLEAN DEFAULT TRUE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT uq_message_followers UNIQUE (model, record_id, COALESCE(user_id, partner_id)),
|
|
CONSTRAINT chk_message_followers_user_or_partner CHECK (
|
|
(user_id IS NOT NULL AND partner_id IS NULL) OR
|
|
(partner_id IS NOT NULL AND user_id IS NULL)
|
|
)
|
|
);
|
|
|
|
-- Tabla: notifications (Notificaciones a usuarios)
|
|
CREATE TABLE system.notifications (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
|
|
-- Contenido
|
|
title VARCHAR(255) NOT NULL,
|
|
message TEXT NOT NULL,
|
|
url VARCHAR(500), -- URL para acción (ej: /sales/orders/123)
|
|
|
|
-- Referencia (opcional)
|
|
model VARCHAR(100),
|
|
record_id UUID,
|
|
|
|
-- Estado
|
|
status system.notification_status NOT NULL DEFAULT 'pending',
|
|
read_at TIMESTAMP,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
sent_at TIMESTAMP
|
|
);
|
|
|
|
-- Tabla: activities (Actividades programadas)
|
|
CREATE TABLE system.activities (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Referencia polimórfica
|
|
model VARCHAR(100) NOT NULL,
|
|
record_id UUID NOT NULL,
|
|
|
|
-- Actividad
|
|
activity_type system.activity_type NOT NULL,
|
|
summary VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Asignación
|
|
assigned_to UUID REFERENCES auth.users(id),
|
|
assigned_by UUID REFERENCES auth.users(id),
|
|
|
|
-- Fechas
|
|
due_date DATE NOT NULL,
|
|
due_time TIME,
|
|
|
|
-- Estado
|
|
status system.activity_status NOT NULL DEFAULT 'planned',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
completed_at TIMESTAMP,
|
|
completed_by UUID REFERENCES auth.users(id)
|
|
);
|
|
|
|
-- Tabla: message_templates (Plantillas de mensajes/emails)
|
|
CREATE TABLE system.message_templates (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
name VARCHAR(255) NOT NULL,
|
|
model VARCHAR(100), -- Para qué modelo se usa
|
|
|
|
-- Contenido
|
|
subject VARCHAR(255),
|
|
body_html TEXT,
|
|
body_text TEXT,
|
|
|
|
-- Configuración email
|
|
email_from VARCHAR(255),
|
|
reply_to VARCHAR(255),
|
|
cc VARCHAR(255),
|
|
bcc VARCHAR(255),
|
|
|
|
-- Control
|
|
active BOOLEAN DEFAULT TRUE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_message_templates_name_tenant UNIQUE (tenant_id, name)
|
|
);
|
|
|
|
-- Tabla: email_queue (Cola de envío de emails)
|
|
CREATE TABLE system.email_queue (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID REFERENCES auth.tenants(id),
|
|
|
|
-- Destinatarios
|
|
email_to VARCHAR(255) NOT NULL,
|
|
email_cc VARCHAR(500),
|
|
email_bcc VARCHAR(500),
|
|
|
|
-- Contenido
|
|
subject VARCHAR(255) NOT NULL,
|
|
body_html TEXT,
|
|
body_text TEXT,
|
|
|
|
-- Remitente
|
|
email_from VARCHAR(255) NOT NULL,
|
|
reply_to VARCHAR(255),
|
|
|
|
-- Attachments
|
|
attachment_ids UUID[] DEFAULT '{}',
|
|
|
|
-- Estado
|
|
status system.email_status NOT NULL DEFAULT 'queued',
|
|
attempts INTEGER DEFAULT 0,
|
|
max_attempts INTEGER DEFAULT 3,
|
|
error_message TEXT,
|
|
|
|
-- Tracking
|
|
message_id VARCHAR(500),
|
|
opened_at TIMESTAMP,
|
|
clicked_at TIMESTAMP,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
scheduled_at TIMESTAMP,
|
|
sent_at TIMESTAMP,
|
|
failed_at TIMESTAMP
|
|
);
|
|
|
|
-- Tabla: logs (Logs del sistema)
|
|
CREATE TABLE system.logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID REFERENCES auth.tenants(id),
|
|
|
|
-- Nivel y fuente
|
|
level system.log_level NOT NULL,
|
|
logger VARCHAR(100), -- Módulo que genera el log
|
|
|
|
-- Mensaje
|
|
message TEXT NOT NULL,
|
|
stack_trace TEXT,
|
|
|
|
-- Contexto
|
|
user_id UUID REFERENCES auth.users(id),
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
request_id UUID,
|
|
|
|
-- Referencia (opcional)
|
|
model VARCHAR(100),
|
|
record_id UUID,
|
|
|
|
-- Metadata adicional
|
|
metadata JSONB DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Tabla: reports (Definiciones de reportes)
|
|
CREATE TABLE system.reports (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
name VARCHAR(255) NOT NULL,
|
|
code VARCHAR(50) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Tipo
|
|
model VARCHAR(100), -- Para qué modelo es el reporte
|
|
report_type VARCHAR(50), -- 'standard', 'custom', 'dashboard'
|
|
|
|
-- Query/Template
|
|
query_template TEXT, -- SQL template o JSON query
|
|
template_file VARCHAR(255), -- Path al archivo de plantilla
|
|
|
|
-- Configuración
|
|
default_format system.report_format DEFAULT 'pdf',
|
|
is_public BOOLEAN DEFAULT FALSE,
|
|
|
|
-- Control
|
|
active BOOLEAN DEFAULT TRUE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_reports_code_tenant UNIQUE (tenant_id, code)
|
|
);
|
|
|
|
-- Tabla: report_executions (Ejecuciones de reportes)
|
|
CREATE TABLE system.report_executions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
report_id UUID NOT NULL REFERENCES system.reports(id) ON DELETE CASCADE,
|
|
|
|
-- Parámetros de ejecución
|
|
parameters JSONB DEFAULT '{}',
|
|
format system.report_format NOT NULL,
|
|
|
|
-- Resultado
|
|
file_url VARCHAR(500),
|
|
file_size BIGINT,
|
|
error_message TEXT,
|
|
|
|
-- Estado
|
|
status VARCHAR(20) DEFAULT 'pending', -- pending, running, completed, failed
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
started_at TIMESTAMP,
|
|
completed_at TIMESTAMP
|
|
);
|
|
|
|
-- Tabla: dashboards (Dashboards configurables)
|
|
CREATE TABLE system.dashboards (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Configuración
|
|
layout JSONB DEFAULT '{}', -- Grid layout configuration
|
|
is_default BOOLEAN DEFAULT FALSE,
|
|
|
|
-- Visibilidad
|
|
user_id UUID REFERENCES auth.users(id), -- NULL = compartido
|
|
is_public BOOLEAN DEFAULT FALSE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_dashboards_name_user UNIQUE (tenant_id, name, COALESCE(user_id, '00000000-0000-0000-0000-000000000000'::UUID))
|
|
);
|
|
|
|
-- Tabla: dashboard_widgets (Widgets en dashboards)
|
|
CREATE TABLE system.dashboard_widgets (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
dashboard_id UUID NOT NULL REFERENCES system.dashboards(id) ON DELETE CASCADE,
|
|
|
|
-- Tipo de widget
|
|
widget_type VARCHAR(50) NOT NULL, -- 'chart', 'kpi', 'table', 'calendar', etc.
|
|
title VARCHAR(255),
|
|
|
|
-- Configuración
|
|
config JSONB NOT NULL DEFAULT '{}', -- Widget-specific configuration
|
|
position JSONB DEFAULT '{}', -- {x, y, w, h} para grid
|
|
|
|
-- Data source
|
|
data_source VARCHAR(100), -- Model o query
|
|
query_params JSONB DEFAULT '{}',
|
|
|
|
-- Refresh
|
|
refresh_interval INTEGER, -- Segundos
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
);
|
|
|
|
-- =====================================================
|
|
-- INDICES
|
|
-- =====================================================
|
|
|
|
-- Messages
|
|
CREATE INDEX idx_messages_tenant_id ON system.messages(tenant_id);
|
|
CREATE INDEX idx_messages_model_record ON system.messages(model, record_id);
|
|
CREATE INDEX idx_messages_author_id ON system.messages(author_id);
|
|
CREATE INDEX idx_messages_parent_id ON system.messages(parent_id);
|
|
CREATE INDEX idx_messages_created_at ON system.messages(created_at DESC);
|
|
|
|
-- Message Followers
|
|
CREATE INDEX idx_message_followers_model_record ON system.message_followers(model, record_id);
|
|
CREATE INDEX idx_message_followers_user_id ON system.message_followers(user_id);
|
|
CREATE INDEX idx_message_followers_partner_id ON system.message_followers(partner_id);
|
|
|
|
-- Notifications
|
|
CREATE INDEX idx_notifications_tenant_id ON system.notifications(tenant_id);
|
|
CREATE INDEX idx_notifications_user_id ON system.notifications(user_id);
|
|
CREATE INDEX idx_notifications_status ON system.notifications(status);
|
|
CREATE INDEX idx_notifications_model_record ON system.notifications(model, record_id);
|
|
CREATE INDEX idx_notifications_created_at ON system.notifications(created_at DESC);
|
|
|
|
-- Activities
|
|
CREATE INDEX idx_activities_tenant_id ON system.activities(tenant_id);
|
|
CREATE INDEX idx_activities_model_record ON system.activities(model, record_id);
|
|
CREATE INDEX idx_activities_assigned_to ON system.activities(assigned_to);
|
|
CREATE INDEX idx_activities_due_date ON system.activities(due_date);
|
|
CREATE INDEX idx_activities_status ON system.activities(status);
|
|
|
|
-- Message Templates
|
|
CREATE INDEX idx_message_templates_tenant_id ON system.message_templates(tenant_id);
|
|
CREATE INDEX idx_message_templates_model ON system.message_templates(model);
|
|
CREATE INDEX idx_message_templates_active ON system.message_templates(active) WHERE active = TRUE;
|
|
|
|
-- Email Queue
|
|
CREATE INDEX idx_email_queue_status ON system.email_queue(status);
|
|
CREATE INDEX idx_email_queue_scheduled_at ON system.email_queue(scheduled_at);
|
|
CREATE INDEX idx_email_queue_created_at ON system.email_queue(created_at);
|
|
|
|
-- Logs
|
|
CREATE INDEX idx_logs_tenant_id ON system.logs(tenant_id);
|
|
CREATE INDEX idx_logs_level ON system.logs(level);
|
|
CREATE INDEX idx_logs_logger ON system.logs(logger);
|
|
CREATE INDEX idx_logs_user_id ON system.logs(user_id);
|
|
CREATE INDEX idx_logs_created_at ON system.logs(created_at DESC);
|
|
CREATE INDEX idx_logs_model_record ON system.logs(model, record_id);
|
|
|
|
-- Reports
|
|
CREATE INDEX idx_reports_tenant_id ON system.reports(tenant_id);
|
|
CREATE INDEX idx_reports_code ON system.reports(code);
|
|
CREATE INDEX idx_reports_active ON system.reports(active) WHERE active = TRUE;
|
|
|
|
-- Report Executions
|
|
CREATE INDEX idx_report_executions_tenant_id ON system.report_executions(tenant_id);
|
|
CREATE INDEX idx_report_executions_report_id ON system.report_executions(report_id);
|
|
CREATE INDEX idx_report_executions_created_by ON system.report_executions(created_by);
|
|
CREATE INDEX idx_report_executions_created_at ON system.report_executions(created_at DESC);
|
|
|
|
-- Dashboards
|
|
CREATE INDEX idx_dashboards_tenant_id ON system.dashboards(tenant_id);
|
|
CREATE INDEX idx_dashboards_user_id ON system.dashboards(user_id);
|
|
CREATE INDEX idx_dashboards_is_public ON system.dashboards(is_public) WHERE is_public = TRUE;
|
|
|
|
-- Dashboard Widgets
|
|
CREATE INDEX idx_dashboard_widgets_dashboard_id ON system.dashboard_widgets(dashboard_id);
|
|
CREATE INDEX idx_dashboard_widgets_type ON system.dashboard_widgets(widget_type);
|
|
|
|
-- =====================================================
|
|
-- FUNCTIONS
|
|
-- =====================================================
|
|
|
|
-- Función: notify_followers
|
|
CREATE OR REPLACE FUNCTION system.notify_followers(
|
|
p_model VARCHAR,
|
|
p_record_id UUID,
|
|
p_message_id UUID
|
|
)
|
|
RETURNS VOID AS $$
|
|
BEGIN
|
|
INSERT INTO system.notifications (tenant_id, user_id, title, message, model, record_id)
|
|
SELECT
|
|
get_current_tenant_id(),
|
|
mf.user_id,
|
|
'New message in ' || p_model,
|
|
m.body,
|
|
p_model,
|
|
p_record_id
|
|
FROM system.message_followers mf
|
|
JOIN system.messages m ON m.id = p_message_id
|
|
WHERE mf.model = p_model
|
|
AND mf.record_id = p_record_id
|
|
AND mf.user_id IS NOT NULL
|
|
AND mf.email_notifications = TRUE;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION system.notify_followers IS 'Notifica a los seguidores de un registro cuando hay un nuevo mensaje';
|
|
|
|
-- Función: mark_activity_as_overdue
|
|
CREATE OR REPLACE FUNCTION system.mark_activities_as_overdue()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_updated_count INTEGER;
|
|
BEGIN
|
|
WITH updated AS (
|
|
UPDATE system.activities
|
|
SET status = 'overdue'
|
|
WHERE status = 'planned'
|
|
AND due_date < CURRENT_DATE
|
|
RETURNING id
|
|
)
|
|
SELECT COUNT(*) INTO v_updated_count FROM updated;
|
|
|
|
RETURN v_updated_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION system.mark_activities_as_overdue IS 'Marca actividades vencidas como overdue (ejecutar diariamente)';
|
|
|
|
-- Función: clean_old_logs
|
|
CREATE OR REPLACE FUNCTION system.clean_old_logs(p_days_to_keep INTEGER DEFAULT 90)
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_deleted_count INTEGER;
|
|
BEGIN
|
|
WITH deleted AS (
|
|
DELETE FROM system.logs
|
|
WHERE created_at < CURRENT_TIMESTAMP - (p_days_to_keep || ' days')::INTERVAL
|
|
AND level != 'critical'
|
|
RETURNING id
|
|
)
|
|
SELECT COUNT(*) INTO v_deleted_count FROM deleted;
|
|
|
|
RETURN v_deleted_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION system.clean_old_logs IS 'Limpia logs antiguos (mantener solo críticos)';
|
|
|
|
-- =====================================================
|
|
-- TRIGGERS
|
|
-- =====================================================
|
|
|
|
CREATE TRIGGER trg_messages_updated_at
|
|
BEFORE UPDATE ON system.messages
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_message_templates_updated_at
|
|
BEFORE UPDATE ON system.message_templates
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_reports_updated_at
|
|
BEFORE UPDATE ON system.reports
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_dashboards_updated_at
|
|
BEFORE UPDATE ON system.dashboards
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_dashboard_widgets_updated_at
|
|
BEFORE UPDATE ON system.dashboard_widgets
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
-- =====================================================
|
|
-- ROW LEVEL SECURITY (RLS)
|
|
-- =====================================================
|
|
|
|
ALTER TABLE system.messages ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE system.notifications ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE system.activities ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE system.message_templates ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE system.logs ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE system.reports ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE system.report_executions ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE system.dashboards ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_messages ON system.messages
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_notifications ON system.notifications
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_activities ON system.activities
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_message_templates ON system.message_templates
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_logs ON system.logs
|
|
USING (tenant_id = get_current_tenant_id() OR tenant_id IS NULL);
|
|
|
|
CREATE POLICY tenant_isolation_reports ON system.reports
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_report_executions ON system.report_executions
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
CREATE POLICY tenant_isolation_dashboards ON system.dashboards
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
-- =====================================================
|
|
-- TRACKING AUTOMÁTICO (mail.thread pattern de Odoo)
|
|
-- =====================================================
|
|
|
|
-- Tabla: field_tracking_config (Configuración de campos a trackear)
|
|
CREATE TABLE system.field_tracking_config (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
table_schema VARCHAR(50) NOT NULL,
|
|
table_name VARCHAR(100) NOT NULL,
|
|
field_name VARCHAR(100) NOT NULL,
|
|
track_changes BOOLEAN NOT NULL DEFAULT true,
|
|
field_type VARCHAR(50) NOT NULL, -- 'text', 'integer', 'numeric', 'boolean', 'uuid', 'timestamp', 'json'
|
|
display_label VARCHAR(255) NOT NULL, -- Para mostrar en UI: "Estado", "Monto", "Cliente", etc.
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT uq_field_tracking UNIQUE (table_schema, table_name, field_name)
|
|
);
|
|
|
|
-- Índice para búsqueda rápida
|
|
CREATE INDEX idx_field_tracking_config_table
|
|
ON system.field_tracking_config(table_schema, table_name);
|
|
|
|
-- Tabla: change_log (Historial de cambios en registros)
|
|
CREATE TABLE system.change_log (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
|
|
|
|
-- Referencia al registro modificado
|
|
table_schema VARCHAR(50) NOT NULL,
|
|
table_name VARCHAR(100) NOT NULL,
|
|
record_id UUID NOT NULL,
|
|
|
|
-- Usuario que hizo el cambio
|
|
changed_by UUID NOT NULL REFERENCES auth.users(id),
|
|
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
-- Tipo de cambio
|
|
change_type VARCHAR(20) NOT NULL CHECK (change_type IN ('create', 'update', 'delete', 'state_change')),
|
|
|
|
-- Campo modificado (NULL para create/delete)
|
|
field_name VARCHAR(100),
|
|
field_label VARCHAR(255), -- Para UI: "Estado", "Monto Total", etc.
|
|
|
|
-- Valores anterior y nuevo
|
|
old_value TEXT,
|
|
new_value TEXT,
|
|
|
|
-- Metadata adicional
|
|
change_context JSONB, -- Info adicional: IP, user agent, módulo, etc.
|
|
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Índices para performance del change_log
|
|
CREATE INDEX idx_change_log_tenant_id ON system.change_log(tenant_id);
|
|
CREATE INDEX idx_change_log_record ON system.change_log(table_schema, table_name, record_id);
|
|
CREATE INDEX idx_change_log_changed_by ON system.change_log(changed_by);
|
|
CREATE INDEX idx_change_log_changed_at ON system.change_log(changed_at DESC);
|
|
CREATE INDEX idx_change_log_type ON system.change_log(change_type);
|
|
|
|
-- Índice compuesto para queries comunes
|
|
CREATE INDEX idx_change_log_record_date
|
|
ON system.change_log(table_schema, table_name, record_id, changed_at DESC);
|
|
|
|
-- RLS Policy para multi-tenancy
|
|
ALTER TABLE system.change_log ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_change_log ON system.change_log
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
-- =====================================================
|
|
-- FUNCIÓN DE TRACKING AUTOMÁTICO
|
|
-- =====================================================
|
|
|
|
-- Función: track_field_changes
|
|
-- Función genérica para trackear cambios automáticamente
|
|
CREATE OR REPLACE FUNCTION system.track_field_changes()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
v_tenant_id UUID;
|
|
v_user_id UUID;
|
|
v_field_name TEXT;
|
|
v_field_label TEXT;
|
|
v_old_value TEXT;
|
|
v_new_value TEXT;
|
|
v_field_config RECORD;
|
|
BEGIN
|
|
-- Obtener tenant_id y user_id del registro
|
|
IF TG_OP = 'DELETE' THEN
|
|
v_tenant_id := OLD.tenant_id;
|
|
v_user_id := OLD.deleted_by;
|
|
ELSE
|
|
v_tenant_id := NEW.tenant_id;
|
|
v_user_id := NEW.updated_by;
|
|
END IF;
|
|
|
|
-- Registrar creación
|
|
IF TG_OP = 'INSERT' THEN
|
|
INSERT INTO system.change_log (
|
|
tenant_id, table_schema, table_name, record_id,
|
|
changed_by, change_type, change_context
|
|
) VALUES (
|
|
v_tenant_id, TG_TABLE_SCHEMA, TG_TABLE_NAME, NEW.id,
|
|
NEW.created_by, 'create',
|
|
jsonb_build_object('operation', 'INSERT')
|
|
);
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
-- Registrar eliminación (soft delete)
|
|
IF TG_OP = 'UPDATE' AND OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
|
INSERT INTO system.change_log (
|
|
tenant_id, table_schema, table_name, record_id,
|
|
changed_by, change_type, change_context
|
|
) VALUES (
|
|
v_tenant_id, TG_TABLE_SCHEMA, TG_TABLE_NAME, NEW.id,
|
|
NEW.deleted_by, 'delete',
|
|
jsonb_build_object('operation', 'SOFT_DELETE', 'deleted_at', NEW.deleted_at)
|
|
);
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
-- Registrar cambios en campos configurados
|
|
IF TG_OP = 'UPDATE' THEN
|
|
-- Iterar sobre campos configurados para esta tabla
|
|
FOR v_field_config IN
|
|
SELECT field_name, display_label, field_type
|
|
FROM system.field_tracking_config
|
|
WHERE table_schema = TG_TABLE_SCHEMA
|
|
AND table_name = TG_TABLE_NAME
|
|
AND track_changes = true
|
|
LOOP
|
|
v_field_name := v_field_config.field_name;
|
|
v_field_label := v_field_config.display_label;
|
|
|
|
-- Obtener valores antiguo y nuevo (usar EXECUTE para campos dinámicos)
|
|
EXECUTE format('SELECT ($1).%I::TEXT, ($2).%I::TEXT', v_field_name, v_field_name)
|
|
INTO v_old_value, v_new_value
|
|
USING OLD, NEW;
|
|
|
|
-- Si el valor cambió, registrarlo
|
|
IF v_old_value IS DISTINCT FROM v_new_value THEN
|
|
INSERT INTO system.change_log (
|
|
tenant_id, table_schema, table_name, record_id,
|
|
changed_by, change_type, field_name, field_label,
|
|
old_value, new_value, change_context
|
|
) VALUES (
|
|
v_tenant_id, TG_TABLE_SCHEMA, TG_TABLE_NAME, NEW.id,
|
|
v_user_id,
|
|
CASE
|
|
WHEN v_field_name = 'status' OR v_field_name = 'state' THEN 'state_change'
|
|
ELSE 'update'
|
|
END,
|
|
v_field_name, v_field_label,
|
|
v_old_value, v_new_value,
|
|
jsonb_build_object('operation', 'UPDATE', 'field_type', v_field_config.field_type)
|
|
);
|
|
END IF;
|
|
END LOOP;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
COMMENT ON FUNCTION system.track_field_changes IS
|
|
'Función trigger para trackear cambios automáticamente según configuración en field_tracking_config (patrón mail.thread de Odoo)';
|
|
|
|
-- =====================================================
|
|
-- SEED DATA: Configuración de campos a trackear
|
|
-- =====================================================
|
|
|
|
-- FINANCIAL: Facturas
|
|
INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES
|
|
('financial', 'invoices', 'status', 'text', 'Estado'),
|
|
('financial', 'invoices', 'partner_id', 'uuid', 'Cliente/Proveedor'),
|
|
('financial', 'invoices', 'invoice_date', 'timestamp', 'Fecha de Factura'),
|
|
('financial', 'invoices', 'amount_total', 'numeric', 'Monto Total'),
|
|
('financial', 'invoices', 'payment_term_id', 'uuid', 'Término de Pago')
|
|
ON CONFLICT (table_schema, table_name, field_name) DO NOTHING;
|
|
|
|
-- FINANCIAL: Asientos contables
|
|
INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES
|
|
('financial', 'journal_entries', 'status', 'text', 'Estado'),
|
|
('financial', 'journal_entries', 'date', 'timestamp', 'Fecha del Asiento'),
|
|
('financial', 'journal_entries', 'journal_id', 'uuid', 'Diario Contable')
|
|
ON CONFLICT (table_schema, table_name, field_name) DO NOTHING;
|
|
|
|
-- PURCHASE: Órdenes de compra
|
|
INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES
|
|
('purchase', 'purchase_orders', 'status', 'text', 'Estado'),
|
|
('purchase', 'purchase_orders', 'partner_id', 'uuid', 'Proveedor'),
|
|
('purchase', 'purchase_orders', 'order_date', 'timestamp', 'Fecha de Orden'),
|
|
('purchase', 'purchase_orders', 'amount_total', 'numeric', 'Monto Total'),
|
|
('purchase', 'purchase_orders', 'receipt_status', 'text', 'Estado de Recepción')
|
|
ON CONFLICT (table_schema, table_name, field_name) DO NOTHING;
|
|
|
|
-- SALES: Órdenes de venta
|
|
INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES
|
|
('sales', 'sales_orders', 'status', 'text', 'Estado'),
|
|
('sales', 'sales_orders', 'partner_id', 'uuid', 'Cliente'),
|
|
('sales', 'sales_orders', 'order_date', 'timestamp', 'Fecha de Orden'),
|
|
('sales', 'sales_orders', 'amount_total', 'numeric', 'Monto Total'),
|
|
('sales', 'sales_orders', 'invoice_status', 'text', 'Estado de Facturación'),
|
|
('sales', 'sales_orders', 'delivery_status', 'text', 'Estado de Entrega')
|
|
ON CONFLICT (table_schema, table_name, field_name) DO NOTHING;
|
|
|
|
-- INVENTORY: Movimientos de stock
|
|
INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES
|
|
('inventory', 'stock_moves', 'status', 'text', 'Estado'),
|
|
('inventory', 'stock_moves', 'product_id', 'uuid', 'Producto'),
|
|
('inventory', 'stock_moves', 'product_qty', 'numeric', 'Cantidad'),
|
|
('inventory', 'stock_moves', 'location_id', 'uuid', 'Ubicación Origen'),
|
|
('inventory', 'stock_moves', 'location_dest_id', 'uuid', 'Ubicación Destino')
|
|
ON CONFLICT (table_schema, table_name, field_name) DO NOTHING;
|
|
|
|
-- PROJECTS: Proyectos
|
|
INSERT INTO system.field_tracking_config (table_schema, table_name, field_name, field_type, display_label) VALUES
|
|
('projects', 'projects', 'status', 'text', 'Estado'),
|
|
('projects', 'projects', 'name', 'text', 'Nombre del Proyecto'),
|
|
('projects', 'projects', 'manager_id', 'uuid', 'Responsable'),
|
|
('projects', 'projects', 'date_start', 'timestamp', 'Fecha de Inicio'),
|
|
('projects', 'projects', 'date_end', 'timestamp', 'Fecha de Fin')
|
|
ON CONFLICT (table_schema, table_name, field_name) DO NOTHING;
|
|
|
|
-- =====================================================
|
|
-- COMENTARIOS
|
|
-- =====================================================
|
|
|
|
COMMENT ON SCHEMA system IS 'Schema de mensajería, notificaciones, logs, reportes y tracking automático';
|
|
COMMENT ON TABLE system.messages IS 'Mensajes del chatter (comentarios, notas, emails)';
|
|
COMMENT ON TABLE system.message_followers IS 'Seguidores de registros para notificaciones';
|
|
COMMENT ON TABLE system.notifications IS 'Notificaciones a usuarios';
|
|
COMMENT ON TABLE system.activities IS 'Actividades programadas (llamadas, reuniones, tareas)';
|
|
COMMENT ON TABLE system.message_templates IS 'Plantillas de mensajes y emails';
|
|
COMMENT ON TABLE system.email_queue IS 'Cola de envío de emails';
|
|
COMMENT ON TABLE system.logs IS 'Logs del sistema y auditoría';
|
|
COMMENT ON TABLE system.reports IS 'Definiciones de reportes';
|
|
COMMENT ON TABLE system.report_executions IS 'Ejecuciones de reportes con resultados';
|
|
COMMENT ON TABLE system.dashboards IS 'Dashboards configurables por usuario';
|
|
COMMENT ON TABLE system.dashboard_widgets IS 'Widgets dentro de dashboards';
|
|
COMMENT ON TABLE system.field_tracking_config IS 'Configuración de campos a trackear automáticamente por tabla (patrón mail.thread de Odoo)';
|
|
COMMENT ON TABLE system.change_log IS 'Historial de cambios en registros (mail.thread pattern de Odoo). Registra automáticamente cambios de estado y campos críticos.';
|
|
|
|
-- =====================================================
|
|
-- FIN DEL SCHEMA SYSTEM
|
|
-- =====================================================
|