From 439809abc7d629092a0d609ad7b254e122b6834f Mon Sep 17 00:00:00 2001 From: enlineawork Date: Fri, 11 Sep 2026 01:05:42 -0300 Subject: [PATCH] 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]);