Files
OmniOilPersonal/services/backend-api/migrations/20260408000000_initial_schema.sql
2026-05-07 10:27:37 -05:00

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);