552 lines
29 KiB
SQL
552 lines
29 KiB
SQL
-- =============================================================================
|
|
-- OMNIOIL — CONSOLIDATED MASTER SCHEMA (v2026-05-03)
|
|
-- =============================================================================
|
|
-- Esquema único consolidado. Reemplaza todas las migraciones anteriores.
|
|
-- Ejecutar una sola vez en una BD limpia.
|
|
-- =============================================================================
|
|
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 1: SEGURIDAD Y ACCESO
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS roles (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(50) UNIQUE NOT NULL,
|
|
permissions JSONB NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
INSERT INTO roles (name, permissions) VALUES
|
|
('superadmin', '{"SYNC_WRITE": true, "DATA_READ": true, "CAMPO_READ": true, "USERS_ADMIN": true}'::jsonb),
|
|
('admin', '{"SYNC_WRITE": true, "DATA_READ": true, "CAMPO_READ": true, "USERS_ADMIN": true}'::jsonb),
|
|
('supervisor', '{"SYNC_WRITE": true, "DATA_READ": true, "CAMPO_READ": true, "USERS_ADMIN": false}'::jsonb),
|
|
('operador', '{"SYNC_WRITE": true, "DATA_READ": true, "CAMPO_READ": false, "USERS_ADMIN": false}'::jsonb),
|
|
('anh_reader', '{"DATA_READ": true, "CAMPO_READ": true}'::jsonb)
|
|
ON CONFLICT (name) DO NOTHING;
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
email VARCHAR(100) UNIQUE NOT NULL,
|
|
full_name VARCHAR(200),
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
role_id INT NOT NULL REFERENCES roles(id),
|
|
two_factor_secret TEXT,
|
|
two_factor_enabled BOOLEAN DEFAULT false,
|
|
is_active BOOLEAN DEFAULT true,
|
|
last_login TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
revoked_at TIMESTAMPTZ
|
|
);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 2: JERARQUÍA OPERATIVA Y CUMPLIMIENTO ANH
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS edge_projects (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
name VARCHAR(100) NOT NULL UNIQUE,
|
|
client VARCHAR(150) NOT NULL,
|
|
operator_name VARCHAR(150) NOT NULL,
|
|
contract_number VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
municipality VARCHAR(100),
|
|
department VARCHAR(100),
|
|
latitude DOUBLE PRECISION,
|
|
longitude DOUBLE PRECISION,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
is_active BOOLEAN DEFAULT true,
|
|
installer_status VARCHAR(50) DEFAULT 'NOT_STARTED',
|
|
installer_url TEXT,
|
|
mqtt_password TEXT,
|
|
agent_master_secret TEXT,
|
|
agent_config_toml TEXT, -- Config TOML del agente nativo (server-side)
|
|
last_health_report JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
CONSTRAINT check_installer_status CHECK (installer_status IN ('NOT_STARTED', 'QUEUED', 'BUILDING', 'READY', 'COMPLETED', 'FAILED'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_project_access (
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
project_role VARCHAR(50) DEFAULT 'viewer',
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
PRIMARY KEY (user_id, project_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS edge_assets (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
code VARCHAR(50) UNIQUE NOT NULL,
|
|
name VARCHAR(100) NOT NULL,
|
|
asset_type VARCHAR(20) NOT NULL CHECK (asset_type IN ('POZO', 'TANQUE', 'MEDIDOR_GAS', 'AGUA', 'OTRO')),
|
|
last_calibration_date DATE,
|
|
next_calibration_date DATE,
|
|
calibration_certificate_url TEXT,
|
|
external_api_url TEXT, -- URL de API externa para integración multitenant
|
|
external_api_key TEXT, -- API key para autenticación externa
|
|
is_collector_enabled BOOLEAN DEFAULT FALSE, -- Activa el data-collector para este asset
|
|
is_active BOOLEAN DEFAULT true,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS edge_devices (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
asset_id UUID NOT NULL REFERENCES edge_assets(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
protocol VARCHAR(30) NOT NULL CHECK (protocol IN ('MODBUS_TCP', 'MODBUS_RTU', 'OPC_UA', 'MQTT', 'ETHERNET_IP', 'PROFINET', 'S7')),
|
|
host VARCHAR(255) NOT NULL,
|
|
port INTEGER NOT NULL,
|
|
protocol_config JSONB DEFAULT '{}'::jsonb,
|
|
is_enabled BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS edge_variables (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
device_id UUID NOT NULL REFERENCES edge_devices(id) ON DELETE CASCADE,
|
|
address VARCHAR(255) NOT NULL,
|
|
data_type VARCHAR(30) NOT NULL CHECK (data_type IN ('bool', 'int16', 'uint16', 'int32', 'uint32', 'float32', 'float64', 'string')),
|
|
alias VARCHAR(100) NOT NULL,
|
|
unit VARCHAR(30),
|
|
polling_interval_ms INTEGER DEFAULT 5000,
|
|
byte_order VARCHAR(10) DEFAULT 'BE' CHECK (byte_order IN ('BE', 'LE', 'BE_SWAP', 'LE_SWAP')),
|
|
alarm_min DOUBLE PRECISION,
|
|
alarm_max DOUBLE PRECISION,
|
|
alarm_enabled BOOLEAN DEFAULT false,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
is_enabled BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 3: TELEMETRÍA (TimescaleDB)
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS telemetry_raw (
|
|
time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
asset_id UUID REFERENCES edge_assets(id) ON DELETE CASCADE,
|
|
data JSONB NOT NULL
|
|
);
|
|
SELECT create_hypertable('telemetry_raw', 'time', if_not_exists => TRUE);
|
|
|
|
-- Compresión: datos SCADA con alta repetitividad → 90-97% de ahorro en disco
|
|
ALTER TABLE telemetry_raw SET (
|
|
timescaledb.compress,
|
|
timescaledb.compress_orderby = 'time DESC',
|
|
timescaledb.compress_segmentby = 'project_id, asset_id'
|
|
);
|
|
SELECT add_compression_policy('telemetry_raw', INTERVAL '7 days', if_not_exists => TRUE);
|
|
|
|
-- Retención: datos crudos 31 días (dashboard tiempo real)
|
|
SELECT add_retention_policy('telemetry_raw', INTERVAL '31 days', if_not_exists => TRUE);
|
|
|
|
-- Vista continua de 5 minutos — ANH + historial operativo
|
|
CREATE MATERIALIZED VIEW IF NOT EXISTS telemetry_5min
|
|
WITH (timescaledb.continuous, timescaledb.materialized_only = false) AS
|
|
SELECT
|
|
time_bucket('5 minutes', time) AS bucket,
|
|
project_id,
|
|
asset_id,
|
|
last(data, time) AS last_reading,
|
|
first(data, time) AS first_reading,
|
|
count(*) AS reading_count,
|
|
last(time, time) AS last_seen_at
|
|
FROM telemetry_raw
|
|
GROUP BY bucket, project_id, asset_id
|
|
WITH NO DATA;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_telemetry_5min_project ON telemetry_5min (project_id, bucket DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_telemetry_5min_asset ON telemetry_5min (asset_id, bucket DESC);
|
|
|
|
SELECT add_continuous_aggregate_policy(
|
|
'telemetry_5min',
|
|
start_offset => INTERVAL '1 hour',
|
|
end_offset => INTERVAL '5 minutes',
|
|
schedule_interval => INTERVAL '5 minutes',
|
|
if_not_exists => TRUE
|
|
);
|
|
|
|
ALTER MATERIALIZED VIEW telemetry_5min SET (
|
|
timescaledb.compress,
|
|
timescaledb.compress_orderby = 'bucket DESC',
|
|
timescaledb.compress_segmentby = 'project_id, asset_id'
|
|
);
|
|
SELECT add_compression_policy('telemetry_5min', INTERVAL '30 days', if_not_exists => TRUE);
|
|
-- Retención: 5 años (ANH exige 2 años mínimo)
|
|
SELECT add_retention_policy( 'telemetry_5min', INTERVAL '5 years', if_not_exists => TRUE);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 4: LOGS DEL AGENTE (TimescaleDB)
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS agent_logs (
|
|
id UUID NOT NULL DEFAULT uuid_generate_v4(),
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
agent_hostname VARCHAR(255),
|
|
agent_ip VARCHAR(45), -- IP del agente reportante
|
|
level VARCHAR(20),
|
|
category VARCHAR(50),
|
|
message TEXT,
|
|
context JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
SELECT create_hypertable('agent_logs', 'timestamp', if_not_exists => TRUE, migrate_data => TRUE);
|
|
-- Hypertables no soportan PRIMARY KEY sin incluir la columna de partición;
|
|
-- usamos índice simple para lookup por id
|
|
CREATE INDEX IF NOT EXISTS idx_agent_logs_id ON agent_logs (id);
|
|
|
|
ALTER TABLE agent_logs SET (
|
|
timescaledb.compress,
|
|
timescaledb.compress_orderby = 'timestamp DESC',
|
|
timescaledb.compress_segmentby = 'project_id'
|
|
);
|
|
SELECT add_compression_policy('agent_logs', INTERVAL '7 days', if_not_exists => TRUE);
|
|
SELECT add_retention_policy( 'agent_logs', INTERVAL '90 days', if_not_exists => TRUE);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 5: EDGE AGENT DEPLOYMENTS
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS edge_deployments (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
image_tag VARCHAR(500) NOT NULL,
|
|
image_hash VARCHAR(100),
|
|
status VARCHAR(30) NOT NULL DEFAULT 'BUILDING',
|
|
previous_image_tag VARCHAR(500),
|
|
previous_image_hash VARCHAR(100),
|
|
config_snapshot JSONB,
|
|
build_metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
error_message TEXT,
|
|
deployed_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 6: MQTT AUTH & ACL (Mosquitto + go-auth)
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS mqtt_users (
|
|
username VARCHAR(100) PRIMARY KEY,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
is_admin BOOLEAN DEFAULT false,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS mqtt_acls (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(100) NOT NULL REFERENCES mqtt_users(username) ON DELETE CASCADE,
|
|
topic VARCHAR(500) NOT NULL,
|
|
rw INT DEFAULT 1, -- 1=read, 2=write, 4=subscribe, 7=read+write+subscribe
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
CONSTRAINT mqtt_acls_username_topic_key UNIQUE (username, topic)
|
|
);
|
|
|
|
-- go-auth con Mosquitto v2 usa MOSQ_ACL_SUBSCRIBE=4 → rw=7 cubre todos los permisos
|
|
INSERT INTO mqtt_acls (username, topic, rw)
|
|
SELECT 'admin', '#', 7
|
|
FROM mqtt_users WHERE username = 'admin'
|
|
ON CONFLICT (username, topic) DO UPDATE SET rw = 7;
|
|
|
|
UPDATE mqtt_acls SET rw = 7 WHERE topic LIKE 'omnioil/deploy/%';
|
|
UPDATE mqtt_acls SET rw = 7 WHERE topic IN ('omnioil/#', 'provision/#');
|
|
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 7: PROVISIONING DE AGENTES (P256 + token de un solo uso)
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- Token embebido en el binario sin secretos — autentica el primer registro
|
|
CREATE TABLE IF NOT EXISTS agent_provisioning_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
token TEXT NOT NULL UNIQUE,
|
|
used BOOLEAN NOT NULL DEFAULT false,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_provisioning_tokens_project ON agent_provisioning_tokens(project_id);
|
|
CREATE INDEX IF NOT EXISTS idx_provisioning_tokens_token ON agent_provisioning_tokens(token);
|
|
|
|
-- Clave pública P256 del agente para cifrar futuros re-aprovisionamientos
|
|
CREATE TABLE IF NOT EXISTS agent_public_keys (
|
|
project_id UUID PRIMARY KEY REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
public_key_pem TEXT NOT NULL,
|
|
registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 8: MÓDULOS ANH (Operaciones, Pruebas, Laboratorio, Medidores)
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS operation_movements (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
asset_id UUID NOT NULL REFERENCES edge_assets(id) ON DELETE CASCADE,
|
|
movement_type VARCHAR(30) NOT NULL CHECK (movement_type IN ('VENTA', 'RECIBO', 'TRANSFERENCIA', 'QUEMA', 'CONSUMO')),
|
|
start_time TIMESTAMPTZ NOT NULL,
|
|
end_time TIMESTAMPTZ NOT NULL,
|
|
volumen_neto DECIMAL(18, 4),
|
|
details JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
CONSTRAINT check_movement_times CHECK (end_time >= start_time)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS operations_downtime (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
asset_id UUID NOT NULL REFERENCES edge_assets(id) ON DELETE CASCADE,
|
|
start_time TIMESTAMPTZ NOT NULL,
|
|
end_time TIMESTAMPTZ,
|
|
is_planned BOOLEAN NOT NULL DEFAULT false,
|
|
reason VARCHAR(255) NOT NULL,
|
|
comments TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS well_tests (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
asset_id UUID NOT NULL REFERENCES edge_assets(id) ON DELETE CASCADE,
|
|
test_date TIMESTAMPTZ NOT NULL,
|
|
test_type VARCHAR(50) NOT NULL,
|
|
bopd NUMERIC(10, 2),
|
|
bwpd NUMERIC(10, 2),
|
|
gor NUMERIC(10, 2),
|
|
bsw NUMERIC(5, 2),
|
|
wellhead_pressure NUMERIC(10, 2),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS lab_results (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
asset_id UUID NOT NULL REFERENCES edge_assets(id) ON DELETE CASCADE,
|
|
sample_time TIMESTAMPTZ NOT NULL,
|
|
results JSONB NOT NULL,
|
|
lab_technician VARCHAR(150),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS meter_readings (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
asset_id UUID NOT NULL REFERENCES edge_assets(id) ON DELETE CASCADE,
|
|
reading_time TIMESTAMPTZ NOT NULL,
|
|
value NUMERIC(15, 2) NOT NULL,
|
|
unit VARCHAR(20) NOT NULL,
|
|
meter_factor NUMERIC(6, 4) DEFAULT 1.0,
|
|
corrected_value NUMERIC(15, 2),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 9: REPORTES ANH (Resolución 0651)
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS anh_reports (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
filename VARCHAR(255) NOT NULL,
|
|
generated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
status VARCHAR(50) NOT NULL, -- GENERATED, SENT, FAILED
|
|
hash_sha384 TEXT NOT NULL,
|
|
content JSONB NOT NULL,
|
|
size_bytes BIGINT NOT NULL,
|
|
period_start TIMESTAMPTZ,
|
|
period_end TIMESTAMPTZ,
|
|
sent_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_anh_reports_generated_at ON anh_reports (generated_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_anh_reports_status ON anh_reports (status);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 10: ALERTAS Y NOTIFICACIONES
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS telemetry_alarms (
|
|
id UUID NOT NULL DEFAULT uuid_generate_v4(),
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
asset_id UUID REFERENCES edge_assets(id) ON DELETE CASCADE,
|
|
variable_alias VARCHAR(100),
|
|
device_name VARCHAR(100),
|
|
current_value DOUBLE PRECISION,
|
|
alarm_min DOUBLE PRECISION,
|
|
alarm_max DOUBLE PRECISION,
|
|
alarm_type VARCHAR(50) NOT NULL,
|
|
message TEXT,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
SELECT create_hypertable('telemetry_alarms', 'timestamp', if_not_exists => TRUE, migrate_data => TRUE);
|
|
CREATE INDEX IF NOT EXISTS idx_telemetry_alarms_id ON telemetry_alarms (id);
|
|
|
|
-- Habilitar compresión ANTES de registrar la política (requerido por TimescaleDB)
|
|
ALTER TABLE telemetry_alarms SET (
|
|
timescaledb.compress,
|
|
timescaledb.compress_orderby = 'timestamp DESC',
|
|
timescaledb.compress_segmentby = 'project_id, asset_id'
|
|
);
|
|
SELECT add_compression_policy('telemetry_alarms', INTERVAL '30 days', if_not_exists => TRUE);
|
|
-- Retención de alarmas: 2 años (trazabilidad operativa para auditorías)
|
|
SELECT add_retention_policy( 'telemetry_alarms', INTERVAL '2 years', if_not_exists => TRUE);
|
|
|
|
CREATE TABLE IF NOT EXISTS notification_configs (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
channel_type VARCHAR(20) NOT NULL,
|
|
destination TEXT NOT NULL,
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 11: AUDITORÍA (ISO 27001)
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
user_email TEXT,
|
|
action TEXT NOT NULL, -- e.g. "user.login", "project.create", "deploy.trigger"
|
|
resource TEXT, -- e.g. "project:uuid", "user:uuid"
|
|
ip_address TEXT,
|
|
user_agent TEXT,
|
|
metadata JSONB DEFAULT '{}',
|
|
success BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at DESC);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 12: DOMAIN OUTBOX (Mensajería interna)
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE TABLE IF NOT EXISTS domain_outbox (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
event_type VARCHAR(100) NOT NULL,
|
|
payload JSONB NOT NULL,
|
|
status VARCHAR(20) DEFAULT 'PENDING',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- ÍNDICES DE RENDIMIENTO GENERALES
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
CREATE INDEX IF NOT EXISTS idx_agent_logs_project ON agent_logs(project_id, timestamp DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_telemetry_alarms_project ON telemetry_alarms(project_id, timestamp DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_operation_movements_asset ON operation_movements(asset_id);
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 13: REGLAS DE ALERTA Y LOGS DE NOTIFICACIÓN
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
-- Reglas de alerta a nivel de proyecto (complementa alarm_min/max de edge_variables)
|
|
CREATE TABLE IF NOT EXISTS alert_rules (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
asset_id UUID REFERENCES edge_assets(id) ON DELETE CASCADE,
|
|
variable_alias TEXT,
|
|
condition VARCHAR(20) NOT NULL CHECK (condition IN ('ABOVE', 'BELOW', 'EQUAL', 'DELTA')),
|
|
threshold DOUBLE PRECISION NOT NULL,
|
|
severity VARCHAR(20) NOT NULL DEFAULT 'WARNING' CHECK (severity IN ('INFO','WARNING','CRITICAL')),
|
|
message_template TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
cooldown_minutes INT NOT NULL DEFAULT 15,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_alert_rules_project ON alert_rules(project_id);
|
|
CREATE INDEX IF NOT EXISTS idx_alert_rules_asset ON alert_rules(asset_id);
|
|
|
|
-- Log de notificaciones enviadas (trazabilidad)
|
|
CREATE TABLE IF NOT EXISTS notification_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
alarm_id UUID,
|
|
project_id UUID REFERENCES edge_projects(id) ON DELETE SET NULL,
|
|
channel_type VARCHAR(20) NOT NULL,
|
|
destination TEXT NOT NULL,
|
|
subject TEXT,
|
|
body TEXT,
|
|
success BOOLEAN NOT NULL DEFAULT true,
|
|
error_message TEXT,
|
|
sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notification_logs_project ON notification_logs(project_id);
|
|
CREATE INDEX IF NOT EXISTS idx_notification_logs_sent_at ON notification_logs(sent_at DESC);
|
|
|
|
|
|
-- =════════════════════════════════════════════════════════════════════════════
|
|
-- MÓDULO 14: BILLING Y FACTURACIÓN
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
-- Suscripción activa por proyecto
|
|
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id UUID NOT NULL UNIQUE REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
tier VARCHAR(20) NOT NULL DEFAULT 'starter' CHECK (tier IN ('starter','standard','enterprise')),
|
|
tag_limit INT NOT NULL DEFAULT 50,
|
|
price_per_tag BIGINT NOT NULL DEFAULT 90000,
|
|
min_annual_fee BIGINT NOT NULL DEFAULT 4000000,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active','suspended','cancelled','trial')),
|
|
trial_ends_at TIMESTAMPTZ,
|
|
current_period_start TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
current_period_end TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '1 year'),
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Facturas generadas (por período)
|
|
CREATE TABLE IF NOT EXISTS invoices (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
subscription_id UUID REFERENCES subscriptions(id) ON DELETE SET NULL,
|
|
invoice_number VARCHAR(30) NOT NULL UNIQUE,
|
|
period_start TIMESTAMPTZ NOT NULL,
|
|
period_end TIMESTAMPTZ NOT NULL,
|
|
active_tags INT NOT NULL DEFAULT 0,
|
|
unit_price BIGINT NOT NULL DEFAULT 90000,
|
|
subtotal BIGINT NOT NULL DEFAULT 0,
|
|
minimum_applied BOOLEAN NOT NULL DEFAULT false,
|
|
total_cop BIGINT NOT NULL DEFAULT 0,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','sent','paid','overdue','cancelled')),
|
|
due_date TIMESTAMPTZ,
|
|
paid_at TIMESTAMPTZ,
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_invoices_project ON invoices(project_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status);
|
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_project ON subscriptions(project_id);
|
|
|
|
-- Secuencia para numeración de facturas
|
|
CREATE SEQUENCE IF NOT EXISTS invoice_number_seq START 1;
|
|
|
|
-- Snapshot diario de uso (para facturación precisa)
|
|
CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE,
|
|
snapshot_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
active_tags INT NOT NULL DEFAULT 0,
|
|
active_assets INT NOT NULL DEFAULT 0,
|
|
active_devices INT NOT NULL DEFAULT 0,
|
|
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (project_id, snapshot_date)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_usage_snapshots_project ON usage_snapshots(project_id, snapshot_date DESC);
|