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 { 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(); 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(); 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(); 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(); 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(); const projectedRights=new Set(); 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{ 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{ 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{ const installationIds=new Map(); 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(); 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(); 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{ 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{ 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{ 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{ 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{ 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={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}`); } }