erp-construccion-database-v2/schemas/10-documents-schema-ddl.sql
Adrian Flores Cortes efce73bdbd [GAP-006,007] feat: Add DDL for Digital Signatures and Audit Triggers
GAP-006: Digital Signatures
- documents.signature_type ENUM
- documents.signature_status ENUM
- documents.digital_signatures table with RLS

GAP-007: Audit Triggers
- audit schema
- audit.change_log table
- audit.log_changes() trigger function
- Triggers on 13 critical tables

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 01:15:16 -06:00

1030 lines
35 KiB
PL/PgSQL

-- ============================================================================
-- 10-documents-schema-ddl.sql
-- Schema: documents
-- ERP Construccion - Modulo Gestion Documental (MAE-016)
-- ============================================================================
-- Descripcion: Sistema de gestion documental incluyendo:
-- - Repositorio de documentos estructurado
-- - Control de versiones
-- - Flujos de aprobacion
-- - Permisos granulares
-- - Anotaciones sobre planos
-- - Integracion con almacenamiento externo (S3)
-- ============================================================================
-- Autor: Claude-Arquitecto-Orquestador
-- Fecha: 2026-01-25
-- Version: 1.0.0
-- ============================================================================
-- Crear schema si no existe
CREATE SCHEMA IF NOT EXISTS documents;
-- ============================================================================
-- ENUMS
-- ============================================================================
-- Tipo de documento
CREATE TYPE documents.document_type AS ENUM (
'plan', -- Planos arquitectonicos/estructurales
'specification', -- Especificaciones tecnicas
'contract', -- Contratos
'permit', -- Permisos y licencias
'report', -- Reportes
'photograph', -- Fotografias
'drawing', -- Dibujos y croquis
'manual', -- Manuales
'procedure', -- Procedimientos
'form', -- Formatos
'correspondence', -- Correspondencia
'invoice', -- Facturas
'estimate', -- Estimaciones/Presupuestos
'other' -- Otro
);
-- Estado del documento
CREATE TYPE documents.document_status AS ENUM (
'draft', -- Borrador
'pending_review', -- Pendiente de revision
'in_review', -- En revision
'approved', -- Aprobado
'rejected', -- Rechazado
'obsolete', -- Obsoleto
'archived' -- Archivado
);
-- Nivel de acceso
CREATE TYPE documents.access_level AS ENUM (
'public', -- Publico (todos en el tenant)
'internal', -- Interno (empleados)
'confidential', -- Confidencial
'restricted' -- Restringido (solo autorizados)
);
-- Estado de version
CREATE TYPE documents.version_status AS ENUM (
'current', -- Version actual
'superseded', -- Reemplazada
'archived' -- Archivada
);
-- Estado de flujo de aprobacion
CREATE TYPE documents.workflow_status AS ENUM (
'draft', -- Borrador
'pending', -- Pendiente
'in_progress', -- En progreso
'approved', -- Aprobado
'rejected', -- Rechazado
'cancelled' -- Cancelado
);
-- Tipo de paso de aprobacion
CREATE TYPE documents.approval_step_type AS ENUM (
'review', -- Revision
'approval', -- Aprobacion
'signature', -- Firma
'comment' -- Solo comentario
);
-- Accion de aprobacion
CREATE TYPE documents.approval_action AS ENUM (
'approve', -- Aprobar
'reject', -- Rechazar
'request_changes' -- Solicitar cambios
);
-- Tipo de anotacion
CREATE TYPE documents.annotation_type AS ENUM (
'comment', -- Comentario
'markup', -- Marcado
'highlight', -- Resaltado
'arrow', -- Flecha
'dimension', -- Dimension
'stamp', -- Sello
'signature' -- Firma
);
-- ============================================================================
-- TABLAS
-- ============================================================================
-- ----------------------------------------------------------------------------
-- 1. Categorias de Documentos
-- ----------------------------------------------------------------------------
CREATE TABLE documents.document_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Informacion basica
code VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
description TEXT,
-- Jerarquia
parent_id UUID REFERENCES documents.document_categories(id),
level INTEGER NOT NULL DEFAULT 1,
path VARCHAR(500), -- Path completo separado por /
-- Configuracion
default_access_level documents.access_level DEFAULT 'internal',
requires_approval BOOLEAN DEFAULT FALSE,
retention_days INTEGER, -- Tiempo de retencion en dias
allowed_extensions VARCHAR(255)[], -- Extensiones permitidas
-- Icono y color
icon VARCHAR(50),
color VARCHAR(20),
-- Estado
is_active BOOLEAN NOT NULL DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
-- Metadatos
metadata JSONB,
-- Auditoria
created_by UUID,
updated_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uq_document_categories_tenant_code UNIQUE (tenant_id, code)
);
-- ----------------------------------------------------------------------------
-- 2. Documentos (Registro Principal)
-- ----------------------------------------------------------------------------
CREATE TABLE documents.documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Identificacion
document_code VARCHAR(100) NOT NULL,
title VARCHAR(500) NOT NULL,
description TEXT,
-- Clasificacion
category_id UUID REFERENCES documents.document_categories(id),
document_type documents.document_type NOT NULL,
status documents.document_status NOT NULL DEFAULT 'draft',
access_level documents.access_level NOT NULL DEFAULT 'internal',
-- Version actual
current_version_id UUID, -- Se actualiza con trigger
current_version_number VARCHAR(20) DEFAULT '1.0',
-- Proyecto/Contexto
project_id UUID,
project_code VARCHAR(50),
project_name VARCHAR(255),
-- Metadatos del documento
author VARCHAR(255),
keywords VARCHAR(255)[],
tags VARCHAR(100)[],
-- Fechas importantes
document_date DATE,
effective_date DATE,
expiry_date DATE,
review_date DATE,
-- Origen
source VARCHAR(100), -- internal, external, contractor
external_reference VARCHAR(255), -- Referencia externa si aplica
original_filename VARCHAR(500),
-- Relaciones
parent_document_id UUID REFERENCES documents.documents(id), -- Documento padre
related_documents UUID[], -- Array de IDs relacionados
-- Flujo de aprobacion
requires_approval BOOLEAN DEFAULT FALSE,
current_workflow_id UUID,
approved_by_id UUID,
approved_at TIMESTAMPTZ,
-- Estadisticas
view_count INTEGER DEFAULT 0,
download_count INTEGER DEFAULT 0,
last_accessed_at TIMESTAMPTZ,
-- Flags
is_template BOOLEAN DEFAULT FALSE,
is_locked BOOLEAN DEFAULT FALSE,
locked_by_id UUID,
locked_at TIMESTAMPTZ,
-- Notas y metadatos
notes TEXT,
custom_fields JSONB,
metadata JSONB,
-- Auditoria
created_by UUID,
updated_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uq_documents_tenant_code UNIQUE (tenant_id, document_code)
);
-- ----------------------------------------------------------------------------
-- 3. Versiones de Documentos
-- ----------------------------------------------------------------------------
CREATE TABLE documents.document_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Documento padre
document_id UUID NOT NULL REFERENCES documents.documents(id) ON DELETE CASCADE,
-- Version
version_number VARCHAR(20) NOT NULL,
version_label VARCHAR(100), -- Etiqueta opcional (ej: "Final", "Para construccion")
status documents.version_status NOT NULL DEFAULT 'current',
-- Archivo
file_name VARCHAR(500) NOT NULL,
file_extension VARCHAR(20) NOT NULL,
file_size_bytes BIGINT NOT NULL,
mime_type VARCHAR(100),
checksum_md5 VARCHAR(32),
checksum_sha256 VARCHAR(64),
-- Almacenamiento
storage_provider VARCHAR(50) NOT NULL DEFAULT 's3', -- s3, azure, local
storage_bucket VARCHAR(255),
storage_key VARCHAR(1000) NOT NULL, -- Path en el storage
storage_url VARCHAR(2000), -- URL de acceso
thumbnail_url VARCHAR(2000),
preview_url VARCHAR(2000),
-- Procesamiento
is_processed BOOLEAN DEFAULT FALSE,
ocr_text TEXT, -- Texto extraido por OCR
extracted_metadata JSONB,
-- Paginas (para PDFs/planos)
page_count INTEGER,
page_dimensions JSONB, -- Dimensiones por pagina
-- Cambios
change_summary TEXT,
change_type VARCHAR(50), -- major, minor, patch
-- Subido por
uploaded_by_id UUID,
uploaded_by_name VARCHAR(255),
upload_source VARCHAR(50), -- web, mobile, api, email
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
superseded_at TIMESTAMPTZ,
superseded_by_version_id UUID
);
-- ----------------------------------------------------------------------------
-- 4. Permisos de Documentos
-- ----------------------------------------------------------------------------
CREATE TABLE documents.document_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Objeto
document_id UUID REFERENCES documents.documents(id) ON DELETE CASCADE,
category_id UUID REFERENCES documents.document_categories(id) ON DELETE CASCADE,
-- Sujeto (quien tiene el permiso)
user_id UUID,
role_id UUID,
team_id UUID,
-- Permisos
can_view BOOLEAN DEFAULT FALSE,
can_download BOOLEAN DEFAULT FALSE,
can_edit BOOLEAN DEFAULT FALSE,
can_delete BOOLEAN DEFAULT FALSE,
can_share BOOLEAN DEFAULT FALSE,
can_approve BOOLEAN DEFAULT FALSE,
can_annotate BOOLEAN DEFAULT FALSE,
-- Vigencia
valid_from TIMESTAMPTZ,
valid_until TIMESTAMPTZ,
-- Otorgado por
granted_by_id UUID,
granted_at TIMESTAMPTZ DEFAULT NOW(),
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_permission_target CHECK (
(document_id IS NOT NULL AND category_id IS NULL) OR
(document_id IS NULL AND category_id IS NOT NULL)
),
CONSTRAINT chk_permission_subject CHECK (
(user_id IS NOT NULL AND role_id IS NULL AND team_id IS NULL) OR
(user_id IS NULL AND role_id IS NOT NULL AND team_id IS NULL) OR
(user_id IS NULL AND role_id IS NULL AND team_id IS NOT NULL)
)
);
-- ----------------------------------------------------------------------------
-- 5. Flujos de Aprobacion (Definicion)
-- ----------------------------------------------------------------------------
CREATE TABLE documents.approval_workflows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Identificacion
workflow_code VARCHAR(50) NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
-- Aplica a
category_id UUID REFERENCES documents.document_categories(id),
document_type documents.document_type,
-- Pasos (JSON array con la definicion)
steps JSONB NOT NULL, -- [{step_number, name, type, approvers[], required_count}]
-- Configuracion
allow_parallel BOOLEAN DEFAULT FALSE, -- Pasos pueden ser paralelos
allow_skip BOOLEAN DEFAULT FALSE, -- Permitir saltar pasos
auto_archive_on_approval BOOLEAN DEFAULT FALSE,
-- Estado
is_active BOOLEAN NOT NULL DEFAULT TRUE,
-- Metadatos
metadata JSONB,
-- Auditoria
created_by UUID,
updated_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uq_approval_workflows_tenant_code UNIQUE (tenant_id, workflow_code)
);
-- ----------------------------------------------------------------------------
-- 6. Instancias de Flujo de Aprobacion
-- ----------------------------------------------------------------------------
CREATE TABLE documents.approval_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Flujo y documento
workflow_id UUID NOT NULL REFERENCES documents.approval_workflows(id),
document_id UUID NOT NULL REFERENCES documents.documents(id),
version_id UUID REFERENCES documents.document_versions(id),
-- Estado
status documents.workflow_status NOT NULL DEFAULT 'draft',
current_step INTEGER DEFAULT 1,
total_steps INTEGER NOT NULL,
-- Fechas
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
due_date TIMESTAMPTZ,
-- Iniciador
initiated_by_id UUID,
initiated_by_name VARCHAR(255),
-- Resultado final
final_action documents.approval_action,
final_comments TEXT,
final_approver_id UUID,
-- Notas
notes TEXT,
metadata JSONB,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ----------------------------------------------------------------------------
-- 7. Pasos de Aprobacion (Instancias)
-- ----------------------------------------------------------------------------
CREATE TABLE documents.approval_steps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Instancia
instance_id UUID NOT NULL REFERENCES documents.approval_instances(id) ON DELETE CASCADE,
-- Paso
step_number INTEGER NOT NULL,
step_name VARCHAR(255) NOT NULL,
step_type documents.approval_step_type NOT NULL,
-- Aprobadores requeridos
required_approvers UUID[], -- IDs de usuarios requeridos
required_count INTEGER DEFAULT 1, -- Cuantas aprobaciones se necesitan
-- Estado
status documents.workflow_status NOT NULL DEFAULT 'pending',
-- Fechas
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
due_date TIMESTAMPTZ,
-- Resultado
action_taken documents.approval_action,
approved_by UUID[],
rejected_by UUID[],
-- Notas
notes TEXT,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ----------------------------------------------------------------------------
-- 8. Acciones de Aprobacion
-- ----------------------------------------------------------------------------
CREATE TABLE documents.approval_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Paso
step_id UUID NOT NULL REFERENCES documents.approval_steps(id) ON DELETE CASCADE,
instance_id UUID NOT NULL REFERENCES documents.approval_instances(id),
-- Accion
action documents.approval_action NOT NULL,
comments TEXT,
-- Usuario que toma la accion
user_id UUID NOT NULL,
user_name VARCHAR(255),
-- Firma digital (si aplica)
signature_data TEXT,
signature_timestamp TIMESTAMPTZ,
signature_ip VARCHAR(45),
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ----------------------------------------------------------------------------
-- 9. Anotaciones sobre Documentos
-- ----------------------------------------------------------------------------
CREATE TABLE documents.annotations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Documento y version
document_id UUID NOT NULL REFERENCES documents.documents(id) ON DELETE CASCADE,
version_id UUID REFERENCES documents.document_versions(id),
-- Tipo
annotation_type documents.annotation_type NOT NULL,
-- Ubicacion en el documento
page_number INTEGER,
x_position DECIMAL(10,4),
y_position DECIMAL(10,4),
width DECIMAL(10,4),
height DECIMAL(10,4),
rotation DECIMAL(5,2),
-- Contenido
content TEXT, -- Texto del comentario o SVG del dibujo
style JSONB, -- Estilos (color, grosor, fuente, etc.)
-- Respuesta a otra anotacion
parent_annotation_id UUID REFERENCES documents.annotations(id),
-- Estado
is_resolved BOOLEAN DEFAULT FALSE,
resolved_by_id UUID,
resolved_at TIMESTAMPTZ,
-- Visibilidad
is_private BOOLEAN DEFAULT FALSE,
visible_to UUID[], -- Array de user_ids si es privado
-- Autor
author_id UUID NOT NULL,
author_name VARCHAR(255),
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- ----------------------------------------------------------------------------
-- 10. Historial de Acceso a Documentos
-- ----------------------------------------------------------------------------
CREATE TABLE documents.access_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Documento
document_id UUID NOT NULL REFERENCES documents.documents(id),
version_id UUID REFERENCES documents.document_versions(id),
-- Accion
action VARCHAR(50) NOT NULL, -- view, download, print, share, edit
action_details JSONB,
-- Usuario
user_id UUID NOT NULL,
user_name VARCHAR(255),
user_ip VARCHAR(45),
user_agent TEXT,
-- Contexto
session_id VARCHAR(100),
request_source VARCHAR(50), -- web, mobile, api
-- Timestamp
accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ----------------------------------------------------------------------------
-- 11. Compartidos Externos
-- ----------------------------------------------------------------------------
CREATE TABLE documents.document_shares (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Documento
document_id UUID NOT NULL REFERENCES documents.documents(id) ON DELETE CASCADE,
version_id UUID REFERENCES documents.document_versions(id),
-- Link compartido
share_token VARCHAR(100) NOT NULL UNIQUE,
share_url VARCHAR(2000),
-- Destinatario
shared_with_email VARCHAR(255),
shared_with_name VARCHAR(255),
-- Permisos
can_download BOOLEAN DEFAULT FALSE,
can_comment BOOLEAN DEFAULT FALSE,
-- Seguridad
password_hash VARCHAR(255), -- Si requiere password
max_downloads INTEGER,
download_count INTEGER DEFAULT 0,
-- Vigencia
expires_at TIMESTAMPTZ,
is_revoked BOOLEAN DEFAULT FALSE,
revoked_at TIMESTAMPTZ,
revoked_by_id UUID,
-- Compartido por
shared_by_id UUID NOT NULL,
shared_by_name VARCHAR(255),
-- Notificacion
notification_sent BOOLEAN DEFAULT FALSE,
notification_sent_at TIMESTAMPTZ,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_accessed_at TIMESTAMPTZ
);
-- ============================================================================
-- INDICES
-- ============================================================================
-- Document Categories
CREATE INDEX idx_document_categories_tenant ON documents.document_categories(tenant_id);
CREATE INDEX idx_document_categories_parent ON documents.document_categories(parent_id);
-- Documents
CREATE INDEX idx_documents_tenant ON documents.documents(tenant_id);
CREATE INDEX idx_documents_tenant_category ON documents.documents(tenant_id, category_id);
CREATE INDEX idx_documents_tenant_type ON documents.documents(tenant_id, document_type);
CREATE INDEX idx_documents_tenant_status ON documents.documents(tenant_id, status);
CREATE INDEX idx_documents_tenant_project ON documents.documents(tenant_id, project_id);
CREATE INDEX idx_documents_keywords ON documents.documents USING gin(keywords);
CREATE INDEX idx_documents_tags ON documents.documents USING gin(tags);
CREATE INDEX idx_documents_fulltext ON documents.documents USING gin(
to_tsvector('spanish', coalesce(title, '') || ' ' || coalesce(description, ''))
);
-- Document Versions
CREATE INDEX idx_document_versions_tenant ON documents.document_versions(tenant_id);
CREATE INDEX idx_document_versions_document ON documents.document_versions(document_id);
CREATE INDEX idx_document_versions_status ON documents.document_versions(status);
-- Document Permissions
CREATE INDEX idx_document_permissions_document ON documents.document_permissions(document_id);
CREATE INDEX idx_document_permissions_category ON documents.document_permissions(category_id);
CREATE INDEX idx_document_permissions_user ON documents.document_permissions(user_id);
CREATE INDEX idx_document_permissions_role ON documents.document_permissions(role_id);
-- Approval Workflows
CREATE INDEX idx_approval_workflows_tenant ON documents.approval_workflows(tenant_id);
CREATE INDEX idx_approval_workflows_category ON documents.approval_workflows(category_id);
-- Approval Instances
CREATE INDEX idx_approval_instances_tenant ON documents.approval_instances(tenant_id);
CREATE INDEX idx_approval_instances_document ON documents.approval_instances(document_id);
CREATE INDEX idx_approval_instances_status ON documents.approval_instances(status);
-- Approval Steps
CREATE INDEX idx_approval_steps_instance ON documents.approval_steps(instance_id);
CREATE INDEX idx_approval_steps_status ON documents.approval_steps(status);
-- Approval Actions
CREATE INDEX idx_approval_actions_step ON documents.approval_actions(step_id);
CREATE INDEX idx_approval_actions_user ON documents.approval_actions(user_id);
-- Annotations
CREATE INDEX idx_annotations_tenant ON documents.annotations(tenant_id);
CREATE INDEX idx_annotations_document ON documents.annotations(document_id);
CREATE INDEX idx_annotations_version ON documents.annotations(version_id);
CREATE INDEX idx_annotations_author ON documents.annotations(author_id);
-- Access Logs
CREATE INDEX idx_access_logs_tenant ON documents.access_logs(tenant_id);
CREATE INDEX idx_access_logs_document ON documents.access_logs(document_id);
CREATE INDEX idx_access_logs_user ON documents.access_logs(user_id);
CREATE INDEX idx_access_logs_accessed ON documents.access_logs(accessed_at);
-- Document Shares
CREATE INDEX idx_document_shares_tenant ON documents.document_shares(tenant_id);
CREATE INDEX idx_document_shares_document ON documents.document_shares(document_id);
CREATE INDEX idx_document_shares_token ON documents.document_shares(share_token);
-- ============================================================================
-- ROW LEVEL SECURITY (RLS)
-- ============================================================================
ALTER TABLE documents.document_categories ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents.documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents.document_versions ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents.document_permissions ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents.approval_workflows ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents.approval_instances ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents.approval_steps ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents.approval_actions ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents.annotations ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents.access_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents.document_shares ENABLE ROW LEVEL SECURITY;
-- ============================================================================
-- TRIGGERS DE AUDITORIA
-- ============================================================================
CREATE OR REPLACE FUNCTION documents.set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_document_categories_updated_at
BEFORE UPDATE ON documents.document_categories
FOR EACH ROW EXECUTE FUNCTION documents.set_updated_at();
CREATE TRIGGER trg_documents_updated_at
BEFORE UPDATE ON documents.documents
FOR EACH ROW EXECUTE FUNCTION documents.set_updated_at();
CREATE TRIGGER trg_document_permissions_updated_at
BEFORE UPDATE ON documents.document_permissions
FOR EACH ROW EXECUTE FUNCTION documents.set_updated_at();
CREATE TRIGGER trg_approval_workflows_updated_at
BEFORE UPDATE ON documents.approval_workflows
FOR EACH ROW EXECUTE FUNCTION documents.set_updated_at();
CREATE TRIGGER trg_approval_instances_updated_at
BEFORE UPDATE ON documents.approval_instances
FOR EACH ROW EXECUTE FUNCTION documents.set_updated_at();
CREATE TRIGGER trg_approval_steps_updated_at
BEFORE UPDATE ON documents.approval_steps
FOR EACH ROW EXECUTE FUNCTION documents.set_updated_at();
CREATE TRIGGER trg_annotations_updated_at
BEFORE UPDATE ON documents.annotations
FOR EACH ROW EXECUTE FUNCTION documents.set_updated_at();
-- ============================================================================
-- FUNCIONES AUXILIARES
-- ============================================================================
-- Actualizar version actual del documento
CREATE OR REPLACE FUNCTION documents.update_current_version()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.status = 'current' THEN
-- Marcar versiones anteriores como superseded
UPDATE documents.document_versions
SET status = 'superseded',
superseded_at = NOW(),
superseded_by_version_id = NEW.id
WHERE document_id = NEW.document_id
AND id != NEW.id
AND status = 'current';
-- Actualizar documento con version actual
UPDATE documents.documents
SET current_version_id = NEW.id,
current_version_number = NEW.version_number,
updated_at = NOW()
WHERE id = NEW.document_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_update_current_version
AFTER INSERT OR UPDATE ON documents.document_versions
FOR EACH ROW EXECUTE FUNCTION documents.update_current_version();
-- Incrementar contador de vistas
CREATE OR REPLACE FUNCTION documents.increment_view_count()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.action = 'view' THEN
UPDATE documents.documents
SET view_count = view_count + 1,
last_accessed_at = NOW()
WHERE id = NEW.document_id;
ELSIF NEW.action = 'download' THEN
UPDATE documents.documents
SET download_count = download_count + 1,
last_accessed_at = NOW()
WHERE id = NEW.document_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_increment_view_count
AFTER INSERT ON documents.access_logs
FOR EACH ROW EXECUTE FUNCTION documents.increment_view_count();
-- Actualizar path de categoria
CREATE OR REPLACE FUNCTION documents.update_category_path()
RETURNS TRIGGER AS $$
DECLARE
v_parent_path VARCHAR(500);
BEGIN
IF NEW.parent_id IS NULL THEN
NEW.path := '/' || NEW.code;
NEW.level := 1;
ELSE
SELECT path, level + 1 INTO v_parent_path, NEW.level
FROM documents.document_categories
WHERE id = NEW.parent_id;
NEW.path := v_parent_path || '/' || NEW.code;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_update_category_path
BEFORE INSERT OR UPDATE ON documents.document_categories
FOR EACH ROW EXECUTE FUNCTION documents.update_category_path();
-- ============================================================================
-- COMENTARIOS DE DOCUMENTACION
-- ============================================================================
COMMENT ON SCHEMA documents IS 'Modulo de Gestion Documental (MAE-016) - ERP Construccion';
COMMENT ON TABLE documents.document_categories IS 'Categorias jerarquicas para clasificar documentos';
COMMENT ON TABLE documents.documents IS 'Registro principal de documentos';
COMMENT ON TABLE documents.document_versions IS 'Versiones de documentos con archivos';
COMMENT ON TABLE documents.document_permissions IS 'Permisos granulares por documento o categoria';
COMMENT ON TABLE documents.approval_workflows IS 'Definicion de flujos de aprobacion';
COMMENT ON TABLE documents.approval_instances IS 'Instancias de flujos de aprobacion activos';
COMMENT ON TABLE documents.approval_steps IS 'Pasos de aprobacion por instancia';
COMMENT ON TABLE documents.approval_actions IS 'Acciones tomadas en pasos de aprobacion';
COMMENT ON TABLE documents.annotations IS 'Anotaciones y comentarios sobre documentos';
COMMENT ON TABLE documents.access_logs IS 'Historial de acceso a documentos';
COMMENT ON TABLE documents.document_shares IS 'Links de compartido externo';
-- ============================================================================
-- FK CONSTRAINTS ADICIONALES
-- NOTA: Fortalecer referencias internas y externas del schema documents.
-- ============================================================================
-- FK: documents.parent_document_id → documents.documents (auto-referencia)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_documents_parent'
) THEN
ALTER TABLE documents.documents
ADD CONSTRAINT fk_documents_parent
FOREIGN KEY (parent_document_id) REFERENCES documents.documents(id)
ON DELETE SET NULL;
RAISE NOTICE 'FK creada: documents.parent_document_id → documents.documents';
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'FK documents.parent_document_id no creada: %', SQLERRM;
END $$;
-- FK: access_logs.document_id → documents.documents (para mantener logs aunque doc se elimine)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_access_logs_document'
) THEN
ALTER TABLE documents.access_logs
ADD CONSTRAINT fk_access_logs_document
FOREIGN KEY (document_id) REFERENCES documents.documents(id)
ON DELETE CASCADE;
RAISE NOTICE 'FK creada: access_logs.document_id → documents.documents';
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'FK access_logs.document_id no creada: %', SQLERRM;
END $$;
-- FK: access_logs.user_id → auth.users
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users') THEN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_access_logs_user'
) THEN
ALTER TABLE documents.access_logs
ADD CONSTRAINT fk_access_logs_user
FOREIGN KEY (user_id) REFERENCES auth.users(id)
ON DELETE SET NULL;
RAISE NOTICE 'FK creada: access_logs.user_id → auth.users';
END IF;
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'FK access_logs.user_id no creada: %', SQLERRM;
END $$;
-- FK: document_shares.shared_by_id → auth.users
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users') THEN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_document_shares_shared_by'
) THEN
ALTER TABLE documents.document_shares
ADD CONSTRAINT fk_document_shares_shared_by
FOREIGN KEY (shared_by_id) REFERENCES auth.users(id)
ON DELETE SET NULL;
RAISE NOTICE 'FK creada: document_shares.shared_by_id → auth.users';
END IF;
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'FK document_shares.shared_by_id no creada: %', SQLERRM;
END $$;
-- Índices adicionales para campos sin índice
CREATE INDEX IF NOT EXISTS idx_documents_parent ON documents.documents(parent_document_id);
CREATE INDEX IF NOT EXISTS idx_document_shares_shared_by ON documents.document_shares(shared_by_id);
-- ============================================================================
-- GAP-006: FIRMAS DIGITALES
-- ============================================================================
-- Tipo de firma digital
CREATE TYPE documents.signature_type AS ENUM (
'simple', -- Firma simple (click to sign)
'advanced', -- Firma avanzada (con certificado)
'qualified' -- Firma cualificada (certificado oficial)
);
-- Estado de la firma
CREATE TYPE documents.signature_status AS ENUM (
'pending', -- Pendiente de firma
'signed', -- Firmado
'rejected', -- Rechazado
'expired', -- Expirado
'revoked' -- Revocado
);
-- Tabla de firmas digitales
CREATE TABLE IF NOT EXISTS documents.digital_signatures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Documento firmado
document_id UUID NOT NULL,
version_id UUID,
-- Firmante
signer_id UUID NOT NULL,
signer_name VARCHAR(200) NOT NULL,
signer_email VARCHAR(255),
signer_role VARCHAR(100),
-- Tipo y estado
signature_type documents.signature_type NOT NULL DEFAULT 'simple',
status documents.signature_status NOT NULL DEFAULT 'pending',
-- Datos de firma
signature_data TEXT,
signature_hash VARCHAR(128),
hash_algorithm VARCHAR(20) DEFAULT 'SHA-256',
-- Certificado (para firmas avanzadas/cualificadas)
certificate_info JSONB,
certificate_serial VARCHAR(100),
certificate_issuer VARCHAR(255),
certificate_valid_from TIMESTAMPTZ,
certificate_valid_to TIMESTAMPTZ,
-- Timestamps
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
signed_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
-- Contexto de firma
ip_address INET,
user_agent TEXT,
geolocation JSONB,
-- Validación
is_valid BOOLEAN DEFAULT true,
validation_errors JSONB,
last_validated_at TIMESTAMPTZ,
-- Razón (para rechazo/revocación)
reason TEXT,
-- Metadatos
metadata JSONB DEFAULT '{}',
-- Auditoría
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Foreign keys
CONSTRAINT fk_digital_signatures_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_digital_signatures_document
FOREIGN KEY (document_id) REFERENCES documents.documents(id) ON DELETE CASCADE,
CONSTRAINT fk_digital_signatures_version
FOREIGN KEY (version_id) REFERENCES documents.document_versions(id) ON DELETE SET NULL
);
-- Índices para digital_signatures
CREATE INDEX IF NOT EXISTS idx_digital_signatures_tenant ON documents.digital_signatures(tenant_id);
CREATE INDEX IF NOT EXISTS idx_digital_signatures_document ON documents.digital_signatures(document_id);
CREATE INDEX IF NOT EXISTS idx_digital_signatures_signer ON documents.digital_signatures(signer_id);
CREATE INDEX IF NOT EXISTS idx_digital_signatures_status ON documents.digital_signatures(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_digital_signatures_signed_at ON documents.digital_signatures(signed_at);
-- RLS para digital_signatures
ALTER TABLE documents.digital_signatures ENABLE ROW LEVEL SECURITY;
CREATE POLICY digital_signatures_tenant_isolation ON documents.digital_signatures
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- FK opcional: signer_id → auth.users
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users') THEN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_digital_signatures_signer'
) THEN
ALTER TABLE documents.digital_signatures
ADD CONSTRAINT fk_digital_signatures_signer
FOREIGN KEY (signer_id) REFERENCES auth.users(id)
ON DELETE SET NULL;
RAISE NOTICE 'FK creada: digital_signatures.signer_id → auth.users';
END IF;
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'FK digital_signatures.signer_id no creada: %', SQLERRM;
END $$;
COMMENT ON TABLE documents.digital_signatures IS 'GAP-006: Firmas digitales para documentos';
-- ============================================================================
-- FIN DEL SCRIPT
-- FK constraints adicionales: 5 (incluyendo digital_signatures)
-- ============================================================================