40-clients/ucepsa/edge-oee-demo/sql/versions/093_lossintelligence_shadow_foundation.sql
2026-07-16 12:27:29 +02:00

264 lines
10 KiB
PL/PgSQL

BEGIN;
CREATE SCHEMA IF NOT EXISTS mv_loss_intelligence;
COMMENT ON SCHEMA mv_loss_intelligence IS
'Capa común MESAVAULT LossIntelligence. Conserva configuración, catálogo de acciones, Loss Ledger, pruebas y decisiones. No sustituye las tablas raw de captura.';
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.deployment_config (
id BIGSERIAL PRIMARY KEY,
tenant TEXT NOT NULL,
site TEXT NOT NULL,
scope_code TEXT NOT NULL,
scope_name TEXT NOT NULL,
mode TEXT NOT NULL DEFAULT 'SHADOW'
CHECK (mode IN ('SHADOW', 'ACTIVE', 'PAUSED', 'CLOSED')),
planned_go_live_date DATE,
actual_go_live_at TIMESTAMPTZ,
timezone TEXT NOT NULL DEFAULT 'Europe/Madrid',
review_owner_primary TEXT,
review_owner_secondary TEXT,
review_cadence TEXT NOT NULL DEFAULT 'DAILY',
daily_review_limit INTEGER NOT NULL DEFAULT 5
CHECK (daily_review_limit BETWEEN 1 AND 20),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant, site, scope_code),
CHECK (actual_go_live_at IS NULL OR planned_go_live_date IS NULL OR actual_go_live_at::date >= planned_go_live_date - 31)
);
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.economic_parameters (
id BIGSERIAL PRIMARY KEY,
tenant TEXT NOT NULL,
site TEXT NOT NULL,
scope_code TEXT NOT NULL DEFAULT 'GLOBAL',
machine_id TEXT,
product_id BIGINT,
parameter_code TEXT NOT NULL,
parameter_value NUMERIC(18,6) NOT NULL,
unit TEXT NOT NULL,
confidence TEXT NOT NULL DEFAULT 'LOW'
CHECK (confidence IN ('HIGH', 'MEDIUM', 'LOW', 'NOT_ACCEPTED')),
includes_energy BOOLEAN,
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
valid_to TIMESTAMPTZ,
is_active BOOLEAN NOT NULL DEFAULT true,
source_ref TEXT,
approved_by TEXT,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (valid_to IS NULL OR valid_to > valid_from)
);
CREATE INDEX IF NOT EXISTS idx_li_economic_parameters_lookup
ON mv_loss_intelligence.economic_parameters
(tenant, site, parameter_code, is_active, valid_from DESC);
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.product_cycle_parameters (
id BIGSERIAL PRIMARY KEY,
tenant TEXT NOT NULL,
site TEXT NOT NULL,
machine_id TEXT,
product_id BIGINT,
product_default_code TEXT,
product_name TEXT,
units_per_cycle NUMERIC(10,4) NOT NULL
CHECK (units_per_cycle > 0),
target_cycle_rate_ppm NUMERIC(12,4)
CHECK (target_cycle_rate_ppm IS NULL OR target_cycle_rate_ppm > 0),
target_units_per_min NUMERIC(14,4)
GENERATED ALWAYS AS (
CASE
WHEN target_cycle_rate_ppm IS NULL THEN NULL
ELSE target_cycle_rate_ppm * units_per_cycle
END
) STORED,
confidence TEXT NOT NULL DEFAULT 'LOW'
CHECK (confidence IN ('HIGH', 'MEDIUM', 'LOW')),
source_ref TEXT,
approved_by TEXT,
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
valid_to TIMESTAMPTZ,
is_active BOOLEAN NOT NULL DEFAULT true,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (product_id IS NOT NULL OR product_default_code IS NOT NULL),
CHECK (valid_to IS NULL OR valid_to > valid_from)
);
CREATE INDEX IF NOT EXISTS idx_li_product_cycle_parameters_lookup
ON mv_loss_intelligence.product_cycle_parameters
(tenant, site, product_id, product_default_code, machine_id, is_active, valid_from DESC);
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.loss_policies (
id BIGSERIAL PRIMARY KEY,
tenant TEXT NOT NULL,
site TEXT NOT NULL,
policy_code TEXT NOT NULL,
source_type TEXT NOT NULL DEFAULT 'MACHINE_STOP',
noise_below_s NUMERIC(12,2) NOT NULL DEFAULT 5
CHECK (noise_below_s >= 0),
aggregate_below_s NUMERIC(12,2) NOT NULL DEFAULT 300
CHECK (aggregate_below_s >= noise_below_s),
operator_individual_from_s NUMERIC(12,2) NOT NULL DEFAULT 300
CHECK (operator_individual_from_s >= noise_below_s),
aggregation_window_min INTEGER NOT NULL DEFAULT 15
CHECK (aggregation_window_min BETWEEN 1 AND 1440),
require_valid_order BOOLEAN NOT NULL DEFAULT true,
require_closed_event BOOLEAN NOT NULL DEFAULT true,
exclude_planned_by_default BOOLEAN NOT NULL DEFAULT true,
is_active BOOLEAN NOT NULL DEFAULT true,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant, site, policy_code)
);
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.action_catalog (
action_id TEXT PRIMARY KEY,
catalog_code TEXT NOT NULL,
catalog_version TEXT NOT NULL,
tenant TEXT,
site TEXT,
loss_type TEXT NOT NULL,
source_cause_code TEXT,
action_type TEXT NOT NULL
CHECK (action_type IN ('TECHNICAL', 'METHOD', 'ORGANIZATIONAL', 'PROCESS_DESIGN', 'DATA_QUALITY', 'INVESTMENT_DECISION')),
action_candidate TEXT NOT NULL,
applies_when JSONB NOT NULL DEFAULT '[]'::jsonb,
does_not_apply_when JSONB NOT NULL DEFAULT '[]'::jsonb,
suggested_owner TEXT,
test_window TEXT NOT NULL,
metric_before TEXT NOT NULL,
metric_after TEXT NOT NULL,
success_criteria TEXT NOT NULL,
risk_level TEXT NOT NULL DEFAULT 'LOW'
CHECK (risk_level IN ('LOW', 'MEDIUM', 'HIGH')),
cost_to_test TEXT NOT NULL DEFAULT 'LOW'
CHECK (cost_to_test IN ('LOW', 'MEDIUM', 'HIGH')),
is_active BOOLEAN NOT NULL DEFAULT true,
source_ref TEXT,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_li_action_catalog_lookup
ON mv_loss_intelligence.action_catalog
(loss_type, source_cause_code, is_active);
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.loss_ledger (
loss_id BIGSERIAL PRIMARY KEY,
tenant TEXT NOT NULL,
site TEXT NOT NULL,
deployment_id BIGINT REFERENCES mv_loss_intelligence.deployment_config(id),
ledger_phase TEXT NOT NULL DEFAULT 'SHADOW'
CHECK (ledger_phase IN ('SHADOW', 'ACTIVE')),
source_type TEXT NOT NULL,
source_key TEXT NOT NULL,
machine_id TEXT,
asset TEXT,
order_ref TEXT,
odoo_production_id BIGINT,
odoo_workorder_id BIGINT,
product_id BIGINT,
product_default_code TEXT,
product_name TEXT,
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
event_count INTEGER NOT NULL DEFAULT 1 CHECK (event_count > 0),
duration_s NUMERIC(18,2) NOT NULL DEFAULT 0 CHECK (duration_s >= 0),
cycle_count NUMERIC(18,4),
units_per_cycle NUMERIC(10,4),
estimated_units NUMERIC(18,4),
loss_type TEXT NOT NULL,
cause_code TEXT,
classification_status TEXT NOT NULL DEFAULT 'PENDING'
CHECK (classification_status IN ('PENDING', 'CLASSIFIED', 'EXCLUDED', 'NOT_REQUIRED')),
capacity_loss_eur NUMERIC(18,2),
energy_waste_kwh NUMERIC(18,6),
energy_waste_eur NUMERIC(18,2),
technical_confidence TEXT NOT NULL DEFAULT 'MEDIUM'
CHECK (technical_confidence IN ('HIGH', 'MEDIUM', 'LOW')),
economic_confidence TEXT NOT NULL DEFAULT 'LOW'
CHECK (economic_confidence IN ('HIGH', 'MEDIUM', 'LOW', 'NOT_ACCEPTED')),
priority_score NUMERIC(12,2),
priority_level TEXT
CHECK (priority_level IS NULL OR priority_level IN ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL')),
action_catalog_id TEXT REFERENCES mv_loss_intelligence.action_catalog(action_id),
action_candidate TEXT,
action_final TEXT,
review_owner TEXT,
review_due_at TIMESTAMPTZ,
status TEXT NOT NULL DEFAULT 'DETECTED'
CHECK (status IN ('DETECTED', 'CANDIDATE', 'IN_REVIEW', 'IN_TRIAL', 'VALIDATED', 'ADAPTED', 'DISCARDED', 'ESCALATED', 'CLOSED')),
learning_reusable TEXT,
evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb,
detected_at TIMESTAMPTZ NOT NULL DEFAULT now(),
promoted_at TIMESTAMPTZ,
closed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant, site, source_type, source_key)
);
CREATE INDEX IF NOT EXISTS idx_li_loss_ledger_review
ON mv_loss_intelligence.loss_ledger
(tenant, site, status, review_due_at, priority_score DESC);
CREATE INDEX IF NOT EXISTS idx_li_loss_ledger_order_machine
ON mv_loss_intelligence.loss_ledger
(tenant, site, machine_id, order_ref, started_at DESC);
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.action_trials (
trial_id BIGSERIAL PRIMARY KEY,
loss_id BIGINT NOT NULL REFERENCES mv_loss_intelligence.loss_ledger(loss_id) ON DELETE CASCADE,
trial_code TEXT NOT NULL,
action_final TEXT NOT NULL,
owner TEXT NOT NULL,
baseline_from TIMESTAMPTZ,
baseline_to TIMESTAMPTZ,
test_from TIMESTAMPTZ,
test_to TIMESTAMPTZ,
metric_code TEXT NOT NULL,
metric_unit TEXT,
metric_before NUMERIC(18,6),
metric_after NUMERIC(18,6),
change_pct NUMERIC(12,4),
success_criteria TEXT NOT NULL,
result TEXT
CHECK (result IS NULL OR result IN ('PENDING', 'SUCCESS', 'PARTIAL', 'NO_CHANGE', 'WORSE', 'INVALID_TEST')),
decision TEXT
CHECK (decision IS NULL OR decision IN ('KEEP', 'ADAPT', 'REPEAT', 'DISCARD', 'SCALE')),
learning TEXT,
evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (loss_id, trial_code)
);
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.review_decisions (
decision_id BIGSERIAL PRIMARY KEY,
loss_id BIGINT REFERENCES mv_loss_intelligence.loss_ledger(loss_id) ON DELETE CASCADE,
trial_id BIGINT REFERENCES mv_loss_intelligence.action_trials(trial_id) ON DELETE CASCADE,
decision_type TEXT NOT NULL
CHECK (decision_type IN ('PROMOTE', 'CLASSIFY', 'ASSIGN_ACTION', 'ADAPT_ACTION', 'DEFER', 'EXCLUDE', 'CLOSE', 'ESCALATE')),
decision_text TEXT NOT NULL,
decided_by TEXT NOT NULL,
decided_at TIMESTAMPTZ NOT NULL DEFAULT now(),
next_review_at TIMESTAMPTZ,
evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb,
CHECK (loss_id IS NOT NULL OR trial_id IS NOT NULL)
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ucepsa_ro') THEN
GRANT USAGE ON SCHEMA mv_loss_intelligence TO grafana_ucepsa_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA mv_loss_intelligence TO grafana_ucepsa_ro;
END IF;
END $$;
COMMIT;