chore: import DH V2 D5.6.4 production baseline

This commit is contained in:
DH V2
2026-09-05 10:12:35 -03:00
commit 82213e72f5
757 changed files with 84218 additions and 0 deletions
@@ -0,0 +1,354 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
const newPermissions = [
'asset_registry.read',
'asset_registry.manage',
'assets.change_operational_status',
] as const;
const rolePermissionValues = `
('admin', 'asset_registry.read'), ('admin', 'asset_registry.manage'), ('admin', 'assets.change_operational_status'),
('director', 'asset_registry.read'), ('director', 'asset_registry.manage'), ('director', 'assets.change_operational_status'),
('supervisor', 'asset_registry.read'), ('supervisor', 'asset_registry.manage'), ('supervisor', 'assets.change_operational_status'),
('inspector', 'asset_registry.read'), ('auditor', 'asset_registry.read')
`;
function quoteIdentifier(identifier: string): string {
return `"${identifier.replaceAll('"', '""')}"`;
}
export class PhaseD533DefinitiveOperationalModel1787508000000 implements MigrationInterface {
name = 'PhaseD533DefinitiveOperationalModel1787508000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE asset_operational_status AS ENUM ('UNKNOWN','IN_SERVICE','TEMPORARILY_OUT_OF_SERVICE','OUT_OF_SERVICE','DECOMMISSIONED','ABANDONED')`);
await queryRunner.query(`ALTER TABLE assets ADD COLUMN operational_status asset_operational_status NOT NULL DEFAULT 'UNKNOWN'`);
await queryRunner.query(`CREATE INDEX idx_assets_operational_status ON assets (operational_status)`);
await queryRunner.query(`
UPDATE asset_types SET name='Área',
description='Área hidrocarburífera administrada como ancla territorial. Los permisos, concesiones y titulares se registran por separado en la capa legal.',
updated_at=CURRENT_TIMESTAMP WHERE code='area'
`);
await queryRunner.query(`
UPDATE asset_types SET name='Organización',
description='Entidad jurídica u organización administrada (empresa, UTE u otra figura). Su rol como operadora, titular o participante se registra mediante relaciones históricas.',
updated_at=CURRENT_TIMESTAMP WHERE code='empresa'
`);
await queryRunner.query(`
UPDATE asset_attribute_definitions d SET is_active=false,updated_at=CURRENT_TIMESTAMP
FROM asset_types t WHERE d.asset_type_id=t.id AND t.code='area'
AND d.code IN ('situacion_concesion','vencimiento_concesion')
`);
await queryRunner.query(`
INSERT INTO asset_attribute_definitions (asset_type_id,code,name,data_type,is_required,is_active,unit,options,sort_order)
SELECT t.id,p.code,p.name,'TEXT'::asset_attribute_data_type,false,true,NULL,NULL,p.sort_order
FROM asset_types t CROSS JOIN (VALUES ('cuenca','Cuenca',20),('departamento','Departamento',30)) p(code,name,sort_order)
WHERE t.code='area' ON CONFLICT DO NOTHING
`);
await queryRunner.query(`CREATE TYPE organization_kind AS ENUM ('COMPANY','UTE','PUBLIC_ENTITY','OTHER')`);
await queryRunner.query(`
CREATE TABLE organization_profiles (
asset_id uuid PRIMARY KEY,
organization_kind organization_kind NOT NULL DEFAULT 'COMPANY',
legal_name varchar(240), tax_id varchar(32), notes text,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_by uuid,
CONSTRAINT fk_organization_profiles_asset FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_organization_profiles_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`CREATE UNIQUE INDEX uq_organization_profiles_tax_id ON organization_profiles (tax_id) WHERE tax_id IS NOT NULL`);
await queryRunner.query(`
INSERT INTO organization_profiles (asset_id,organization_kind,legal_name)
SELECT a.id,'COMPANY'::organization_kind,a.name FROM assets a JOIN asset_types t ON t.id=a.asset_type_id
WHERE t.operational_role='COMPANY' ON CONFLICT (asset_id) DO NOTHING
`);
await queryRunner.query(`CREATE TYPE source_document_type AS ENUM ('NOTE','TECHNICAL_REPORT','INSPECTION_ACT','INVENTORY','RESOLUTION','DECREE','CONTRACT','SPREADSHEET','OTHER')`);
await queryRunner.query(`
CREATE TABLE source_documents (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), document_type source_document_type NOT NULL,
document_number varchar(160), title varchar(300) NOT NULL, issuer varchar(240), document_date date,
external_reference varchar(500), notes text,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by uuid, updated_by uuid,
CONSTRAINT chk_source_documents_title CHECK (length(btrim(title)) >= 3),
CONSTRAINT fk_source_documents_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_source_documents_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`CREATE INDEX idx_source_documents_number ON source_documents (document_number)`);
await queryRunner.query(`CREATE INDEX idx_source_documents_document_date ON source_documents (document_date DESC)`);
await queryRunner.query(`CREATE UNIQUE INDEX uq_source_documents_number_issuer ON source_documents (document_number,issuer) WHERE document_number IS NOT NULL AND issuer IS NOT NULL`);
await queryRunner.query(`CREATE TYPE organization_membership_role AS ENUM ('MEMBER','LEAD_MEMBER','OTHER')`);
await queryRunner.query(`
CREATE TABLE organization_memberships (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), parent_organization_id uuid NOT NULL, member_organization_id uuid NOT NULL,
role organization_membership_role NOT NULL DEFAULT 'MEMBER', participation_percent numeric(7,4),
valid_from date NOT NULL DEFAULT CURRENT_DATE, valid_until date, source_document_id uuid, notes text, end_reason text,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by uuid, ended_by uuid,
CONSTRAINT chk_organization_membership_different CHECK (parent_organization_id <> member_organization_id),
CONSTRAINT chk_organization_membership_participation CHECK (participation_percent IS NULL OR (participation_percent > 0 AND participation_percent <= 100)),
CONSTRAINT chk_organization_membership_dates CHECK (valid_until IS NULL OR valid_until >= valid_from),
CONSTRAINT fk_organization_membership_parent FOREIGN KEY (parent_organization_id) REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_organization_membership_member FOREIGN KEY (member_organization_id) REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_organization_membership_document FOREIGN KEY (source_document_id) REFERENCES source_documents(id) ON DELETE SET NULL,
CONSTRAINT fk_organization_membership_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_organization_membership_ended_by FOREIGN KEY (ended_by) REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`CREATE INDEX idx_organization_memberships_parent_id ON organization_memberships (parent_organization_id)`);
await queryRunner.query(`CREATE INDEX idx_organization_memberships_member_id ON organization_memberships (member_organization_id)`);
await queryRunner.query(`CREATE INDEX idx_organization_memberships_valid_until ON organization_memberships (valid_until)`);
await queryRunner.query(`CREATE UNIQUE INDEX uq_organization_memberships_active_role ON organization_memberships (parent_organization_id,member_organization_id,role) WHERE valid_until IS NULL`);
await queryRunner.query(`CREATE TYPE asset_source_document_relation_type AS ENUM ('SOURCE','MENTIONS','VALIDATES','SUPERSEDES','OTHER')`);
await queryRunner.query(`
CREATE TABLE asset_source_documents (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), asset_id uuid NOT NULL, document_id uuid NOT NULL,
relation_type asset_source_document_relation_type NOT NULL DEFAULT 'SOURCE', notes text,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by uuid,
CONSTRAINT fk_asset_source_documents_asset FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_asset_source_documents_document FOREIGN KEY (document_id) REFERENCES source_documents(id) ON DELETE RESTRICT,
CONSTRAINT fk_asset_source_documents_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT uq_asset_source_documents UNIQUE (asset_id,document_id,relation_type)
)
`);
await queryRunner.query(`CREATE INDEX idx_asset_source_documents_asset_id ON asset_source_documents (asset_id)`);
await queryRunner.query(`CREATE INDEX idx_asset_source_documents_document_id ON asset_source_documents (document_id)`);
await queryRunner.query(`
CREATE TABLE asset_external_identifiers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), asset_id uuid NOT NULL, namespace varchar(80) NOT NULL, value varchar(180) NOT NULL,
valid_from timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_until timestamptz, source_document_id uuid, notes text, end_reason text,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by uuid, ended_by uuid,
CONSTRAINT chk_asset_external_identifier_namespace CHECK (namespace ~ '^[A-Z0-9][A-Z0-9._/-]{1,79}$'),
CONSTRAINT chk_asset_external_identifier_value CHECK (length(btrim(value)) >= 1),
CONSTRAINT chk_asset_external_identifier_dates CHECK (valid_until IS NULL OR valid_until >= valid_from),
CONSTRAINT fk_asset_external_identifier_asset FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_asset_external_identifier_document FOREIGN KEY (source_document_id) REFERENCES source_documents(id) ON DELETE SET NULL,
CONSTRAINT fk_asset_external_identifier_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_asset_external_identifier_ended_by FOREIGN KEY (ended_by) REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`CREATE INDEX idx_asset_external_identifiers_asset_id ON asset_external_identifiers (asset_id)`);
await queryRunner.query(`CREATE INDEX idx_asset_external_identifiers_namespace ON asset_external_identifiers (namespace)`);
await queryRunner.query(`CREATE UNIQUE INDEX uq_asset_external_identifiers_active_namespace_value ON asset_external_identifiers (namespace,value) WHERE valid_until IS NULL`);
await queryRunner.query(`CREATE TYPE area_organization_role AS ENUM ('OPERATOR','TECHNICAL_OPERATOR','CONCESSIONAIRE','PERMIT_HOLDER','PARTICIPANT','OTHER')`);
await queryRunner.query(`DROP INDEX uq_area_company_relations_active_pair`);
await queryRunner.query(`
ALTER TABLE area_company_relations
ADD COLUMN relation_role area_organization_role NOT NULL DEFAULT 'OPERATOR',
ADD COLUMN participation_percent numeric(7,4), ADD COLUMN legal_instrument varchar(240), ADD COLUMN source_document_id uuid,
ADD CONSTRAINT chk_area_company_relations_participation CHECK (participation_percent IS NULL OR (participation_percent > 0 AND participation_percent <= 100)),
ADD CONSTRAINT fk_area_company_relations_source_document FOREIGN KEY (source_document_id) REFERENCES source_documents(id) ON DELETE SET NULL
`);
await queryRunner.query(`CREATE UNIQUE INDEX uq_area_company_relations_active_role ON area_company_relations (area_id,company_id,relation_role) WHERE valid_until IS NULL`);
await queryRunner.query(`CREATE INDEX idx_area_company_relations_role ON area_company_relations (relation_role)`);
await queryRunner.query(`
CREATE OR REPLACE FUNCTION enforce_asset_operational_context() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE asset_role asset_type_operational_role; area_role asset_type_operational_role; company_role asset_type_operational_role; active_relation_id uuid;
BEGIN
IF NEW.operational_area_id IS NULL AND NEW.operator_company_id IS NULL THEN RETURN NEW; END IF;
IF NEW.operational_area_id IS NULL OR NEW.operator_company_id IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area and organization must be assigned together'; END IF;
SELECT operational_role INTO asset_role FROM asset_types WHERE id=NEW.asset_type_id;
IF asset_role <> 'GENERIC'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='area and organization assets cannot receive an operational assignment'; END IF;
SELECT t.operational_role INTO area_role FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE a.id=NEW.operational_area_id AND a.information_status<>'INACTIVE' AND t.is_active=true;
SELECT t.operational_role INTO company_role FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE a.id=NEW.operator_company_id AND a.information_status<>'INACTIVE' AND t.is_active=true;
IF area_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area must be an active AREA asset'; END IF;
IF company_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operator organization must be an active COMPANY-role asset'; END IF;
SELECT r.id INTO active_relation_id FROM area_company_relations r WHERE r.area_id=NEW.operational_area_id AND r.company_id=NEW.operator_company_id AND r.relation_role='OPERATOR'::area_organization_role AND r.valid_until IS NULL FOR KEY SHARE;
IF active_relation_id IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area and organization do not have an active OPERATOR relation'; END IF;
IF NEW.parent_id IS NULL OR NOT EXISTS (WITH RECURSIVE ancestors AS (SELECT id,parent_id FROM assets WHERE id=NEW.parent_id UNION ALL SELECT p.id,p.parent_id FROM assets p JOIN ancestors c ON p.id=c.parent_id) SELECT 1 FROM ancestors WHERE id=NEW.operational_area_id LIMIT 1) THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area must be an ancestor in the physical hierarchy'; END IF;
RETURN NEW;
END $$
`);
await queryRunner.query(`CREATE TYPE area_legal_right_type AS ENUM ('EXPLOITATION_CONCESSION','EXPLORATION_PERMIT','TRANSPORT_CONCESSION','OTHER')`);
await queryRunner.query(`CREATE TYPE area_legal_right_status AS ENUM ('ACTIVE','EXPIRED','REVOKED','PENDING')`);
await queryRunner.query(`
CREATE TABLE area_legal_rights (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), area_id uuid NOT NULL, right_type area_legal_right_type NOT NULL,
name varchar(260) NOT NULL, instrument_number varchar(180), valid_from date, valid_until date,
status area_legal_right_status NOT NULL DEFAULT 'ACTIVE', source_document_id uuid, notes text,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by uuid, updated_by uuid,
CONSTRAINT chk_area_legal_right_dates CHECK (valid_until IS NULL OR valid_from IS NULL OR valid_until >= valid_from),
CONSTRAINT chk_area_legal_right_name CHECK (length(btrim(name)) >= 3),
CONSTRAINT fk_area_legal_right_area FOREIGN KEY (area_id) REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_area_legal_right_source_document FOREIGN KEY (source_document_id) REFERENCES source_documents(id) ON DELETE SET NULL,
CONSTRAINT fk_area_legal_right_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_area_legal_right_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`CREATE INDEX idx_area_legal_rights_area_id ON area_legal_rights (area_id)`);
await queryRunner.query(`CREATE INDEX idx_area_legal_rights_valid_until ON area_legal_rights (valid_until)`);
await queryRunner.query(`CREATE TYPE area_legal_right_organization_role AS ENUM ('HOLDER','PARTICIPANT','OPERATOR','OTHER')`);
await queryRunner.query(`
CREATE TABLE area_legal_right_organizations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), right_id uuid NOT NULL, organization_id uuid NOT NULL,
role area_legal_right_organization_role NOT NULL, participation_percent numeric(7,4), valid_from date NOT NULL DEFAULT CURRENT_DATE,
valid_until date, notes text, end_reason text, created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by uuid, ended_by uuid,
CONSTRAINT chk_area_legal_right_org_participation CHECK (participation_percent IS NULL OR (participation_percent > 0 AND participation_percent <= 100)),
CONSTRAINT chk_area_legal_right_org_dates CHECK (valid_until IS NULL OR valid_until >= valid_from),
CONSTRAINT fk_area_legal_right_org_right FOREIGN KEY (right_id) REFERENCES area_legal_rights(id) ON DELETE RESTRICT,
CONSTRAINT fk_area_legal_right_org_organization FOREIGN KEY (organization_id) REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_area_legal_right_org_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_area_legal_right_org_ended_by FOREIGN KEY (ended_by) REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`CREATE INDEX idx_area_legal_right_org_right_id ON area_legal_right_organizations (right_id)`);
await queryRunner.query(`CREATE INDEX idx_area_legal_right_org_organization_id ON area_legal_right_organizations (organization_id)`);
await queryRunner.query(`CREATE INDEX idx_area_legal_right_org_valid_until ON area_legal_right_organizations (valid_until)`);
await queryRunner.query(`CREATE UNIQUE INDEX uq_area_legal_right_org_active_role ON area_legal_right_organizations (right_id,organization_id,role) WHERE valid_until IS NULL`);
await queryRunner.query(`
CREATE FUNCTION enforce_d533_registry_roles() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE resolved_role asset_type_operational_role; parent_kind organization_kind; member_kind organization_kind;
BEGIN
IF TG_TABLE_NAME='organization_profiles' THEN
SELECT t.operational_role INTO resolved_role FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE a.id=NEW.asset_id;
IF resolved_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='organization profile requires a COMPANY-role asset'; END IF;
IF NEW.organization_kind <> 'UTE'::organization_kind AND EXISTS (SELECT 1 FROM organization_memberships m WHERE m.parent_organization_id=NEW.asset_id AND m.valid_until IS NULL) THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='organization with active members must remain profiled as UTE'; END IF;
IF NEW.organization_kind = 'UTE'::organization_kind AND EXISTS (SELECT 1 FROM organization_memberships m WHERE m.member_organization_id=NEW.asset_id AND m.valid_until IS NULL) THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='organization that actively belongs to a UTE cannot itself become UTE'; END IF;
ELSIF TG_TABLE_NAME='organization_memberships' THEN
IF NEW.parent_organization_id=NEW.member_organization_id THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='organization cannot be a member of itself'; END IF;
SELECT t.operational_role,p.organization_kind INTO resolved_role,parent_kind FROM assets a JOIN asset_types t ON t.id=a.asset_type_id LEFT JOIN organization_profiles p ON p.asset_id=a.id WHERE a.id=NEW.parent_organization_id;
IF resolved_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role OR parent_kind IS DISTINCT FROM 'UTE'::organization_kind THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='parent organization membership requires an organization profiled as UTE'; END IF;
SELECT t.operational_role,p.organization_kind INTO resolved_role,member_kind FROM assets a JOIN asset_types t ON t.id=a.asset_type_id LEFT JOIN organization_profiles p ON p.asset_id=a.id WHERE a.id=NEW.member_organization_id;
IF resolved_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role OR member_kind IS NULL OR member_kind='UTE'::organization_kind THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='UTE member must be a non-UTE organization'; END IF;
ELSIF TG_TABLE_NAME='area_legal_rights' THEN
SELECT t.operational_role INTO resolved_role FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE a.id=NEW.area_id;
IF resolved_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='legal right requires an AREA asset'; END IF;
ELSIF TG_TABLE_NAME='area_legal_right_organizations' THEN
SELECT t.operational_role INTO resolved_role FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE a.id=NEW.organization_id;
IF resolved_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='legal right participant requires a COMPANY-role asset'; END IF;
END IF;
RETURN NEW;
END $$
`);
await queryRunner.query(`CREATE TRIGGER trg_organization_profiles_role BEFORE INSERT OR UPDATE OF asset_id,organization_kind ON organization_profiles FOR EACH ROW EXECUTE FUNCTION enforce_d533_registry_roles()`);
await queryRunner.query(`CREATE TRIGGER trg_organization_memberships_role BEFORE INSERT OR UPDATE OF parent_organization_id,member_organization_id ON organization_memberships FOR EACH ROW EXECUTE FUNCTION enforce_d533_registry_roles()`);
await queryRunner.query(`CREATE TRIGGER trg_area_legal_rights_role BEFORE INSERT OR UPDATE OF area_id ON area_legal_rights FOR EACH ROW EXECUTE FUNCTION enforce_d533_registry_roles()`);
await queryRunner.query(`CREATE TRIGGER trg_area_legal_right_org_role BEFORE INSERT OR UPDATE OF organization_id ON area_legal_right_organizations FOR EACH ROW EXECUTE FUNCTION enforce_d533_registry_roles()`);
await queryRunner.query(`
CREATE OR REPLACE FUNCTION protect_operational_anchor_inactivation() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE role asset_type_operational_role;
BEGIN
IF NEW.information_status IS NOT DISTINCT FROM OLD.information_status OR NEW.information_status <> 'INACTIVE' THEN RETURN NEW; END IF;
SELECT operational_role INTO role FROM asset_types WHERE id=NEW.asset_type_id;
IF role='AREA'::asset_type_operational_role AND (
EXISTS (SELECT 1 FROM area_company_relations WHERE area_id=NEW.id AND valid_until IS NULL) OR EXISTS (SELECT 1 FROM assets WHERE operational_area_id=NEW.id) OR
EXISTS (SELECT 1 FROM area_legal_rights r WHERE r.area_id=NEW.id AND r.status IN ('ACTIVE','PENDING') AND (r.valid_until IS NULL OR r.valid_until>=CURRENT_DATE))
) THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='area cannot be inactivated while operational relations, assignments or legal rights are active'; END IF;
IF role='COMPANY'::asset_type_operational_role AND (
EXISTS (SELECT 1 FROM area_company_relations WHERE company_id=NEW.id AND valid_until IS NULL) OR EXISTS (SELECT 1 FROM assets WHERE operator_company_id=NEW.id) OR
EXISTS (SELECT 1 FROM organization_memberships m WHERE (m.parent_organization_id=NEW.id OR m.member_organization_id=NEW.id) AND m.valid_until IS NULL) OR
EXISTS (SELECT 1 FROM area_legal_right_organizations p JOIN area_legal_rights r ON r.id=p.right_id WHERE p.organization_id=NEW.id AND p.valid_until IS NULL AND r.status IN ('ACTIVE','PENDING') AND (r.valid_until IS NULL OR r.valid_until>=CURRENT_DATE))
) THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='organization cannot be inactivated while operational relations, assignments, memberships or legal participation are active'; END IF;
RETURN NEW;
END $$
`);
await queryRunner.query(`UPDATE asset_versions SET snapshot=jsonb_set(snapshot,'{operationalStatus}','"UNKNOWN"'::jsonb,true) WHERE NOT (snapshot ? 'operationalStatus')`);
const descriptions: Record<string,string> = {
'asset_registry.read':'Consultar identificadores, documentos, organizaciones y derechos del Maestro',
'asset_registry.manage':'Administrar identificadores, documentos, organizaciones y derechos del Maestro',
'assets.change_operational_status':'Cambiar el estado operativo de activos',
};
for (const permission of newPermissions) {
await queryRunner.query(`INSERT INTO permissions (code,description) VALUES ($1,$2) ON CONFLICT (code) DO UPDATE SET description=EXCLUDED.description`,[permission,descriptions[permission]]);
}
await queryRunner.query(`WITH mapping(role_code,permission_code) AS (VALUES ${rolePermissionValues}) INSERT INTO role_permissions (role_id,permission_id) SELECT r.id,p.id FROM mapping m JOIN roles r ON r.code=m.role_code JOIN permissions p ON p.code=m.permission_code ON CONFLICT (role_id,permission_id) DO NOTHING`);
const appRole=process.env.DB_APP_USER;
if (!appRole) throw new Error('Missing required environment variable: DB_APP_USER');
const roleRows=(await queryRunner.query('SELECT 1 FROM pg_roles WHERE rolname=$1',[appRole])) as unknown[];
if (roleRows.length!==1) throw new Error('Configured DB_APP_USER does not exist');
const applicationRole=quoteIdentifier(appRole);
const tables=['organization_profiles','organization_memberships','source_documents','asset_source_documents','asset_external_identifiers','area_legal_rights','area_legal_right_organizations'].map(quoteIdentifier).join(', ');
await queryRunner.query(`GRANT SELECT, INSERT, UPDATE ON TABLE ${tables} TO ${applicationRole}`);
await queryRunner.query(`REVOKE DELETE ON TABLE ${tables} FROM ${applicationRole}`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`WITH mapping(role_code,permission_code) AS (VALUES ${rolePermissionValues}) DELETE FROM role_permissions rp USING roles r,permissions p,mapping m WHERE rp.role_id=r.id AND rp.permission_id=p.id AND r.code=m.role_code AND p.code=m.permission_code`);
await queryRunner.query(`DELETE FROM permissions WHERE code=ANY($1::text[]) AND NOT EXISTS (SELECT 1 FROM role_permissions WHERE permission_id=permissions.id)`,[newPermissions]);
await queryRunner.query(`UPDATE asset_versions SET snapshot=snapshot-'operationalStatus'`);
await queryRunner.query(`
CREATE OR REPLACE FUNCTION protect_operational_anchor_inactivation() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE role asset_type_operational_role; BEGIN
IF NEW.information_status IS NOT DISTINCT FROM OLD.information_status OR NEW.information_status<>'INACTIVE' THEN RETURN NEW; END IF;
SELECT operational_role INTO role FROM asset_types WHERE id=NEW.asset_type_id;
IF role='AREA'::asset_type_operational_role AND (EXISTS (SELECT 1 FROM area_company_relations WHERE area_id=NEW.id AND valid_until IS NULL) OR EXISTS (SELECT 1 FROM assets WHERE operational_area_id=NEW.id)) THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='area cannot be inactivated while operational relations or assignments are active'; END IF;
IF role='COMPANY'::asset_type_operational_role AND (EXISTS (SELECT 1 FROM area_company_relations WHERE company_id=NEW.id AND valid_until IS NULL) OR EXISTS (SELECT 1 FROM assets WHERE operator_company_id=NEW.id)) THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='company cannot be inactivated while operational relations or assignments are active'; END IF;
RETURN NEW; END $$
`);
await queryRunner.query('DROP TRIGGER trg_area_legal_right_org_role ON area_legal_right_organizations');
await queryRunner.query('DROP TRIGGER trg_area_legal_rights_role ON area_legal_rights');
await queryRunner.query('DROP TRIGGER trg_organization_memberships_role ON organization_memberships');
await queryRunner.query('DROP TRIGGER trg_organization_profiles_role ON organization_profiles');
await queryRunner.query('DROP FUNCTION enforce_d533_registry_roles()');
await queryRunner.query('DROP TABLE area_legal_right_organizations');
await queryRunner.query('DROP TYPE area_legal_right_organization_role');
await queryRunner.query('DROP TABLE area_legal_rights');
await queryRunner.query('DROP TYPE area_legal_right_status');
await queryRunner.query('DROP TYPE area_legal_right_type');
await queryRunner.query(`
CREATE OR REPLACE FUNCTION enforce_asset_operational_context() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE asset_role asset_type_operational_role; area_role asset_type_operational_role; company_role asset_type_operational_role; active_relation_id uuid;
BEGIN
IF NEW.operational_area_id IS NULL AND NEW.operator_company_id IS NULL THEN RETURN NEW; END IF;
IF NEW.operational_area_id IS NULL OR NEW.operator_company_id IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area and company must be assigned together'; END IF;
SELECT operational_role INTO asset_role FROM asset_types WHERE id=NEW.asset_type_id;
IF asset_role <> 'GENERIC'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='area and company assets cannot receive an operational assignment'; END IF;
SELECT t.operational_role INTO area_role FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE a.id=NEW.operational_area_id AND a.information_status<>'INACTIVE' AND t.is_active=true;
SELECT t.operational_role INTO company_role FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE a.id=NEW.operator_company_id AND a.information_status<>'INACTIVE' AND t.is_active=true;
IF area_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area must be an active AREA asset'; END IF;
IF company_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operator company must be an active COMPANY asset'; END IF;
SELECT r.id INTO active_relation_id FROM area_company_relations r WHERE r.area_id=NEW.operational_area_id AND r.company_id=NEW.operator_company_id AND r.valid_until IS NULL FOR KEY SHARE;
IF active_relation_id IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area and company do not have an active relation'; END IF;
IF NEW.parent_id IS NULL OR NOT EXISTS (WITH RECURSIVE ancestors AS (SELECT id,parent_id FROM assets WHERE id=NEW.parent_id UNION ALL SELECT p.id,p.parent_id FROM assets p JOIN ancestors c ON p.id=c.parent_id) SELECT 1 FROM ancestors WHERE id=NEW.operational_area_id LIMIT 1) THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area must be an ancestor in the physical hierarchy'; END IF;
RETURN NEW; END $$
`);
await queryRunner.query('DROP INDEX idx_area_company_relations_role');
await queryRunner.query('DROP INDEX uq_area_company_relations_active_role');
await queryRunner.query('ALTER TABLE area_company_relations DROP CONSTRAINT fk_area_company_relations_source_document');
await queryRunner.query('ALTER TABLE area_company_relations DROP CONSTRAINT chk_area_company_relations_participation');
await queryRunner.query('ALTER TABLE area_company_relations DROP COLUMN source_document_id,DROP COLUMN legal_instrument,DROP COLUMN participation_percent,DROP COLUMN relation_role');
await queryRunner.query(`CREATE UNIQUE INDEX uq_area_company_relations_active_pair ON area_company_relations (area_id,company_id) WHERE valid_until IS NULL`);
await queryRunner.query('DROP TYPE area_organization_role');
await queryRunner.query('DROP TABLE asset_external_identifiers');
await queryRunner.query('DROP TABLE asset_source_documents');
await queryRunner.query('DROP TYPE asset_source_document_relation_type');
await queryRunner.query('DROP TABLE organization_memberships');
await queryRunner.query('DROP TYPE organization_membership_role');
await queryRunner.query('DROP TABLE source_documents');
await queryRunner.query('DROP TYPE source_document_type');
await queryRunner.query('DROP TABLE organization_profiles');
await queryRunner.query('DROP TYPE organization_kind');
await queryRunner.query(`UPDATE asset_types SET name='Área / Concesión',description='Área hidrocarburífera o concesión administrada. Funciona como ancla territorial y operativa.',updated_at=CURRENT_TIMESTAMP WHERE code='area'`);
await queryRunner.query(`UPDATE asset_types SET name='Empresa / Operadora',description='Empresa operadora, concesionaria o integrante de una explotación. Se vincula a una o más áreas mediante relaciones operativas históricas.',updated_at=CURRENT_TIMESTAMP WHERE code='empresa'`);
await queryRunner.query(`UPDATE asset_attribute_definitions d SET is_active=true,updated_at=CURRENT_TIMESTAMP FROM asset_types t WHERE d.asset_type_id=t.id AND t.code='area' AND d.code IN ('situacion_concesion','vencimiento_concesion')`);
await queryRunner.query(`DELETE FROM asset_attribute_definitions d USING asset_types t WHERE d.asset_type_id=t.id AND t.code='area' AND d.code IN ('cuenca','departamento') AND NOT EXISTS (SELECT 1 FROM asset_attribute_values v WHERE v.definition_id=d.id)`);
await queryRunner.query('DROP INDEX idx_assets_operational_status');
await queryRunner.query('ALTER TABLE assets DROP COLUMN operational_status');
await queryRunner.query('DROP TYPE asset_operational_status');
}
}