From 583fd58e1c0c0f6f6bb4f26931d33a6dcb6bd697 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:48:57 -0300 Subject: [PATCH 01/29] feat: add authoritative inventory reference data --- .../authoritative-inventory-model.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 api-v3/src/reference-data/authoritative-inventory-model.ts diff --git a/api-v3/src/reference-data/authoritative-inventory-model.ts b/api-v3/src/reference-data/authoritative-inventory-model.ts new file mode 100644 index 0000000..f5e0167 --- /dev/null +++ b/api-v3/src/reference-data/authoritative-inventory-model.ts @@ -0,0 +1,30 @@ +import { inflateSync } from 'node:zlib'; + +export const AUTHORITATIVE_INVENTORY_MODEL_SOURCES = { + territory: { + file: 'modelo_yacimientos.sql', + sha256: '6a1d96bc8af7e755dd0e2fd9faf08335625b9c76865208c3b8413d868aa5eb1a', + }, + inventory: { + file: 'modelo_relacional instalaciones y sub.sql', + sha256: 'fb513f3909e78db298361a8a6998ca77026748f9b03cbab44c77b884ca862b49', + }, +} as const; + +const PAYLOAD = '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......'; + +export type AuthoritativeInventoryModel = { + departments: Array<{id:number;nombre:string}>; + companies: Array<{id:number;nombre:string}>; + concessionTypes: Array<{id:number;nombre:string}>; + areas: Array<{id:number;departamento_id:number;nombre:string}>; + fields: Array<{id:number;nombre:string;area_id:number;empresa_id:number;tipo_concesion_id:number}>; + installationFamilies: Array<{id:number;nombre:string}>; + subinstallationFamilies: Array<{id:number;instalacion_id:number;nombre:string}>; + findings: Array<{id:number;nombre:string}>; + subinstallationFindings: Array<{subinstalacion_id:number;hallazgo_id:number}>; + installationFindings: Array<{instalacion_id:number;hallazgo_id:number}>; +}; + +export const AUTHORITATIVE_INVENTORY_MODEL: AuthoritativeInventoryModel = + JSON.parse(inflateSync(Buffer.from(PAYLOAD, 'base64')).toString('utf8')) as AuthoritativeInventoryModel; From fb6b2388c379a0673098e56053dfe580bcb3e163 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:49:37 -0300 Subject: [PATCH 02/29] feat: add authoritative inventory payload chunk 1 --- .../reference-data/authoritative-inventory-model.payload-1.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 api-v3/src/reference-data/authoritative-inventory-model.payload-1.ts diff --git a/api-v3/src/reference-data/authoritative-inventory-model.payload-1.ts b/api-v3/src/reference-data/authoritative-inventory-model.payload-1.ts new file mode 100644 index 0000000..4df5d24 --- /dev/null +++ b/api-v3/src/reference-data/authoritative-inventory-model.payload-1.ts @@ -0,0 +1 @@ +export default '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'; From d2e82719f6a1c87053d941debaea7560acab5565 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:49:58 -0300 Subject: [PATCH 03/29] feat: add authoritative inventory payload chunk 2 --- .../reference-data/authoritative-inventory-model.payload-2.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 api-v3/src/reference-data/authoritative-inventory-model.payload-2.ts diff --git a/api-v3/src/reference-data/authoritative-inventory-model.payload-2.ts b/api-v3/src/reference-data/authoritative-inventory-model.payload-2.ts new file mode 100644 index 0000000..f4a2889 --- /dev/null +++ b/api-v3/src/reference-data/authoritative-inventory-model.payload-2.ts @@ -0,0 +1 @@ +export default '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'; From be543de927a225d9cb6ea838ec8f9340fa08091b Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:50:17 -0300 Subject: [PATCH 04/29] feat: add authoritative inventory payload chunk 3 --- .../reference-data/authoritative-inventory-model.payload-3.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 api-v3/src/reference-data/authoritative-inventory-model.payload-3.ts diff --git a/api-v3/src/reference-data/authoritative-inventory-model.payload-3.ts b/api-v3/src/reference-data/authoritative-inventory-model.payload-3.ts new file mode 100644 index 0000000..43a1c82 --- /dev/null +++ b/api-v3/src/reference-data/authoritative-inventory-model.payload-3.ts @@ -0,0 +1 @@ +export default '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'; From a98c9020b09234a578d45ffe0b772fcda4811f79 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:50:40 -0300 Subject: [PATCH 05/29] feat: add authoritative inventory payload chunk 4 --- .../reference-data/authoritative-inventory-model.payload-4.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 api-v3/src/reference-data/authoritative-inventory-model.payload-4.ts diff --git a/api-v3/src/reference-data/authoritative-inventory-model.payload-4.ts b/api-v3/src/reference-data/authoritative-inventory-model.payload-4.ts new file mode 100644 index 0000000..60d742f --- /dev/null +++ b/api-v3/src/reference-data/authoritative-inventory-model.payload-4.ts @@ -0,0 +1 @@ +export default 'yUcRMjmcTgBT37B4nq7FkVMEZE7HK6n4vvgCjr4V8Szb5Ifc1PcTFi270pwOLjpu6N+Oe1F5muxMfafg8ujPny0NQ4hVKh/nahfgeXqf7Q9lv6qH01BT39T3hjyp8OdrubJWbc9byLPxup0VFnZaBpfEWZk8iDut+g68bYM3+TqpPFvlq8rr3KBFp+NYNHgzYnrr3ZaM8+RjsiMLLXkko/tQvOZHGpTmcpxkcA4p+k1F+ngy+Ion+2KkfzwqVrXaRfciv0vvE7Oh/W0RjSvmkZZFjdPmRVV5viPz6LdfEuJ9LjFeDaOK76eLZTP1PWWX4tYFWSmb8qa1vhHs8MWrKaKFD8XpZuqbuJLQpofSkSmETMb01faDu7tkXX75paF96lEAFKu6y++Oa5XT6ud4t+sNGcmFwVUsvfjrIMBNfdfTS+E9PaR6IVKCIxfp7qHoD/tQN4LV3qQL0pmUaBfrRHKF1E6jRSBXPFz8Esc19d1Db8WXzU7y/okc0vK7Eaa+Mehyl2z3n/Ldg8pqJVRyVQSLD8mj8DjWGfmBxdu/OHCiCWK6zsnl28rT+DobR4x8sWSKiPkJRrjOB3l4oAgbbZS70nH5uFH3HzWsktCfQgHUdTdy45N4CVPffHO2yw+S4/4oNo+98fD0j88kk5K8KiGX2+wUnS9iA0J1C4Pd1DfNvAkv4lG8PJ3kM7KAp/PrcBIvxlNjSsZPLEzGy+lwNRbZmoJKjoM6akRlEi2NZ0Q6octKo1lZwHcORi3ZWUVk2d7El/Onv6xGZH+9QNLZq31CU98QMxyF83FoLKYTOUWvtrY80VSsA7VP5WI6G52MwPORNLUN5YvBeLKRWsYqsZtT5eurBXGqozP1fSQlclHNoZgOal9I4Z4sjEk0jBbhXLk+okZlnmN7ixWxT6yyFOFVYjANUUC1+aLk8wxX4eUpBXeqyZaztGpvRe2wyjLowm2Lm9XSuJz+Xl4EX2cIm/ouhvPolqaOX2tVRHA6JHdezhcqERt9TL+8fFMOJU+lkr1UIjkNAHq/WWabEum5nNJiGqVMm/q2gfpgpqlvBni1Em7OQkRIo5lwisv/pKFlvt3Ud/ir+CU1r0Rt3hcOSb9KVBFWWSnXn5T4TyHbhVtUSKTYYcgFJOmYrYo1K95qcRXekr6LhXgjNpGw2EPGRYmJmIN4Yep785FxPJ5NS5rFyWAumS/49zqpqe+/95wyemesLopsE2nhO/IUSNVisX8JZ+Un4zW8M55O4sI1NaJitxM7pny/Y9C8okJ7Th5cGf6frWSGK1GoS/J/nrNfND/NF81vQ+0wpeg8Gt6UCU2RNqeBs5tiTN2hVrvjdV0FLaBi8MVj2jSHQvi1wRm16d1pF9TeN1EiVGFxnWdRcvtqTg7981b9kv581hF6dEnSlUBVnS+FtLwssbpZcXUTmtGEYjlKgZOHGG9eZZF2hhlJymntpFNTiXsVFe/PR+sLTPTjcLRayIkNteHcyQqRbZ8PvxZ34z9sMjJ/JBtDiYE9JNunf7zYM4/kcDw8/U1YHftPyed8J2whYXhsjT0Z/sVN3cIdkewcJSRGhmAqenkRTLI9JhtT3wVOBGXnalBW7ej2vP8335FUImNSduaUKpIpz2TuXrKPCyHK1R3Z8/Rpppk4ViYizWR8oGcrkvNLlTeKfIzCZ8U9sVTsfpcrZSpZBC7i6Sw+5UjL56SdZTwTyTwyX2RFULuexZWjUAmQSWv0mkzW3hGVKyleflPuRv0iaL/e569XjwE61kDHKnQ2SOeAdC5Ix0E6D6TzQboApLMGKKGFEqIssVCeWChTLJQrFsoWC+WLhTLGQjnDUM4wlDMM5Qxr4owNapUNapUNapUNapUNapUNapUNapWNapWNapWNapWNapWNapWNapWNapWNapWNapWNapWNapWNapWNapWNapUDapUDapUDapUDapUDapUDapUDapWDapWDapWDapWDapWDapWDapWDapWDapWDapWDapWDapWDapWDapWDapULapULapULapULapULapULapULapWLapWLapWLapWLapWLapWLapWLapWLapWLapWLapWLapWLapWLapWLahUHtYqDWsVBreKgVnFQqzioVRzUKo5qFUe1iqNaxVGt4qhWcVSrOKpVHNUqjmoVR7WKo1rFUa3iqFZxVKs89GWqhMxBCV2UkKOEHkroo4ToW9sDlNBCCZtY6KOc8VHO+ChnfJQzPsoZH+WMj3LGRznjo5zxUc4EKGcClDMBypkA5UyAciZAOROgnAlQzgQoZwKUM6KSCGNNjdK2YUoHpnRhSg5TejBlEyctC14lC14lC14lC14lC14lC14lC14lOH5ao2xeJQavEoNXicGrxOBVYvAqwfGwGmXzKtnwKtnwKtnwKtnwKtnwKsHxjRpl8yo58Co58Co58Co58Co58CrB/mqNsnmVXHiVXHiVXHiVXHiVXHiVYP+jRtm8ShxeJQ6vEodXicOrxOFVgh2bGqUNUzoDmNKCKRlMacOUDkzpwpSN3Kza0Y4HU/owZYBSuo08CtCghAVb3TVKtzFDOoBTzQN09hpl8+wWmrS34Mkt1JGoUboMprRhSgemdGFK+N1dD6ZsXKVa3UeAUvIBTGnBlI08QjPzDE3NMzQ3z9DkPEOz8wxNzzM0P8/gBD2DM/QMTtEzOEfP4CQ9g7P0DE7TMzhPz+BEPYM9Ewan6hmcq2dwsp7B2XqGpusZmq9naMKeoRl7hqbsGZqzZ2jSnsFZewan7Rmct2dw4p7BmXsGp+4ZnLtncPKewdl7Bru3DM7fMziBz+AMPoNT+AzN4TM0ic/QLD5D0/gMzeMzNJHP0Ew+g1P5DM7lMziZz+BsPoPT+QzO5zM4oc/gjD6DU/oMjpEwOKnP4Kw+g9P6DM7rMzSxz9DMPkNT+wzN7TM0uc/Q7D5D0/sMzu8zOMHP4Aw/g1P8DM7xMzjJz+AsP4PT/AzO8zM40MbgTD+DU/0MzvUzONnP'; From 72a4f701775e5415d873d0a2276f11811bea49c7 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:50:59 -0300 Subject: [PATCH 06/29] feat: add authoritative inventory payload chunk 5 --- .../reference-data/authoritative-inventory-model.payload-5.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 api-v3/src/reference-data/authoritative-inventory-model.payload-5.ts diff --git a/api-v3/src/reference-data/authoritative-inventory-model.payload-5.ts b/api-v3/src/reference-data/authoritative-inventory-model.payload-5.ts new file mode 100644 index 0000000..7583491 --- /dev/null +++ b/api-v3/src/reference-data/authoritative-inventory-model.payload-5.ts @@ -0,0 +1 @@ +export default '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'; From fe6be393020aee9eb5da276237c691161ef5d04f Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:51:51 -0300 Subject: [PATCH 07/29] fix: compose authoritative inventory payload safely --- .../authoritative-inventory-model.ts | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/api-v3/src/reference-data/authoritative-inventory-model.ts b/api-v3/src/reference-data/authoritative-inventory-model.ts index f5e0167..5766ca5 100644 --- a/api-v3/src/reference-data/authoritative-inventory-model.ts +++ b/api-v3/src/reference-data/authoritative-inventory-model.ts @@ -1,4 +1,9 @@ import { inflateSync } from 'node:zlib'; +import payload1 from './authoritative-inventory-model.payload-1'; +import payload2 from './authoritative-inventory-model.payload-2'; +import payload3 from './authoritative-inventory-model.payload-3'; +import payload4 from './authoritative-inventory-model.payload-4'; +import payload5 from './authoritative-inventory-model.payload-5'; export const AUTHORITATIVE_INVENTORY_MODEL_SOURCES = { territory: { @@ -11,20 +16,26 @@ export const AUTHORITATIVE_INVENTORY_MODEL_SOURCES = { }, } as const; -const PAYLOAD = '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......'; - export type AuthoritativeInventoryModel = { - departments: Array<{id:number;nombre:string}>; - companies: Array<{id:number;nombre:string}>; - concessionTypes: Array<{id:number;nombre:string}>; - areas: Array<{id:number;departamento_id:number;nombre:string}>; - fields: Array<{id:number;nombre:string;area_id:number;empresa_id:number;tipo_concesion_id:number}>; - installationFamilies: Array<{id:number;nombre:string}>; - subinstallationFamilies: Array<{id:number;instalacion_id:number;nombre:string}>; - findings: Array<{id:number;nombre:string}>; - subinstallationFindings: Array<{subinstalacion_id:number;hallazgo_id:number}>; - installationFindings: Array<{instalacion_id:number;hallazgo_id:number}>; + departments: Array<{ id: number; nombre: string }>; + companies: Array<{ id: number; nombre: string }>; + concessionTypes: Array<{ id: number; nombre: string }>; + areas: Array<{ id: number; departamento_id: number; nombre: string }>; + fields: Array<{ + id: number; + nombre: string; + area_id: number; + empresa_id: number; + tipo_concesion_id: number; + }>; + installationFamilies: Array<{ id: number; nombre: string }>; + subinstallationFamilies: Array<{ id: number; instalacion_id: number; nombre: string }>; + findings: Array<{ id: number; nombre: string }>; + subinstallationFindings: Array<{ subinstalacion_id: number; hallazgo_id: number }>; + installationFindings: Array<{ instalacion_id: number; hallazgo_id: number }>; }; +const payload = payload1 + payload2 + payload3 + payload4 + payload5; + export const AUTHORITATIVE_INVENTORY_MODEL: AuthoritativeInventoryModel = - JSON.parse(inflateSync(Buffer.from(PAYLOAD, 'base64')).toString('utf8')) as AuthoritativeInventoryModel; + JSON.parse(inflateSync(Buffer.from(payload, 'base64')).toString('utf8')) as AuthoritativeInventoryModel; From 68d1af63ff2e0ce8a58b33bd14230079717312b5 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:58:36 -0300 Subject: [PATCH 08/29] feat(db): apply authoritative inventory relationships --- ...0-authoritative-inventory-relationships.ts | 380 ++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 api-v3/src/database/migrations/1790106600000-authoritative-inventory-relationships.ts diff --git a/api-v3/src/database/migrations/1790106600000-authoritative-inventory-relationships.ts b/api-v3/src/database/migrations/1790106600000-authoritative-inventory-relationships.ts new file mode 100644 index 0000000..4c47570 --- /dev/null +++ b/api-v3/src/database/migrations/1790106600000-authoritative-inventory-relationships.ts @@ -0,0 +1,380 @@ +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}`); + } +} From c1f51eb81f8bb1de0427deea88f51319fd80cde3 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:58:56 -0300 Subject: [PATCH 09/29] feat(api): expose Yacimiento concession relationship --- api-v3/src/database/entities/asset.entity.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api-v3/src/database/entities/asset.entity.ts b/api-v3/src/database/entities/asset.entity.ts index 79110e3..68f3bb6 100644 --- a/api-v3/src/database/entities/asset.entity.ts +++ b/api-v3/src/database/entities/asset.entity.ts @@ -33,6 +33,7 @@ export enum AssetDataOrigin { @Index('idx_assets_parent_id', ['parentId']) @Index('idx_assets_operational_area_id', ['operationalAreaId']) @Index('idx_assets_operator_company_id', ['operatorCompanyId']) +@Index('idx_assets_concession_type_id', ['concessionTypeId']) @Index('idx_assets_inventory_family_id', ['inventoryFamilyId']) @Index('idx_assets_information_status', ['informationStatus']) @Index('idx_assets_operational_status', ['operationalStatus']) @@ -54,6 +55,9 @@ export class Asset extends TimestampedEntity { @Column({ name: 'operator_company_id', type: 'uuid', nullable: true }) operatorCompanyId!: string | null; + @Column({ name: 'concession_type_id', type: 'uuid', nullable: true }) + concessionTypeId!: string | null; + @Column({ name: 'inventory_family_id', type: 'uuid', nullable: true }) inventoryFamilyId!: string | null; From e5c71a0c167ad0ebbee94505dc1197793ba77c17 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:59:18 -0300 Subject: [PATCH 10/29] feat(api): accept Yacimiento concession type --- api-v3/src/asset-master/dto/create-asset.dto.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api-v3/src/asset-master/dto/create-asset.dto.ts b/api-v3/src/asset-master/dto/create-asset.dto.ts index 7a9506f..deb68f0 100644 --- a/api-v3/src/asset-master/dto/create-asset.dto.ts +++ b/api-v3/src/asset-master/dto/create-asset.dto.ts @@ -54,6 +54,10 @@ export class CreateAssetDto { @IsUUID('4') operatorCompanyId?: string | null; + @IsOptional() + @IsUUID('4') + concessionTypeId?: string | null; + @IsOptional() @Transform(({ value }) => typeof value === 'string' && value.trim() ? value.trim() : null, From 06923d28496c3dc050833062a4dd1b8b042e40a1 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:59:28 -0300 Subject: [PATCH 11/29] feat(api): make Yacimiento company and concession editable --- api-v3/src/asset-master/dto/update-asset.dto.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/api-v3/src/asset-master/dto/update-asset.dto.ts b/api-v3/src/asset-master/dto/update-asset.dto.ts index 918c1c6..acc5251 100644 --- a/api-v3/src/asset-master/dto/update-asset.dto.ts +++ b/api-v3/src/asset-master/dto/update-asset.dto.ts @@ -1,6 +1,5 @@ import { Transform } from 'class-transformer'; import { - IsEmpty, IsObject, IsOptional, IsString, @@ -48,14 +47,14 @@ export class UpdateAssetDto { @IsUUID('4') operationalAreaId?: string | null; - /** - * Snapshot histórico/de compatibilidad. La Operadora vigente pertenece a la - * relación temporal Área↔Empresa y no puede editarse como propiedad física. - */ @IsOptional() - @IsEmpty({ message: 'La Operadora se administra en la relación temporal del Área, no en el Inventario' }) + @IsUUID('4') operatorCompanyId?: string | null; + @IsOptional() + @IsUUID('4') + concessionTypeId?: string | null; + @IsOptional() @Transform(({ value }) => typeof value === 'string' && value.trim() ? value.trim() : null, From 474e8d43107f2534578701ea9eb0972973e69602 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 00:59:56 -0300 Subject: [PATCH 12/29] test: lock authoritative inventory SQL model --- .../authoritative-inventory-model.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 api-v3/test/unit/authoritative-inventory-model.test.ts diff --git a/api-v3/test/unit/authoritative-inventory-model.test.ts b/api-v3/test/unit/authoritative-inventory-model.test.ts new file mode 100644 index 0000000..7379f39 --- /dev/null +++ b/api-v3/test/unit/authoritative-inventory-model.test.ts @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + AUTHORITATIVE_INVENTORY_MODEL as model, + AUTHORITATIVE_INVENTORY_MODEL_SOURCES as sources, +} from '../../src/reference-data/authoritative-inventory-model'; + +test('authoritative SQL sources keep their exact cardinalities', () => { + assert.equal(sources.territory.file, 'modelo_yacimientos.sql'); + assert.equal(sources.territory.sha256, '6a1d96bc8af7e755dd0e2fd9faf08335625b9c76865208c3b8413d868aa5eb1a'); + assert.equal(sources.inventory.file, 'modelo_relacional instalaciones y sub.sql'); + assert.equal(sources.inventory.sha256, 'fb513f3909e78db298361a8a6998ca77026748f9b03cbab44c77b884ca862b49'); + assert.equal(model.departments.length, 7); + assert.equal(model.companies.length, 13); + assert.equal(model.concessionTypes.length, 2); + assert.equal(model.areas.length, 64); + assert.equal(model.fields.length, 230); + assert.equal(model.installationFamilies.length, 14); + assert.equal(model.subinstallationFamilies.length, 109); + assert.equal(model.findings.length, 181); + assert.equal(model.installationFindings.length, 48); + assert.equal(model.subinstallationFindings.length, 880); +}); + +test('Area belongs only to Departamento while Yacimiento owns Area Empresa and concession', () => { + for (const area of model.areas) { + assert.ok(model.departments.some((department) => department.id === area.departamento_id)); + assert.deepEqual(Object.keys(area).sort(), ['departamento_id', 'id', 'nombre']); + } + for (const field of model.fields) { + assert.ok(model.areas.some((area) => area.id === field.area_id)); + assert.ok(model.companies.some((company) => company.id === field.empresa_id)); + assert.ok(model.concessionTypes.some((type) => type.id === field.tipo_concesion_id)); + assert.deepEqual(Object.keys(field).sort(), ['area_id', 'empresa_id', 'id', 'nombre', 'tipo_concesion_id']); + } +}); + +test('Subinstalación classifications belong to one installation classification and every mapping resolves', () => { + for (const subinstallation of model.subinstallationFamilies) { + assert.ok(model.installationFamilies.some((installation) => installation.id === subinstallation.instalacion_id)); + } + for (const mapping of model.installationFindings) { + assert.ok(model.installationFamilies.some((installation) => installation.id === mapping.instalacion_id)); + assert.ok(model.findings.some((finding) => finding.id === mapping.hallazgo_id)); + } + for (const mapping of model.subinstallationFindings) { + assert.ok(model.subinstallationFamilies.some((subinstallation) => subinstallation.id === mapping.subinstalacion_id)); + assert.ok(model.findings.some((finding) => finding.id === mapping.hallazgo_id)); + } +}); From cc75c768608b2b8517d481169da9b004d3e6edc2 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 01:02:15 -0300 Subject: [PATCH 13/29] feat(web): present authoritative inventory relationships --- .../AuthoritativeInventoryConfigPage.tsx | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 web-v2/src/pages/AuthoritativeInventoryConfigPage.tsx diff --git a/web-v2/src/pages/AuthoritativeInventoryConfigPage.tsx b/web-v2/src/pages/AuthoritativeInventoryConfigPage.tsx new file mode 100644 index 0000000..af221b1 --- /dev/null +++ b/web-v2/src/pages/AuthoritativeInventoryConfigPage.tsx @@ -0,0 +1,205 @@ +import { useEffect, useMemo, useState } from 'react'; +import { Link } from 'react-router'; +import { Alert, LoadingBlock, errorMessage } from '../components/Feedback'; +import { Icon } from '../components/Icon'; +import { getFindingCatalogAdmin, listAssetTypes } from '../lib/api'; +import type { AssetType, FindingAdminCatalog } from '../lib/api'; +import { listInventoryFamiliesAdmin } from '../lib/inventoryStructureApi'; +import type { InventoryFamily } from '../lib/inventoryStructureApi'; + +type CanonicalKind = 'EMPRESA' | 'DEPARTAMENTO' | 'AREA' | 'YACIMIENTO' | 'INSTALACION' | 'SUBINSTALACION'; + +type StructuralField = { + label: string; + detail: string; + required?: boolean; + relation?: boolean; +}; + +const LEVELS: Array<{ kind: CanonicalKind; label: string; description: string; fields: StructuralField[] }> = [ + { + kind: 'EMPRESA', + label: 'Empresa', + description: 'Maestro independiente. La Empresa se relaciona directamente con uno o más Yacimientos.', + fields: [{ label: 'Nombre', detail: 'Nombre de la Empresa / Operadora.', required: true }], + }, + { + kind: 'DEPARTAMENTO', + label: 'Departamento', + description: 'Raíz territorial. No depende de ningún otro nivel.', + fields: [{ label: 'Nombre', detail: 'Nombre del Departamento.', required: true }], + }, + { + kind: 'AREA', + label: 'Área', + description: 'El Área sólo tiene Nombre y pertenece obligatoriamente a un Departamento.', + fields: [ + { label: 'Departamento', detail: 'Relación obligatoria Departamento → Área.', required: true, relation: true }, + { label: 'Nombre', detail: 'Nombre del Área.', required: true }, + ], + }, + { + kind: 'YACIMIENTO', + label: 'Yacimiento', + description: 'El Yacimiento concentra el contexto operativo: Área, Tipo de concesión y Empresa relacionada.', + fields: [ + { label: 'Área', detail: 'Relación obligatoria Área → Yacimiento.', required: true, relation: true }, + { label: 'Tipo de concesión', detail: 'Explotación o Exploración, según la fuente.', required: true, relation: true }, + { label: 'Empresa relacionada', detail: 'Empresa / Operadora asociada directamente al Yacimiento.', required: true, relation: true }, + { label: 'Nombre', detail: 'Nombre del Yacimiento. Puede repetirse en otra Área.', required: true }, + ], + }, + { + kind: 'INSTALACION', + label: 'Instalación', + description: 'Cada Instalación pertenece a un Yacimiento y posee una clasificación técnica.', + fields: [ + { label: 'Yacimiento', detail: 'Relación obligatoria Yacimiento → Instalación.', required: true, relation: true }, + { label: 'Tipo de instalación', detail: 'Clasificación técnica tomada del modelo de Instalaciones.', required: true, relation: true }, + ], + }, + { + kind: 'SUBINSTALACION', + label: 'Subinstalación', + description: 'Cada Subinstalación pertenece a una Instalación y usa una clasificación compatible con ella.', + fields: [ + { label: 'Instalación', detail: 'Relación obligatoria Instalación → Subinstalación.', required: true, relation: true }, + { label: 'Tipo de subinstalación', detail: 'Clasificación técnica compatible con el Tipo de instalación padre.', required: true, relation: true }, + ], + }, +]; + +const EMPTY_CATALOG: FindingAdminCatalog = { categories: [], items: [] }; + +function canonicalType(types: AssetType[], kind: CanonicalKind): AssetType | null { + if (kind === 'EMPRESA') return types.find((type) => type.operationalRole === 'COMPANY' && type.isActive) ?? null; + if (kind === 'AREA') return types.find((type) => type.operationalRole === 'AREA' && type.isActive) ?? null; + return types.find((type) => type.code.toLowerCase() === kind.toLowerCase() && type.isActive) ?? null; +} + +export function AuthoritativeInventoryConfigPage() { + const [types, setTypes] = useState([]); + const [families, setFamilies] = useState([]); + const [catalog, setCatalog] = useState(EMPTY_CATALOG); + const [selectedKind, setSelectedKind] = useState('AREA'); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + Promise.all([listAssetTypes(), listInventoryFamiliesAdmin(), getFindingCatalogAdmin()]) + .then(([loadedTypes, loadedFamilies, loadedCatalog]) => { + setTypes(loadedTypes); + setFamilies(loadedFamilies); + setCatalog(loadedCatalog); + }) + .catch((requestError) => setError(errorMessage(requestError))) + .finally(() => setLoading(false)); + }, []); + + const level = LEVELS.find((item) => item.kind === selectedKind) ?? LEVELS[0]; + const selectedType = canonicalType(types, selectedKind); + const commonAttributes = useMemo(() => { + const attributes = selectedType?.attributes.filter((attribute) => attribute.isActive) ?? []; + // "Tipo de instalación" is represented by inventoryFamilyId, not duplicated + // as a free-text value in the authoritative structural presentation. + return selectedKind === 'INSTALACION' + ? attributes.filter((attribute) => attribute.code !== 'tipo_instalacion') + : attributes; + }, [selectedKind, selectedType]); + const installationFamilies = families.filter((family) => family.level === 'INSTALLATION' && family.isActive !== false); + const subinstallationFamilies = families.filter((family) => family.level === 'SUBINSTALLATION' && family.isActive !== false); + const activeFindings = catalog.items.filter((item) => item.isActive).length; + + if (loading) return ; + + return
+
+
+ ADMINISTRACIÓN · MODELO AUTORITATIVO +

Configuración de Inventarios

+

Relaciones fijas cargadas desde los SQL definitivos. Los vínculos estructurales no son campos de texto editables.

+
+ Agregar registro +
+ {error && {error}} + +
+
+
+ ESTRUCTURA FÍSICA +

Jerarquía obligatoria

+

Empresa es un maestro independiente y se asocia al Yacimiento. El árbol físico queda separado y sin ambigüedades.

+
+
+
+
1Departamentoraíz
+
2ÁreaDepartamento
+
3YacimientoÁrea + Empresa + concesión
+
4InstalaciónYacimiento
+
5SubinstalaciónInstalación
+
+
+ +

Empresa: ya no pertenece al Área. La relación canónica es Yacimiento → Empresa relacionada.

+
+
+ +
+
+
+ INFORMACIÓN ESTRUCTURAL +

{level.label}

+

{level.description}

+
+ Modelo SQL +
+
+ {LEVELS.map((item) => )} +
+ +
+ {level.fields.map((field, index) =>
+ {index + 1} + + {field.label} + {field.detail} + + + {field.relation && Relación} + {field.required && Obligatorio} + +
)} + {commonAttributes.map((attribute, index) =>
+ {level.fields.length + index + 1} + + {attribute.name} + {attribute.code} · Campo común del nivel + + {attribute.isRequired && Obligatorio} +
)} +
+
+ +
+
+
MODELO TÉCNICO

Tipos de Instalación

{installationFamilies.length} clasificaciones exactas del SQL.

+
+ {installationFamilies.map((family) =>
{family.name}{family.findingCount ?? 0} Hallazgos asociados{family.code}
)} +
+
+
+
MODELO TÉCNICO

Tipos de Subinstalación

{subinstallationFamilies.length} clasificaciones, cada una vinculada a su Tipo de instalación.

+
+ {subinstallationFamilies.map((family) =>
{family.name}{family.parentFamilies.map((parent) => parent.name).join(' · ') || 'Sin padre'} · {family.findingCount ?? 0} Hallazgos
)} +
+
+
+ +
+
+
HALLAZGOS

Catálogo contextual

{activeFindings} Hallazgos cargados con sus relaciones exactas a Instalaciones y Subinstalaciones.

+ Abrir catálogo +
+
+
; +} From 18e16c2ec22c34deaa672159677736a6b1833cd4 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 01:02:38 -0300 Subject: [PATCH 14/29] feat(web): use authoritative inventory configuration page --- web-v2/src/app/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web-v2/src/app/App.tsx b/web-v2/src/app/App.tsx index 18cf09d..de3b7c1 100644 --- a/web-v2/src/app/App.tsx +++ b/web-v2/src/app/App.tsx @@ -17,7 +17,7 @@ import { AssetsPage } from '../pages/AssetsPage'; import { AssetEditorPage } from '../pages/AssetEditorPage'; import { InventoryCreatePage } from '../pages/InventoryCreatePage'; import { FieldDiscoveriesPage } from '../pages/FieldDiscoveriesPage'; -import { AssetTypesPage } from '../pages/AssetTypesPage'; +import { AuthoritativeInventoryConfigPage } from '../pages/AuthoritativeInventoryConfigPage'; import { HistoryPage } from '../pages/HistoryPage'; import { TemporalAssetsPage } from '../pages/TemporalAssetsPage'; import { InspectionVisitsPage } from '../pages/InspectionVisitsPage'; @@ -79,7 +79,7 @@ export function App() { }>} /> }>} /> }>} /> - }>} /> + }>} /> }>} /> }>} /> } /> From 98b9af73449f8937f4ae2919dc9a39c95f07cd6c Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 01:05:00 -0300 Subject: [PATCH 15/29] feat(api): require Yacimiento company and concession relationships --- .../asset-master/dto/create-inventory-structure.dto.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api-v3/src/asset-master/dto/create-inventory-structure.dto.ts b/api-v3/src/asset-master/dto/create-inventory-structure.dto.ts index b604d3c..876c512 100644 --- a/api-v3/src/asset-master/dto/create-inventory-structure.dto.ts +++ b/api-v3/src/asset-master/dto/create-inventory-structure.dto.ts @@ -11,6 +11,7 @@ import { // Invariante de dominio: Empresa es un maestro independiente. La jerarquía física // se expresa sólo como Departamento → Área → Yacimiento → Instalación → Subinstalación. +// La Empresa y el Tipo de concesión se relacionan directamente con el Yacimiento. export const INVENTORY_STRUCTURE_KINDS = [ 'EMPRESA', 'DEPARTAMENTO', @@ -52,6 +53,14 @@ export class CreateInventoryStructureDto { @IsUUID('4') familyId?: string | null; + @IsOptional() + @IsUUID('4') + operatorCompanyId?: string | null; + + @IsOptional() + @IsUUID('4') + concessionTypeId?: string | null; + @IsOptional() @Transform(({ value }) => typeof value === 'string' && value.trim() ? value.trim() : null) @IsString() From 439809abc7d629092a0d609ad7b254e122b6834f Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 01:05:42 -0300 Subject: [PATCH 16/29] feat(api): make Yacimiento own company and concession --- .../inventory-structure.service.ts | 182 ++++++++++++++++-- 1 file changed, 171 insertions(+), 11 deletions(-) diff --git a/api-v3/src/asset-master/inventory-structure.service.ts b/api-v3/src/asset-master/inventory-structure.service.ts index c5d155b..5d58a7f 100644 --- a/api-v3/src/asset-master/inventory-structure.service.ts +++ b/api-v3/src/asset-master/inventory-structure.service.ts @@ -20,6 +20,7 @@ import type { CreateInventoryStructureDto, InventoryStructureKind } from './dto/ import { AssetHistoryService } from './asset-history.service'; type StructureTypeRow = { id: string; code: string; name: string }; +type SimpleOptionRow = { id: string; code: string; name: string }; type FamilyParent = { id: string; code: string; name: string }; type FamilyRow = { id: string; @@ -37,9 +38,17 @@ type ParentRow = { name: string; typeCode: string; inventoryFamilyId: string | null; + operationalAreaId: string | null; + operatorCompanyId: string | null; }; type IdRow = { id: string }; +type YacimientoContext = { + companyId: string; + concessionTypeId: string; + concessionName: string; +}; + const TYPE_CODE_BY_KIND: Record, string> = { DEPARTAMENTO: 'departamento', AREA: 'area', @@ -114,6 +123,21 @@ export class InventoryStructureService { ORDER BY family.level,family.name,family.code `)) as FamilyRow[]; + const companies = (await this.dataSource.query(` + SELECT asset.id,asset.code,COALESCE(profile.legal_name,asset.name) AS name + FROM assets asset + JOIN asset_types type ON type.id=asset.asset_type_id AND type.operational_role='COMPANY' AND type.is_active=true + LEFT JOIN organization_profiles profile ON profile.asset_id=asset.id + WHERE asset.information_status<>'INACTIVE' + ORDER BY COALESCE(profile.legal_name,asset.name),asset.code + `)) as SimpleOptionRow[]; + const concessionTypes = (await this.dataSource.query(` + SELECT id,code,name + FROM concession_types + WHERE is_active=true + ORDER BY CASE lower(name) WHEN 'explotación' THEN 1 WHEN 'exploración' THEN 2 ELSE 9 END,name,code + `)) as SimpleOptionRow[]; + return { independentMasters: [ { kind: 'EMPRESA', label: 'Empresa', type: company, parentKind: null, requiresFamily: false }, @@ -125,6 +149,8 @@ export class InventoryStructureService { { kind: 'INSTALACION', label: 'Instalación', type: instalacion, parentKind: 'YACIMIENTO', requiresFamily: true }, { kind: 'SUBINSTALACION', label: 'Subinstalación', type: subinstalacion, parentKind: 'INSTALACION', requiresFamily: true }, ], + companies, + concessionTypes, installationFamilies: families.filter((item) => item.level==='INSTALLATION'), subinstallationFamilies: families.filter((item) => item.level==='SUBINSTALLATION'), }; @@ -179,25 +205,43 @@ export class InventoryStructureService { const type = await this.requireStructureType(manager, dto.kind); const parent = await this.requireParent(manager, dto.kind, dto.parentId ?? null); const family = await this.requireFamily(manager, dto.kind, dto.familyId ?? null, parent); + const yacimientoContext = await this.requireYacimientoContext(manager, dto); const generatedCode = dto.code?.trim().toUpperCase() || this.generatedCode(dto.kind, dto.name); const operationalAreaId = parent && ['YACIMIENTO','INSTALACION','SUBINSTALACION'].includes(dto.kind) ? await this.resolveAreaId(manager, parent) : null; + const operatorCompanyId = dto.kind === 'YACIMIENTO' + ? yacimientoContext?.companyId ?? null + : dto.kind === 'INSTALACION' || dto.kind === 'SUBINSTALACION' + ? parent?.operatorCompanyId ?? null + : null; + const concessionTypeId = dto.kind === 'YACIMIENTO' + ? yacimientoContext?.concessionTypeId ?? null + : null; + + if ((dto.kind === 'INSTALACION' || dto.kind === 'SUBINSTALACION') && !operatorCompanyId) { + throw new ConflictException({ + code: 'INVENTORY_YACIMIENTO_COMPANY_MISSING', + message: 'El Yacimiento de origen no tiene una Empresa relacionada válida', + }); + } const inserted = (await manager.query(` INSERT INTO assets ( - asset_type_id,parent_id,operational_area_id,operator_company_id,inventory_family_id, + asset_type_id,parent_id,operational_area_id,operator_company_id,concession_type_id,inventory_family_id, code,name,common_name,description,information_status,operational_status, data_origin,source_name,source_reference,source_notes,created_by,updated_by,provenance_updated_by ) VALUES ( - $1::uuid,$2::uuid,$3::uuid,NULL,$4::uuid, - $5::varchar,$6::varchar,$7::varchar,$8::text,$9::asset_information_status,$10::asset_operational_status, - $11::varchar,$12::varchar,$13::varchar,$14::text,$15::uuid,$15::uuid,$15::uuid + $1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::uuid,$6::uuid, + $7::varchar,$8::varchar,$9::varchar,$10::text,$11::asset_information_status,$12::asset_operational_status, + $13::varchar,$14::varchar,$15::varchar,$16::text,$17::uuid,$17::uuid,$17::uuid ) RETURNING id `, [ type.id, parent?.id ?? null, operationalAreaId, + operatorCompanyId, + concessionTypeId, family?.id ?? null, generatedCode, dto.name, @@ -206,9 +250,13 @@ export class InventoryStructureService { AssetInformationStatus.DRAFT, AssetOperationalStatus.UNKNOWN, AssetDataOrigin.MANUAL, - dto.kind === 'EMPRESA' ? 'Maestro manual de Empresas F6' : 'Estructura manual de Inventario F6', + dto.kind === 'EMPRESA' ? 'Maestro manual de Empresas' : 'Estructura manual de Inventario', dto.kind === 'EMPRESA' ? 'inventory-master:empresa' : `inventory-structure:${dto.kind.toLowerCase()}`, - family ? `Clasificación técnica: ${family.code} · ${family.name}` : null, + family + ? `Clasificación técnica: ${family.code} · ${family.name}` + : yacimientoContext + ? `Tipo de concesión: ${yacimientoContext.concessionName}` + : null, principal.userId, ])) as Array<{ id: string }>; const id = inserted[0]?.id; @@ -221,6 +269,15 @@ export class InventoryStructureService { ON CONFLICT (asset_id) DO UPDATE SET legal_name=EXCLUDED.legal_name,updated_by=EXCLUDED.updated_by,updated_at=CURRENT_TIMESTAMP `,[id,dto.name,principal.userId]); } + if (dto.kind === 'YACIMIENTO' && operationalAreaId && operatorCompanyId && concessionTypeId) { + await this.ensureCompatibilityProjection( + manager, + operationalAreaId, + operatorCompanyId, + concessionTypeId, + yacimientoContext?.concessionName ?? 'Concesión', + ); + } const versionNumber = await this.history.capture( manager,id,AssetVersionChangeType.CREATED,principal,request, @@ -229,10 +286,14 @@ export class InventoryStructureService { INSERT INTO asset_context_history ( asset_id,parent_id,operational_area_id,operator_company_id,valid_from, change_reason,asset_version_number,source,request_id,created_by - ) VALUES ($1,$2,$3,NULL,CURRENT_TIMESTAMP,$4,$5,'WEB',$6,$7) + ) VALUES ($1,$2,$3,$4,CURRENT_TIMESTAMP,$5,$6,'WEB',$7,$8) `, [ - id,parent?.id ?? null,operationalAreaId, - dto.kind === 'EMPRESA' ? 'Alta manual de Empresa independiente F6' : 'Alta manual de estructura de Inventario F6', + id,parent?.id ?? null,operationalAreaId,operatorCompanyId, + dto.kind === 'YACIMIENTO' + ? 'Alta manual de Yacimiento con Área, Empresa y Tipo de concesión' + : dto.kind === 'EMPRESA' + ? 'Alta manual de Empresa independiente' + : 'Alta manual de estructura de Inventario', versionNumber,request.requestId,principal.userId, ]); const created = await this.loadView(manager, id); @@ -244,7 +305,8 @@ export class InventoryStructureService { metadata: { versionNumber,inventoryStructureKind: dto.kind, inventoryFamilyId: family?.id ?? null,inventoryFamilyCode: family?.code ?? null, - operatorOwnership: false, + operatorCompanyId, + concessionTypeId, }, }, manager); return created; @@ -290,7 +352,9 @@ export class InventoryStructureService { }); const rows = (await manager.query(` SELECT asset.id,asset.code,asset.name,type.code AS "typeCode", - asset.inventory_family_id AS "inventoryFamilyId" + asset.inventory_family_id AS "inventoryFamilyId", + asset.operational_area_id AS "operationalAreaId", + asset.operator_company_id AS "operatorCompanyId" FROM assets asset JOIN asset_types type ON type.id=asset.asset_type_id WHERE asset.id=$1::uuid AND asset.information_status<>'INACTIVE' @@ -305,8 +369,65 @@ export class InventoryStructureService { return parent; } + private async requireYacimientoContext( + manager: EntityManager, + dto: CreateInventoryStructureDto, + ): Promise { + if (dto.kind !== 'YACIMIENTO') { + if (dto.operatorCompanyId || dto.concessionTypeId) { + throw new BadRequestException({ + code: 'INVENTORY_YACIMIENTO_CONTEXT_NOT_ALLOWED', + message: 'Empresa relacionada y Tipo de concesión sólo corresponden al Yacimiento', + }); + } + return null; + } + if (!dto.operatorCompanyId) { + throw new BadRequestException({ + code: 'INVENTORY_YACIMIENTO_COMPANY_REQUIRED', + message: 'Seleccioná la Empresa relacionada del Yacimiento', + }); + } + if (!dto.concessionTypeId) { + throw new BadRequestException({ + code: 'INVENTORY_YACIMIENTO_CONCESSION_REQUIRED', + message: 'Seleccioná el Tipo de concesión del Yacimiento', + }); + } + const companyRows = (await manager.query(` + SELECT asset.id + FROM assets asset + JOIN asset_types type ON type.id=asset.asset_type_id + WHERE asset.id=$1::uuid + AND type.operational_role='COMPANY' + AND type.is_active=true + AND asset.information_status<>'INACTIVE' + FOR KEY SHARE + `,[dto.operatorCompanyId])) as IdRow[]; + if (!companyRows[0]) throw new BadRequestException({ + code: 'INVENTORY_YACIMIENTO_COMPANY_INVALID', + message: 'La Empresa relacionada seleccionada no es válida', + }); + const concessionRows = (await manager.query(` + SELECT id,name + FROM concession_types + WHERE id=$1::uuid AND is_active=true + FOR KEY SHARE + `,[dto.concessionTypeId])) as Array<{id:string;name:string}>; + if (!concessionRows[0]) throw new BadRequestException({ + code: 'INVENTORY_YACIMIENTO_CONCESSION_INVALID', + message: 'El Tipo de concesión seleccionado no es válido', + }); + return { + companyId: dto.operatorCompanyId, + concessionTypeId: dto.concessionTypeId, + concessionName: concessionRows[0].name, + }; + } + private async resolveAreaId(manager: EntityManager,parent: ParentRow):Promise { if (parent.typeCode.toLowerCase()==='area') return parent.id; + if (parent.operationalAreaId) return parent.operationalAreaId; const rows = (await manager.query(` WITH RECURSIVE lineage AS ( SELECT asset.id,asset.parent_id,asset.asset_type_id FROM assets asset WHERE asset.id=$1::uuid @@ -381,6 +502,40 @@ export class InventoryStructureService { return family; } + private async ensureCompatibilityProjection( + manager: EntityManager, + areaId: string, + companyId: string, + concessionTypeId: string, + concessionName: string, + ): Promise { + const companyProjection = await manager.query(` + SELECT id FROM area_company_relations + WHERE area_id=$1::uuid AND company_id=$2::uuid AND relation_role='OPERATOR' AND valid_to IS NULL + LIMIT 1 + `,[areaId,companyId]); + if (!companyProjection[0]) { + await manager.query(` + INSERT INTO area_company_relations(area_id,company_id,relation_role,valid_from,start_reason) + VALUES($1::uuid,$2::uuid,'OPERATOR',CURRENT_TIMESTAMP,'Proyección derivada de Yacimiento') + `,[areaId,companyId]); + } + const rightType = concessionName.toLocaleLowerCase('es-AR').includes('explor') + ? 'EXPLORATION_PERMIT' + : 'EXPLOITATION_CONCESSION'; + const rightProjection = await manager.query(` + SELECT id FROM area_legal_rights + WHERE area_id=$1::uuid AND right_type=$2::area_legal_right_type AND status='ACTIVE' + LIMIT 1 + `,[areaId,rightType]); + if (!rightProjection[0]) { + await manager.query(` + INSERT INTO area_legal_rights(area_id,right_type,name,status,notes) + VALUES($1::uuid,$2::area_legal_right_type,$3,'ACTIVE',$4) + `,[areaId,rightType,`${concessionName} · proyección de Yacimiento`,`Tipo de concesión canónico: ${concessionTypeId}`]); + } + } + private generatedCode(kind: InventoryStructureKind, name: string): string { const prefix = kind === 'EMPRESA' ? 'EMP' : kind === 'DEPARTAMENTO' ? 'DEP' @@ -400,6 +555,8 @@ export class InventoryStructureService { JSONB_BUILD_OBJECT('id',type.id,'code',type.code,'name',type.name) AS type, CASE WHEN parent.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT('id',parent.id,'code',parent.code,'name',parent.name) END AS parent, CASE WHEN area.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT('id',area.id,'code',area.code,'name',area.name) END AS "operationalArea", + CASE WHEN company.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT('id',company.id,'code',company.code,'name',COALESCE(profile.legal_name,company.name)) END AS "operatorCompany", + CASE WHEN concession.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT('id',concession.id,'code',concession.code,'name',concession.name) END AS "concessionType", CASE WHEN family.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT( 'id',family.id,'code',family.code,'name',family.name,'level',family.level, 'informationLabels',family.information_labels @@ -409,6 +566,9 @@ export class InventoryStructureService { JOIN asset_types type ON type.id=asset.asset_type_id LEFT JOIN assets parent ON parent.id=asset.parent_id LEFT JOIN assets area ON area.id=asset.operational_area_id + LEFT JOIN assets company ON company.id=asset.operator_company_id + LEFT JOIN organization_profiles profile ON profile.asset_id=company.id + LEFT JOIN concession_types concession ON concession.id=asset.concession_type_id LEFT JOIN inventory_families family ON family.id=asset.inventory_family_id WHERE asset.id=$1::uuid `, [id]); From b88147d376b1c3586c4157ed115187de4607d46b Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 01:05:56 -0300 Subject: [PATCH 17/29] feat(web): expose Yacimiento company and concession options --- web-v2/src/lib/inventoryStructureApi.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/web-v2/src/lib/inventoryStructureApi.ts b/web-v2/src/lib/inventoryStructureApi.ts index 8533f3d..3ff41ac 100644 --- a/web-v2/src/lib/inventoryStructureApi.ts +++ b/web-v2/src/lib/inventoryStructureApi.ts @@ -21,7 +21,15 @@ export interface InventoryTechnicalValues { values:Record; } export interface InventoryStructureLevel { kind:InventoryStructureKind; label:string; type:{id:string;code:string;name:string}; parentKind:InventoryStructureKind|null; requiresFamily:boolean; } -export interface InventoryStructureOptions { independentMasters:InventoryStructureLevel[]; levels:InventoryStructureLevel[]; installationFamilies:InventoryFamily[]; subinstallationFamilies:InventoryFamily[]; } +export interface InventoryStructureOption { id:string; code:string; name:string; } +export interface InventoryStructureOptions { + independentMasters:InventoryStructureLevel[]; + levels:InventoryStructureLevel[]; + companies:InventoryStructureOption[]; + concessionTypes:InventoryStructureOption[]; + installationFamilies:InventoryFamily[]; + subinstallationFamilies:InventoryFamily[]; +} export interface InventoryStructureParent { id:string; code:string; name:string; commonName:string|null; type:{id:string;code:string;name:string}; inventoryFamily:null|{id:string;code:string;name:string;level:'INSTALLATION'|'SUBINSTALLATION';informationLabels:string[]}; @@ -35,6 +43,8 @@ export interface InventoryFamilyFindings { family:InventoryFamily; items:Invento export interface CreatedInventoryStructure { id:string; code:string; name:string; commonName:string|null; description:string|null; informationStatus:string; operationalStatus:string; type:{id:string;code:string;name:string}; parent:null|{id:string;code:string;name:string}; operationalArea?:null|{id:string;code:string;name:string}; + operatorCompany?:null|{id:string;code:string;name:string}; + concessionType?:null|{id:string;code:string;name:string}; inventoryFamily:null|{id:string;code:string;name:string;level:string;informationLabels:string[]}; } @@ -53,4 +63,4 @@ export function updateInventoryFamily(familyId:string,input:{name?:string;parent export function createInventoryFamilyAttribute(familyId:string,input:{code:string;name:string;dataType:InventoryFamilyAttributeDataType;isRequired?:boolean;unit?:string|null;options?:string[];sortOrder?:number}){return apiRequest(`/inventory-families/${familyId}/attributes`,{method:'POST',body:JSON.stringify(input)});} export function updateInventoryFamilyAttribute(familyId:string,attributeId:string,input:{name?:string;dataType?:InventoryFamilyAttributeDataType;isRequired?:boolean;isActive?:boolean;unit?:string|null;options?:string[]|null;sortOrder?:number}){return apiRequest(`/inventory-families/${familyId}/attributes/${attributeId}`,{method:'PATCH',body:JSON.stringify(input)});} export function replaceInventoryFamilyFindings(familyId:string,input:{itemIds:string[];reason:string}){return apiRequest(`/inventory-families/${familyId}/findings`,{method:'PUT',body:JSON.stringify(input)});} -export function createInventoryStructure(input:{kind:InventoryStructureKind;code?:string|null;name:string;commonName?:string|null;parentId?:string|null;familyId?:string|null;description?:string|null}){return apiRequest('/inventory-structure',{method:'POST',body:JSON.stringify(input)});} +export function createInventoryStructure(input:{kind:InventoryStructureKind;code?:string|null;name:string;commonName?:string|null;parentId?:string|null;familyId?:string|null;operatorCompanyId?:string|null;concessionTypeId?:string|null;description?:string|null}){return apiRequest('/inventory-structure',{method:'POST',body:JSON.stringify(input)});} From 718e08be13b0572bc567c8ff329bebe684ec2c2e Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 01:06:18 -0300 Subject: [PATCH 18/29] feat(web): capture Yacimiento company and concession --- web-v2/src/pages/InventoryCreatePage.tsx | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/web-v2/src/pages/InventoryCreatePage.tsx b/web-v2/src/pages/InventoryCreatePage.tsx index fca1a07..ec822ee 100644 --- a/web-v2/src/pages/InventoryCreatePage.tsx +++ b/web-v2/src/pages/InventoryCreatePage.tsx @@ -44,6 +44,8 @@ export function InventoryCreatePage() { const [parentSearch, setParentSearch] = useState(''); const [parentId, setParentId] = useState(''); const [familyId, setFamilyId] = useState(''); + const [operatorCompanyId, setOperatorCompanyId] = useState(''); + const [concessionTypeId, setConcessionTypeId] = useState(''); const [familyFindings, setFamilyFindings] = useState(null); const [name, setName] = useState(''); const [code, setCode] = useState(''); @@ -66,6 +68,7 @@ export function InventoryCreatePage() { const requiresParent = Boolean(parentLabelByKind[kind]); const requiresFamily = kind === 'INSTALACION' || kind === 'SUBINSTALACION'; + const requiresYacimientoContext = kind === 'YACIMIENTO'; const parentLabel = parentLabelByKind[kind] ?? ''; useEffect(() => { @@ -99,18 +102,25 @@ export function InventoryCreatePage() { if (!requiresFamily) setFamilyId(''); if (kind==='SUBINSTALACION' && familyId && !families.some((item)=>item.id===familyId)) setFamilyId(''); },[kind,requiresFamily,families,familyId]); + useEffect(() => { + if (!requiresYacimientoContext) { setOperatorCompanyId(''); setConcessionTypeId(''); } + },[requiresYacimientoContext]); const changeKind=(next:InventoryStructureKind) => { - setKind(next); setParentId(''); setParentSearch(''); setFamilyId(''); setFamilyFindings(null); setError(''); + setKind(next); setParentId(''); setParentSearch(''); setFamilyId(''); setFamilyFindings(null); + setOperatorCompanyId(''); setConcessionTypeId(''); setError(''); }; const save=async(event:FormEvent) => { event.preventDefault(); if (requiresParent && !parentId) { setError(`Seleccioná ${parentLabel}.`); return; } + if (requiresYacimientoContext && !operatorCompanyId) { setError('Seleccioná la Empresa relacionada del Yacimiento.'); return; } + if (requiresYacimientoContext && !concessionTypeId) { setError('Seleccioná el Tipo de concesión del Yacimiento.'); return; } if (requiresFamily && !familyId) { setError(`Seleccioná el tipo de ${kindLabel(kind).toLowerCase()}.`); return; } setSaving(true); setError(''); try { const created=await createInventoryStructure({ kind,name:name.trim(),parentId:parentId || null,familyId:familyId || null, + operatorCompanyId:operatorCompanyId || null,concessionTypeId:concessionTypeId || null, code:code.trim() || null,commonName:commonName.trim() || null,description:description.trim() || null, }); navigate(`/inventarios/${created.id}`,{replace:true}); @@ -121,14 +131,15 @@ export function InventoryCreatePage() { if (loading) return ; return
-
CARGA MANUAL

Nuevo registro

Para crear sólo necesitás ubicación, clasificación cuando corresponda y nombre. DH genera el código automáticamente.

+
CARGA MANUAL

Nuevo registro

Modelo fijo: Departamento → Área → Yacimiento → Instalación → Subinstalación. La Empresa y el Tipo de concesión pertenecen al Yacimiento.

{error && {error}}
-

1. ¿Qué querés crear?

Departamento → Área → Yacimiento → Instalación → Subinstalación. Empresa queda fuera del árbol físico.

+

1. ¿Qué querés crear?

Área sólo depende de Departamento. Yacimiento define Área, Empresa relacionada y Tipo de concesión.

{requiresParent &&

2. Ubicación

Elegí el {parentLabel.toLowerCase()} concreto.

} - {requiresFamily &&

3. Clasificación técnica

Define los datos técnicos y Hallazgos aplicables. En Subinstalaciones sólo aparecen clasificaciones compatibles con la Instalación elegida.

{kind==='SUBINSTALACION' && !selectedParent?.inventoryFamily ? La Instalación elegida todavía no tiene clasificación técnica. : }{kind==='SUBINSTALACION' && selectedParent?.inventoryFamily && families.length===0 && No hay tipos de Subinstalación compatibles con {selectedParent.inventoryFamily.name}. Configuralos primero en Configuración de Inventarios.}{selectedFamily &&
{selectedFamily.name}{familyFindings ? `${familyFindings.count} Hallazgo${familyFindings.count===1?'':'s'} asociado${familyFindings.count===1?'':'s'}` : 'Cargando Hallazgos asociados…'}{familyFindings && familyFindings.items.length>0 &&
    {familyFindings.items.slice(0,6).map((item)=>
  • {item.title}
  • )}{familyFindings.items.length>6 &&
  • + {familyFindings.items.length-6} más
  • }
}
}
} -

{requiresFamily ? '4' : requiresParent ? '3' : '2'}. Nombre

Los datos técnicos se completan después, en la ficha del elemento.

Datos opcionales