381 lines
27 KiB
TypeScript
381 lines
27 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
import { AUTHORITATIVE_INVENTORY_MODEL as SOURCE } from '../../reference-data/authoritative-inventory-model';
|
|
|
|
type IdRow = { id: string };
|
|
|
|
const TERRITORY_DOC = 'DH-AUTH-TERRITORY-20260911';
|
|
const INVENTORY_DOC = 'DH-AUTH-INVENTORY-20260911';
|
|
const FINDING_CATEGORY_CODE = 'AUTHMODEL';
|
|
|
|
function sourceCode(prefix: string, id: number): string {
|
|
return `${prefix}-${String(id).padStart(4, '0')}`;
|
|
}
|
|
|
|
export class AuthoritativeInventoryRelationships1790106600000 implements MigrationInterface {
|
|
name = 'AuthoritativeInventoryRelationships1790106600000';
|
|
|
|
public async up(q: QueryRunner): Promise<void> {
|
|
this.assertSource();
|
|
|
|
const companyType = await this.type(q, `SELECT id FROM asset_types WHERE operational_role='COMPANY' AND is_active=true ORDER BY (lower(code)='empresa') DESC, created_at LIMIT 1`);
|
|
const departmentType = await this.type(q, `SELECT id FROM asset_types WHERE lower(code)='departamento' AND is_active=true LIMIT 1`);
|
|
const areaType = await this.type(q, `SELECT id FROM asset_types WHERE lower(code)='area' AND operational_role='AREA' AND is_active=true LIMIT 1`);
|
|
const fieldType = await this.type(q, `SELECT id FROM asset_types WHERE lower(code)='yacimiento' AND is_active=true LIMIT 1`);
|
|
const installationType = await this.type(q, `SELECT id FROM asset_types WHERE lower(code)='instalacion' AND is_active=true LIMIT 1`);
|
|
const subinstallationType = await this.type(q, `SELECT id FROM asset_types WHERE lower(code)='subinstalacion' AND is_active=true LIMIT 1`);
|
|
|
|
await q.query(`
|
|
CREATE TABLE IF NOT EXISTS concession_types (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
code varchar(80) NOT NULL UNIQUE,
|
|
name varchar(120) NOT NULL UNIQUE,
|
|
is_active boolean NOT NULL DEFAULT true,
|
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT chk_concession_types_name CHECK (length(btrim(name)) >= 3)
|
|
)
|
|
`);
|
|
await q.query(`ALTER TABLE assets ADD COLUMN IF NOT EXISTS concession_type_id uuid`);
|
|
await q.query(`
|
|
DO $$ BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='fk_assets_concession_type') THEN
|
|
ALTER TABLE assets ADD CONSTRAINT fk_assets_concession_type
|
|
FOREIGN KEY (concession_type_id) REFERENCES concession_types(id) ON DELETE RESTRICT;
|
|
END IF;
|
|
END $$
|
|
`);
|
|
await q.query(`CREATE INDEX IF NOT EXISTS idx_assets_concession_type_id ON assets(concession_type_id)`);
|
|
await q.query(`
|
|
DO $$ BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dhv2_app') THEN
|
|
GRANT SELECT ON concession_types TO dhv2_app;
|
|
END IF;
|
|
END $$
|
|
`);
|
|
|
|
await q.query(`DELETE FROM asset_type_parent_rules WHERE child_type_id IN ($1::uuid,$2::uuid,$3::uuid,$4::uuid)`, [areaType, fieldType, installationType, subinstallationType]);
|
|
await q.query(`
|
|
INSERT INTO asset_type_parent_rules(child_type_id,parent_type_id) VALUES
|
|
($1::uuid,$2::uuid),($3::uuid,$1::uuid),($4::uuid,$3::uuid),($5::uuid,$4::uuid)
|
|
ON CONFLICT DO NOTHING
|
|
`, [areaType, departmentType, fieldType, installationType, subinstallationType]);
|
|
await q.query(`UPDATE asset_types SET can_be_root=true,updated_at=CURRENT_TIMESTAMP WHERE id IN ($1::uuid,$2::uuid)`, [companyType, departmentType]);
|
|
await q.query(`UPDATE asset_types SET can_be_root=false,updated_at=CURRENT_TIMESTAMP WHERE id IN ($1::uuid,$2::uuid,$3::uuid,$4::uuid)`, [areaType, fieldType, installationType, subinstallationType]);
|
|
await q.query(`UPDATE asset_types SET description='Área territorial. Sólo posee Nombre y pertenece obligatoriamente a un Departamento.',updated_at=CURRENT_TIMESTAMP WHERE id=$1::uuid`, [areaType]);
|
|
await q.query(`UPDATE asset_types SET description='Yacimiento. Pertenece a un Área y define directamente Tipo de concesión y Empresa relacionada.',updated_at=CURRENT_TIMESTAMP WHERE id=$1::uuid`, [fieldType]);
|
|
await q.query(`UPDATE asset_types SET description='Instalación física. Pertenece obligatoriamente a un Yacimiento y utiliza una clasificación técnica.',updated_at=CURRENT_TIMESTAMP WHERE id=$1::uuid`, [installationType]);
|
|
await q.query(`UPDATE asset_types SET description='Subinstalación física. Pertenece obligatoriamente a una Instalación y utiliza una clasificación técnica compatible.',updated_at=CURRENT_TIMESTAMP WHERE id=$1::uuid`, [subinstallationType]);
|
|
|
|
await q.query(`DELETE FROM asset_attribute_definitions WHERE asset_type_id IN ($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::uuid,$6::uuid)`, [companyType, departmentType, areaType, fieldType, installationType, subinstallationType]);
|
|
await this.insertCommonAttributes(q, installationType, [
|
|
['campo_marca','Marca',10],
|
|
['tipo_instalacion','Tipo de instalación',20],
|
|
['campo_modelo','Modelo',30],
|
|
['campo_capacidad','Capacidad',40],
|
|
['campo_numero_serie','Número de serie',50],
|
|
['campo_funcion','Función',60],
|
|
]);
|
|
await this.insertCommonAttributes(q, subinstallationType, [
|
|
['campo_marca','Marca',10],
|
|
['campo_modelo','Modelo',20],
|
|
['campo_capacidad','Capacidad',30],
|
|
['campo_numero_serie','Número de serie',40],
|
|
['campo_funcion','Función',50],
|
|
]);
|
|
|
|
await q.query(`
|
|
DO $$ DECLARE trigger_name text; BEGIN
|
|
FOR trigger_name IN
|
|
SELECT trigger_row.tgname
|
|
FROM pg_trigger trigger_row
|
|
JOIN pg_proc function_row ON function_row.oid=trigger_row.tgfoid
|
|
WHERE trigger_row.tgrelid='assets'::regclass
|
|
AND NOT trigger_row.tgisinternal
|
|
AND function_row.proname='enforce_asset_operational_context'
|
|
LOOP
|
|
EXECUTE format('DROP TRIGGER %I ON assets',trigger_name);
|
|
END LOOP;
|
|
END $$
|
|
`);
|
|
await q.query(`DROP FUNCTION IF EXISTS enforce_asset_operational_context()`);
|
|
|
|
await q.query(`
|
|
CREATE OR REPLACE FUNCTION enforce_authoritative_inventory_relationships()
|
|
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
kind text;
|
|
asset_role asset_type_operational_role;
|
|
parent_kind text;
|
|
parent_area uuid;
|
|
parent_company uuid;
|
|
parent_family uuid;
|
|
company_role asset_type_operational_role;
|
|
family_level text;
|
|
BEGIN
|
|
SELECT lower(code),operational_role INTO kind,asset_role FROM asset_types WHERE id=NEW.asset_type_id;
|
|
IF kind IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='asset type does not exist'; END IF;
|
|
|
|
IF asset_role='COMPANY'::asset_type_operational_role THEN
|
|
IF NEW.parent_id IS NOT NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Empresa no admite padre'; END IF;
|
|
NEW.operational_area_id:=NULL; NEW.operator_company_id:=NULL; NEW.concession_type_id:=NULL;
|
|
NEW.inventory_family_id:=NULL; NEW.is_inventory_instance:=false;
|
|
RETURN NEW;
|
|
ELSIF kind='departamento' THEN
|
|
IF NEW.parent_id IS NOT NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Departamento no admite padre'; END IF;
|
|
NEW.operational_area_id:=NULL; NEW.operator_company_id:=NULL; NEW.concession_type_id:=NULL;
|
|
NEW.inventory_family_id:=NULL; NEW.is_inventory_instance:=false;
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
IF NEW.parent_id IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='El nivel requiere un padre estructural'; END IF;
|
|
SELECT lower(parent_type.code), parent.operational_area_id, parent.operator_company_id, parent.inventory_family_id
|
|
INTO parent_kind,parent_area,parent_company,parent_family
|
|
FROM assets parent JOIN asset_types parent_type ON parent_type.id=parent.asset_type_id
|
|
WHERE parent.id=NEW.parent_id AND parent.information_status<>'INACTIVE';
|
|
IF parent_kind IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='El padre estructural no existe o está inactivo'; END IF;
|
|
|
|
IF kind='area' THEN
|
|
IF parent_kind<>'departamento' THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Área debe pertenecer a un Departamento'; END IF;
|
|
NEW.operational_area_id:=NULL; NEW.operator_company_id:=NULL; NEW.concession_type_id:=NULL;
|
|
NEW.inventory_family_id:=NULL; NEW.is_inventory_instance:=false;
|
|
ELSIF kind='yacimiento' THEN
|
|
IF parent_kind<>'area' THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Yacimiento debe pertenecer a un Área'; END IF;
|
|
IF NEW.operator_company_id IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Yacimiento requiere Empresa relacionada'; END IF;
|
|
SELECT type.operational_role INTO company_role FROM assets company JOIN asset_types type ON type.id=company.asset_type_id
|
|
WHERE company.id=NEW.operator_company_id AND company.information_status<>'INACTIVE' AND type.is_active=true;
|
|
IF company_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='La Empresa relacionada del Yacimiento no es válida'; END IF;
|
|
IF NEW.concession_type_id IS NULL OR NOT EXISTS(SELECT 1 FROM concession_types c WHERE c.id=NEW.concession_type_id AND c.is_active=true) THEN
|
|
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Yacimiento requiere un Tipo de concesión válido';
|
|
END IF;
|
|
IF NEW.operational_area_id IS NOT NULL AND NEW.operational_area_id<>NEW.parent_id THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='El Área operativa del Yacimiento debe coincidir con su Área padre'; END IF;
|
|
NEW.operational_area_id:=NEW.parent_id; NEW.inventory_family_id:=NULL; NEW.is_inventory_instance:=false;
|
|
ELSIF kind='instalacion' THEN
|
|
IF parent_kind<>'yacimiento' THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Instalación debe pertenecer a un Yacimiento'; END IF;
|
|
SELECT level::text INTO family_level FROM inventory_families WHERE id=NEW.inventory_family_id AND is_active=true;
|
|
IF family_level IS DISTINCT FROM 'INSTALLATION' THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Instalación requiere un Tipo de instalación válido'; END IF;
|
|
NEW.operational_area_id:=parent_area; NEW.operator_company_id:=parent_company; NEW.concession_type_id:=NULL; NEW.is_inventory_instance:=true;
|
|
ELSIF kind='subinstalacion' THEN
|
|
IF parent_kind<>'instalacion' THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Subinstalación debe pertenecer a una Instalación'; END IF;
|
|
SELECT level::text INTO family_level FROM inventory_families WHERE id=NEW.inventory_family_id AND is_active=true;
|
|
IF family_level IS DISTINCT FROM 'SUBINSTALLATION' THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Subinstalación requiere un Tipo de subinstalación válido'; END IF;
|
|
IF NOT EXISTS(SELECT 1 FROM inventory_family_parent_rules rule WHERE rule.child_family_id=NEW.inventory_family_id AND rule.parent_family_id=parent_family) THEN
|
|
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='El Tipo de subinstalación no es compatible con el Tipo de instalación padre';
|
|
END IF;
|
|
NEW.operational_area_id:=parent_area; NEW.operator_company_id:=parent_company; NEW.concession_type_id:=NULL; NEW.is_inventory_instance:=true;
|
|
END IF;
|
|
RETURN NEW;
|
|
END $$
|
|
`);
|
|
await q.query(`DROP TRIGGER IF EXISTS trg_assets_authoritative_inventory_relationships ON assets`);
|
|
await q.query(`
|
|
CREATE TRIGGER trg_assets_authoritative_inventory_relationships
|
|
BEFORE INSERT OR UPDATE OF asset_type_id,parent_id,operational_area_id,operator_company_id,inventory_family_id,concession_type_id
|
|
ON assets FOR EACH ROW EXECUTE FUNCTION enforce_authoritative_inventory_relationships()
|
|
`);
|
|
|
|
await q.query(`TRUNCATE TABLE assets CASCADE`);
|
|
await q.query(`TRUNCATE TABLE source_documents CASCADE`);
|
|
await q.query(`TRUNCATE TABLE inventory_families CASCADE`);
|
|
await q.query(`TRUNCATE TABLE finding_categories CASCADE`);
|
|
await q.query(`TRUNCATE TABLE concession_types CASCADE`);
|
|
|
|
const [territoryDoc] = (await q.query(`
|
|
INSERT INTO source_documents(document_type,document_number,title,issuer,external_reference,notes)
|
|
VALUES('SPREADSHEET',$1,$2,'Dirección de Hidrocarburos',$3,$4) RETURNING id
|
|
`, [TERRITORY_DOC,'modelo_yacimientos.sql','sha256:6a1d96bc8af7e755dd0e2fd9faf08335625b9c76865208c3b8413d868aa5eb1a','Modelo relacional exacto de Departamentos, Áreas, Yacimientos, Empresas y Tipo de concesión.'])) as IdRow[];
|
|
const [inventoryDoc] = (await q.query(`
|
|
INSERT INTO source_documents(document_type,document_number,title,issuer,external_reference,notes)
|
|
VALUES('SPREADSHEET',$1,$2,'Dirección de Hidrocarburos',$3,$4) RETURNING id
|
|
`, [INVENTORY_DOC,'modelo_relacional instalaciones y sub.sql','sha256:fb513f3909e78db298361a8a6998ca77026748f9b03cbab44c77b884ca862b49','Modelo relacional exacto de tipos de Instalación/Subinstalación y sus Hallazgos.'])) as IdRow[];
|
|
if(!territoryDoc?.id || !inventoryDoc?.id) throw new Error('Authoritative source documents could not be created');
|
|
|
|
const concessionIds=new Map<number,string>();
|
|
for(const row of SOURCE.concessionTypes){
|
|
const [created]=(await q.query(`INSERT INTO concession_types(code,name,is_active) VALUES($1,$2,true) RETURNING id`,[sourceCode('CONC',row.id),row.nombre])) as IdRow[];
|
|
if(!created?.id)throw new Error(`Could not create concession ${row.nombre}`); concessionIds.set(row.id,created.id);
|
|
}
|
|
|
|
await this.seedFamiliesAndFindings(q);
|
|
|
|
const companyIds=new Map<number,string>();
|
|
for(const row of SOURCE.companies){
|
|
const id=await this.asset(q,companyType,null,null,null,null,sourceCode('ORG',row.id),row.nombre,'modelo_yacimientos.sql',`empresas:${row.id}`);
|
|
companyIds.set(row.id,id);
|
|
await q.query(`INSERT INTO organization_profiles(asset_id,organization_kind,legal_name) VALUES($1::uuid,$2::organization_kind,$3)`,[id,row.nombre.toUpperCase().startsWith('UTE (')?'UTE':'COMPANY',row.nombre]);
|
|
await this.link(q,id,territoryDoc.id,`Empresa · source id ${row.id}`);
|
|
}
|
|
const departmentIds=new Map<number,string>();
|
|
for(const row of SOURCE.departments){
|
|
const id=await this.asset(q,departmentType,null,null,null,null,sourceCode('DEP',row.id),row.nombre,'modelo_yacimientos.sql',`departamentos:${row.id}`);
|
|
departmentIds.set(row.id,id); await this.link(q,id,territoryDoc.id,`Departamento · source id ${row.id}`);
|
|
}
|
|
const areaIds=new Map<number,string>();
|
|
for(const row of SOURCE.areas){
|
|
const parent=departmentIds.get(row.departamento_id); if(!parent)throw new Error(`Missing source Departamento ${row.departamento_id}`);
|
|
const id=await this.asset(q,areaType,parent,null,null,null,sourceCode('AREA',row.id),row.nombre,'modelo_yacimientos.sql',`areas:${row.id}`);
|
|
areaIds.set(row.id,id); await this.link(q,id,territoryDoc.id,`Área · source id ${row.id}`);
|
|
}
|
|
for(const row of SOURCE.fields){
|
|
const area=areaIds.get(row.area_id); const company=companyIds.get(row.empresa_id); const concession=concessionIds.get(row.tipo_concesion_id);
|
|
if(!area||!company||!concession)throw new Error(`Incomplete source relations for Yacimiento ${row.id}`);
|
|
const id=await this.asset(q,fieldType,area,area,company,concession,sourceCode('YAC',row.id),row.nombre,'modelo_yacimientos.sql',`yacimientos:${row.id}`);
|
|
await this.link(q,id,territoryDoc.id,`Yacimiento · source id ${row.id}`);
|
|
}
|
|
|
|
await q.query(`TRUNCATE TABLE area_company_relations CASCADE`);
|
|
await q.query(`TRUNCATE TABLE area_legal_rights CASCADE`);
|
|
const projectedPairs=new Set<string>();
|
|
const projectedRights=new Set<string>();
|
|
for(const row of SOURCE.fields){
|
|
const area=areaIds.get(row.area_id)!; const company=companyIds.get(row.empresa_id)!; const concession=concessionIds.get(row.tipo_concesion_id)!;
|
|
const pair=`${area}|${company}`;
|
|
if(!projectedPairs.has(pair)){
|
|
projectedPairs.add(pair);
|
|
await q.query(`INSERT INTO area_company_relations(area_id,company_id,relation_role,source_document_id,valid_from,start_reason)
|
|
VALUES($1::uuid,$2::uuid,'OPERATOR',$3::uuid,CURRENT_TIMESTAMP,$4)`,[area,company,territoryDoc.id,'Derivado automáticamente de Yacimientos del modelo autoritativo']);
|
|
}
|
|
const rightKey=`${area}|${concession}`;
|
|
if(!projectedRights.has(rightKey)){
|
|
projectedRights.add(rightKey);
|
|
const concessionRow=SOURCE.concessionTypes.find((item)=>item.id===row.tipo_concesion_id)!;
|
|
await q.query(`INSERT INTO area_legal_rights(area_id,right_type,name,status,source_document_id,notes)
|
|
VALUES($1::uuid,$2::area_legal_right_type,$3,'ACTIVE',$4::uuid,$5)`,[
|
|
area,concessionRow.nombre==='Exploración'?'EXPLORATION_PERMIT':'EXPLOITATION_CONCESSION',
|
|
`${concessionRow.nombre} · proyección del Yacimiento`,territoryDoc.id,'Compatibilidad derivada; el Tipo de concesión canónico está en Yacimiento.']);
|
|
}
|
|
}
|
|
|
|
await this.installFamilyFindingSync(q);
|
|
await this.verify(q);
|
|
}
|
|
|
|
public async down():Promise<void>{
|
|
throw new Error('Authoritative inventory model is a deliberate clean-load migration; restore the deploy PRE database backup.');
|
|
}
|
|
|
|
private assertSource():void{
|
|
if(SOURCE.departments.length!==7||SOURCE.companies.length!==13||SOURCE.concessionTypes.length!==2||SOURCE.areas.length!==64||SOURCE.fields.length!==230)
|
|
throw new Error('Authoritative territory source cardinality mismatch');
|
|
if(SOURCE.installationFamilies.length!==14||SOURCE.subinstallationFamilies.length!==109||SOURCE.findings.length!==181||SOURCE.installationFindings.length!==48||SOURCE.subinstallationFindings.length!==880)
|
|
throw new Error('Authoritative inventory source cardinality mismatch');
|
|
for(const area of SOURCE.areas) if(!SOURCE.departments.some((d)=>d.id===area.departamento_id)) throw new Error(`Area ${area.id} has no source Departamento`);
|
|
for(const field of SOURCE.fields){
|
|
if(!SOURCE.areas.some((a)=>a.id===field.area_id)||!SOURCE.companies.some((c)=>c.id===field.empresa_id)||!SOURCE.concessionTypes.some((c)=>c.id===field.tipo_concesion_id))
|
|
throw new Error(`Yacimiento ${field.id} has invalid source relationship`);
|
|
}
|
|
}
|
|
|
|
private async insertCommonAttributes(q:QueryRunner,typeId:string,rows:Array<[string,string,number]>):Promise<void>{
|
|
for(const [code,name,sortOrder] of rows) await q.query(`
|
|
INSERT INTO asset_attribute_definitions(asset_type_id,code,name,data_type,is_required,is_active,unit,options,sort_order)
|
|
VALUES($1::uuid,$2,$3,'TEXT'::asset_attribute_data_type,false,true,NULL,NULL,$4)
|
|
`,[typeId,code,name,sortOrder]);
|
|
}
|
|
|
|
private async seedFamiliesAndFindings(q:QueryRunner):Promise<void>{
|
|
const installationIds=new Map<number,string>();
|
|
for(const row of SOURCE.installationFamilies){
|
|
const [created]=(await q.query(`INSERT INTO inventory_families(code,name,level,information_labels,source_reference,is_active)
|
|
VALUES($1,$2,'INSTALLATION','[]'::jsonb,$3,true) RETURNING id`,[sourceCode('AUTH-I',row.id),row.nombre,`modelo_relacional:instalaciones:${row.id}`])) as IdRow[];
|
|
if(!created?.id)throw new Error(`Could not create Instalación family ${row.id}`); installationIds.set(row.id,created.id);
|
|
}
|
|
const subIds=new Map<number,string>();
|
|
for(const row of SOURCE.subinstallationFamilies){
|
|
const [created]=(await q.query(`INSERT INTO inventory_families(code,name,level,information_labels,source_reference,is_active)
|
|
VALUES($1,$2,'SUBINSTALLATION','[]'::jsonb,$3,true) RETURNING id`,[sourceCode('AUTH-S',row.id),row.nombre,`modelo_relacional:subinstalaciones:${row.id}`])) as IdRow[];
|
|
const parent=installationIds.get(row.instalacion_id); if(!created?.id||!parent)throw new Error(`Invalid Subinstalación family ${row.id}`);
|
|
subIds.set(row.id,created.id);
|
|
await q.query(`INSERT INTO inventory_family_parent_rules(child_family_id,parent_family_id) VALUES($1::uuid,$2::uuid)`,[created.id,parent]);
|
|
}
|
|
|
|
const [category]=(await q.query(`INSERT INTO finding_categories(code,name,sort_order,is_active) VALUES($1,$2,300,true) RETURNING id`,[FINDING_CATEGORY_CODE,'DH · Hallazgos del modelo autoritativo'])) as IdRow[];
|
|
if(!category?.id)throw new Error('Could not create authoritative finding category');
|
|
const findingIds=new Map<number,string>();
|
|
for(const row of SOURCE.findings){
|
|
const [created]=(await q.query(`INSERT INTO finding_catalog_items(category_id,code,source_number,title,import_note,revision,is_active)
|
|
VALUES($1::uuid,$2,$3,$4,$5,1,true) RETURNING id`,[category.id,sourceCode('AUTH-H',row.id),row.id,row.nombre,'modelo_relacional instalaciones y sub.sql'])) as IdRow[];
|
|
if(!created?.id)throw new Error(`Could not create Hallazgo ${row.id}`); findingIds.set(row.id,created.id);
|
|
await q.query(`INSERT INTO finding_catalog_item_versions(item_id,revision,snapshot,actor_username)
|
|
SELECT item.id,item.revision,jsonb_build_object('id',item.id,'categoryId',item.category_id,'code',item.code,'sourceNumber',item.source_number,'title',item.title,'revision',item.revision,'isActive',item.is_active),'migration:AUTHORITATIVE'
|
|
FROM finding_catalog_items item WHERE item.id=$1::uuid`,[created.id]);
|
|
}
|
|
for(const mapping of SOURCE.installationFindings){
|
|
const family=installationIds.get(mapping.instalacion_id); const finding=findingIds.get(mapping.hallazgo_id);
|
|
if(!family||!finding)throw new Error('Invalid installation finding mapping');
|
|
await q.query(`INSERT INTO finding_catalog_item_inventory_families(catalog_item_id,inventory_family_id) VALUES($1::uuid,$2::uuid)`,[finding,family]);
|
|
}
|
|
for(const mapping of SOURCE.subinstallationFindings){
|
|
const family=subIds.get(mapping.subinstalacion_id); const finding=findingIds.get(mapping.hallazgo_id);
|
|
if(!family||!finding)throw new Error('Invalid subinstallation finding mapping');
|
|
await q.query(`INSERT INTO finding_catalog_item_inventory_families(catalog_item_id,inventory_family_id) VALUES($1::uuid,$2::uuid)`,[finding,family]);
|
|
}
|
|
}
|
|
|
|
private async installFamilyFindingSync(q:QueryRunner):Promise<void>{
|
|
await q.query(`
|
|
CREATE OR REPLACE FUNCTION sync_asset_inventory_family_catalog()
|
|
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
DELETE FROM finding_catalog_asset_overrides WHERE asset_id=NEW.id AND reason LIKE 'AUTHORITATIVE familia técnica:%';
|
|
IF NEW.inventory_family_id IS NOT NULL THEN
|
|
INSERT INTO finding_catalog_asset_overrides(asset_id,catalog_item_id,is_enabled,reason,created_by,updated_by)
|
|
SELECT NEW.id,m.catalog_item_id,true,'AUTHORITATIVE familia técnica: catálogo contextual automático',NEW.created_by,NEW.updated_by
|
|
FROM finding_catalog_item_inventory_families m WHERE m.inventory_family_id=NEW.inventory_family_id
|
|
ON CONFLICT(asset_id,catalog_item_id) DO UPDATE SET is_enabled=true,reason='AUTHORITATIVE familia técnica: catálogo contextual automático',updated_by=NEW.updated_by,updated_at=CURRENT_TIMESTAMP;
|
|
END IF;
|
|
RETURN NEW;
|
|
END $$
|
|
`);
|
|
await q.query(`
|
|
CREATE OR REPLACE FUNCTION sync_inventory_family_mapping_assets()
|
|
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF TG_OP='DELETE' THEN
|
|
DELETE FROM finding_catalog_asset_overrides o USING assets a
|
|
WHERE o.asset_id=a.id AND a.inventory_family_id=OLD.inventory_family_id AND o.catalog_item_id=OLD.catalog_item_id AND o.reason LIKE 'AUTHORITATIVE familia técnica:%';
|
|
RETURN OLD;
|
|
END IF;
|
|
INSERT INTO finding_catalog_asset_overrides(asset_id,catalog_item_id,is_enabled,reason,created_by,updated_by)
|
|
SELECT a.id,NEW.catalog_item_id,true,'AUTHORITATIVE familia técnica: catálogo contextual automático',a.created_by,a.updated_by
|
|
FROM assets a WHERE a.inventory_family_id=NEW.inventory_family_id
|
|
ON CONFLICT(asset_id,catalog_item_id) DO UPDATE SET is_enabled=true,reason='AUTHORITATIVE familia técnica: catálogo contextual automático',updated_at=CURRENT_TIMESTAMP;
|
|
RETURN NEW;
|
|
END $$
|
|
`);
|
|
}
|
|
|
|
private async asset(q:QueryRunner,typeId:string,parentId:string|null,areaId:string|null,companyId:string|null,concessionId:string|null,
|
|
code:string,name:string,sourceName:string,sourceReference:string):Promise<string>{
|
|
const [row]=(await q.query(`INSERT INTO assets(asset_type_id,parent_id,operational_area_id,operator_company_id,concession_type_id,inventory_family_id,
|
|
code,name,information_status,operational_status,data_origin,source_name,source_reference,is_inventory_instance)
|
|
VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::uuid,NULL,$6,$7,'VALIDATED','UNKNOWN','PROVIDED_DOCUMENT',$8,$9,false) RETURNING id`,
|
|
[typeId,parentId,areaId,companyId,concessionId,code,name,sourceName,sourceReference])) as IdRow[];
|
|
if(!row?.id)throw new Error(`Could not seed ${code}`); return row.id;
|
|
}
|
|
|
|
private async link(q:QueryRunner,assetId:string,documentId:string,notes:string):Promise<void>{
|
|
await q.query(`INSERT INTO asset_source_documents(asset_id,document_id,relation_type,notes) VALUES($1::uuid,$2::uuid,'SOURCE',$3)`,[assetId,documentId,notes]);
|
|
}
|
|
|
|
private async type(q:QueryRunner,sql:string):Promise<string>{
|
|
const rows=(await q.query(sql)) as IdRow[]; if(!rows[0]?.id)throw new Error(`Missing canonical asset type: ${sql}`); return rows[0].id;
|
|
}
|
|
|
|
private async verify(q:QueryRunner):Promise<void>{
|
|
const [row]=await q.query(`SELECT
|
|
(SELECT COUNT(*) FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE t.operational_role='COMPANY')::integer companies,
|
|
(SELECT COUNT(*) FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE lower(t.code)='departamento')::integer departments,
|
|
(SELECT COUNT(*) FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE lower(t.code)='area')::integer areas,
|
|
(SELECT COUNT(*) FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE lower(t.code)='yacimiento')::integer fields,
|
|
(SELECT COUNT(*) FROM concession_types)::integer concessions,
|
|
(SELECT COUNT(*) FROM inventory_families WHERE level='INSTALLATION')::integer installation_families,
|
|
(SELECT COUNT(*) FROM inventory_families WHERE level='SUBINSTALLATION')::integer subinstallation_families,
|
|
(SELECT COUNT(*) FROM finding_catalog_items)::integer findings,
|
|
(SELECT COUNT(*) FROM finding_catalog_item_inventory_families m JOIN inventory_families f ON f.id=m.inventory_family_id WHERE f.level='INSTALLATION')::integer installation_mappings,
|
|
(SELECT COUNT(*) FROM finding_catalog_item_inventory_families m JOIN inventory_families f ON f.id=m.inventory_family_id WHERE f.level='SUBINSTALLATION')::integer subinstallation_mappings,
|
|
(SELECT COUNT(*) FROM assets y JOIN asset_types t ON t.id=y.asset_type_id JOIN assets a ON a.id=y.parent_id WHERE lower(t.code)='yacimiento' AND (y.operational_area_id IS DISTINCT FROM a.id OR y.operator_company_id IS NULL OR y.concession_type_id IS NULL))::integer invalid_fields`);
|
|
const expected:Record<string,number>={companies:13,departments:7,areas:64,fields:230,concessions:2,installation_families:14,subinstallation_families:109,findings:181,installation_mappings:48,subinstallation_mappings:880,invalid_fields:0};
|
|
for(const [key,value] of Object.entries(expected)) if(Number(row?.[key]??-1)!==value)throw new Error(`Authoritative inventory verification failed: ${key}=${row?.[key]} expected=${value}`);
|
|
}
|
|
}
|