import { randomUUID } from 'node:crypto'; import { BadRequestException, ConflictException, Injectable, NotFoundException, } from '@nestjs/common'; import { DataSource, EntityManager } from 'typeorm'; import { administrationAuditContext, isUniqueViolation } from '../administration/common/administration-audit'; import { AuditService } from '../audit/audit.service'; import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context'; import { AssetDataOrigin, AssetInformationStatus, AssetOperationalStatus, AssetVersionChangeType, AuditAction, } from '../database/entities'; import type { CreateInventoryStructureDto, InventoryStructureKind } from './dto/create-inventory-structure.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; code: string; name: string; level: 'INSTALLATION' | 'SUBINSTALLATION'; legacyTypeCode: string | null; informationLabels: string[]; parentFamilyIds: string[]; parentFamilies: FamilyParent[]; }; type ParentRow = { id: string; code: string; 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', YACIMIENTO: 'yacimiento', INSTALACION: 'instalacion', SUBINSTALACION: 'subinstalacion', }; const PARENT_TYPE_BY_KIND: Record = { EMPRESA: null, DEPARTAMENTO: null, AREA: 'departamento', YACIMIENTO: 'area', INSTALACION: 'yacimiento', SUBINSTALACION: 'instalacion', }; const FAMILY_LEVEL_BY_KIND: Partial> = { INSTALACION: 'INSTALLATION', SUBINSTALACION: 'SUBINSTALLATION', }; @Injectable() export class InventoryStructureService { constructor( private readonly dataSource: DataSource, private readonly audit: AuditService, private readonly history: AssetHistoryService, ) {} async options() { const types = (await this.dataSource.query(` SELECT id,code,name FROM asset_types WHERE ( lower(code) IN ('departamento','area','yacimiento','instalacion','subinstalacion') OR operational_role='COMPANY' ) AND is_active=true ORDER BY CASE WHEN operational_role='COMPANY' THEN 0 WHEN lower(code)='departamento' THEN 1 WHEN lower(code)='area' THEN 2 WHEN lower(code)='yacimiento' THEN 3 WHEN lower(code)='instalacion' THEN 4 WHEN lower(code)='subinstalacion' THEN 5 ELSE 9 END `)) as StructureTypeRow[]; const company = types.find((item) => ['empresa','organizacion'].includes(item.code.toLowerCase())); const departamento = types.find((item) => item.code.toLowerCase()==='departamento'); const area = types.find((item) => item.code.toLowerCase()==='area'); const yacimiento = types.find((item) => item.code.toLowerCase()==='yacimiento'); const instalacion = types.find((item) => item.code.toLowerCase()==='instalacion'); const subinstalacion = types.find((item) => item.code.toLowerCase()==='subinstalacion'); if (!company || !departamento || !area || !yacimiento || !instalacion || !subinstalacion) { throw new ConflictException({ code: 'INVENTORY_STRUCTURE_TYPES_INCOMPLETE', message: 'La configuración maestra de Empresa e Inventario todavía no está completa', }); } const families = (await this.dataSource.query(` SELECT family.id,family.code,family.name,family.level, family.legacy_type_code AS "legacyTypeCode", family.information_labels AS "informationLabels", COALESCE((SELECT JSONB_AGG(rule.parent_family_id ORDER BY parent.name,parent.code) FROM inventory_family_parent_rules rule JOIN inventory_families parent ON parent.id=rule.parent_family_id WHERE rule.child_family_id=family.id),'[]'::jsonb) AS "parentFamilyIds", COALESCE((SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id',parent.id,'code',parent.code,'name',parent.name) ORDER BY parent.name,parent.code) FROM inventory_family_parent_rules rule JOIN inventory_families parent ON parent.id=rule.parent_family_id WHERE rule.child_family_id=family.id),'[]'::jsonb) AS "parentFamilies" FROM inventory_families family WHERE family.is_active=true 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 }, ], levels: [ { kind: 'DEPARTAMENTO', label: 'Departamento', type: departamento, parentKind: null, requiresFamily: false }, { kind: 'AREA', label: 'Área', type: area, parentKind: 'DEPARTAMENTO', requiresFamily: false }, { kind: 'YACIMIENTO', label: 'Yacimiento', type: yacimiento, parentKind: 'AREA', requiresFamily: false }, { 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'), }; } async parents(kindValue: string, search?: string) { const kind = kindValue.toUpperCase() as InventoryStructureKind; if (!(kind in PARENT_TYPE_BY_KIND) || kind === 'DEPARTAMENTO' || kind === 'EMPRESA') { throw new BadRequestException({ code: 'INVENTORY_STRUCTURE_PARENT_KIND_INVALID', message: 'El nivel indicado no requiere un registro padre', }); } const expectedType = PARENT_TYPE_BY_KIND[kind]; const parameters: unknown[] = [expectedType]; let searchSql = ''; if (search?.trim()) { parameters.push(`%${search.trim()}%`); searchSql = `AND (asset.code ILIKE $2 OR asset.name ILIKE $2 OR COALESCE(asset.common_name,'') ILIKE $2)`; } const rows = await this.dataSource.query(` SELECT asset.id,asset.code,asset.name,asset.common_name AS "commonName", JSONB_BUILD_OBJECT('id',type.id,'code',type.code,'name',type.name) AS type, 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 ) END AS "inventoryFamily", CASE WHEN parent.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT( 'id',parent.id,'code',parent.code,'name',parent.name ) END AS parent FROM assets asset JOIN asset_types type ON type.id=asset.asset_type_id LEFT JOIN inventory_families family ON family.id=asset.inventory_family_id LEFT JOIN assets parent ON parent.id=asset.parent_id WHERE lower(type.code)=lower($1::text) AND asset.information_status<>'INACTIVE' ${searchSql} ORDER BY asset.name,asset.code LIMIT 100 `, parameters); return { data: rows }; } async create( dto: CreateInventoryStructureDto, principal: AuthPrincipal, request: RequestWithContext, ) { try { return await this.dataSource.transaction(async (manager) => { 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,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,$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, dto.commonName ?? null, dto.description ?? null, AssetInformationStatus.DRAFT, AssetOperationalStatus.UNKNOWN, AssetDataOrigin.MANUAL, 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}` : yacimientoContext ? `Tipo de concesión: ${yacimientoContext.concessionName}` : null, principal.userId, ])) as Array<{ id: string }>; const id = inserted[0]?.id; if (!id) throw new Error('No se pudo crear el registro'); if (dto.kind === 'EMPRESA') { await manager.query(` INSERT INTO organization_profiles(asset_id,organization_kind,legal_name,updated_by) VALUES ($1::uuid,'COMPANY',$2,$3::uuid) 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, ); await manager.query(` 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,$4,CURRENT_TIMESTAMP,$5,$6,'WEB',$7,$8) `, [ 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); await this.audit.record({ ...administrationAuditContext(principal, request), action: AuditAction.ASSET_CREATED, entityType: 'asset',entityId: id, afterData: created as unknown as Record, metadata: { versionNumber,inventoryStructureKind: dto.kind, inventoryFamilyId: family?.id ?? null,inventoryFamilyCode: family?.code ?? null, operatorCompanyId, concessionTypeId, }, }, manager); return created; }); } catch (error) { if (isUniqueViolation(error)) throw new ConflictException({ code: 'ASSET_CODE_ALREADY_EXISTS',message: 'Ya existe un registro con ese código', }); throw error; } } private async requireStructureType(manager: EntityManager, kind: InventoryStructureKind): Promise { const sql = kind === 'EMPRESA' ? `SELECT id,code,name FROM asset_types WHERE operational_role='COMPANY' AND is_active=true ORDER BY (lower(code)='empresa') DESC,created_at LIMIT 1` : `SELECT id,code,name FROM asset_types WHERE lower(code)=lower($1::text) AND is_active=true LIMIT 1`; const params = kind === 'EMPRESA' ? [] : [TYPE_CODE_BY_KIND[kind as Exclude]]; const rows = (await manager.query(sql,params)) as StructureTypeRow[]; if (!rows[0]) throw new ConflictException({ code: 'INVENTORY_STRUCTURE_TYPE_NOT_CONFIGURED',message: `El nivel ${kind} no está configurado`, }); return rows[0]; } private async requireParent( manager: EntityManager, kind: InventoryStructureKind, parentId: string | null, ): Promise { const expectedType = PARENT_TYPE_BY_KIND[kind]; if (!expectedType) { if (parentId) throw new BadRequestException({ code: 'INVENTORY_ROOT_MUST_NOT_HAVE_PARENT', message: kind === 'EMPRESA' ? 'Una Empresa es un maestro independiente y no puede tener padre' : 'Un Departamento es un registro raíz y no puede tener padre', }); return null; } if (!parentId) throw new BadRequestException({ code: 'INVENTORY_STRUCTURE_PARENT_REQUIRED', message: `Para crear ${kind.toLowerCase()} primero tenés que elegir su ${expectedType}`, }); const rows = (await manager.query(` SELECT asset.id,asset.code,asset.name,type.code AS "typeCode", 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' FOR KEY SHARE `, [parentId])) as ParentRow[]; const parent = rows[0]; if (!parent) throw new NotFoundException({ code: 'INVENTORY_STRUCTURE_PARENT_NOT_FOUND', message: 'El registro padre no existe' }); if (parent.typeCode.toLowerCase() !== expectedType) throw new BadRequestException({ code: 'INVENTORY_STRUCTURE_PARENT_INVALID', message: 'La jerarquía requerida es Departamento → Área → Yacimiento → Instalación → Subinstalación', }); 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 UNION ALL SELECT parent.id,parent.parent_id,parent.asset_type_id FROM assets parent JOIN lineage child ON child.parent_id=parent.id ) SELECT lineage.id FROM lineage JOIN asset_types type ON type.id=lineage.asset_type_id WHERE type.operational_role='AREA' LIMIT 1 `,[parent.id])) as IdRow[]; const areaId=rows[0]?.id; if (!areaId) throw new ConflictException({ code:'INVENTORY_STRUCTURE_AREA_ANCESTOR_MISSING', message:'La ubicación seleccionada no pertenece a un Área válida', }); return areaId; } private async requireFamily( manager: EntityManager, kind: InventoryStructureKind, familyId: string | null, parent: ParentRow | null, ): Promise { const expectedLevel = FAMILY_LEVEL_BY_KIND[kind]; if (!expectedLevel) { if (familyId) throw new BadRequestException({ code: 'INVENTORY_STRUCTURE_FAMILY_NOT_ALLOWED', message: 'Empresa, Departamento, Área y Yacimiento no llevan clasificación técnica', }); return null; } if (!familyId) throw new BadRequestException({ code: 'INVENTORY_STRUCTURE_FAMILY_REQUIRED', message: `Elegí la clasificación técnica de la ${kind.toLowerCase()}`, }); const rows = (await manager.query(` SELECT family.id,family.code,family.name,family.level, family.legacy_type_code AS "legacyTypeCode",family.information_labels AS "informationLabels", COALESCE((SELECT JSONB_AGG(rule.parent_family_id ORDER BY rule.parent_family_id) FROM inventory_family_parent_rules rule WHERE rule.child_family_id=family.id),'[]'::jsonb) AS "parentFamilyIds", COALESCE((SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id',parent.id,'code',parent.code,'name',parent.name) ORDER BY parent.name,parent.code) FROM inventory_family_parent_rules rule JOIN inventory_families parent ON parent.id=rule.parent_family_id WHERE rule.child_family_id=family.id),'[]'::jsonb) AS "parentFamilies" FROM inventory_families family WHERE family.id=$1::uuid AND family.is_active=true LIMIT 1 `, [familyId])) as FamilyRow[]; const family = rows[0]; if (!family) throw new NotFoundException({ code: 'INVENTORY_FAMILY_NOT_FOUND', message: 'La clasificación técnica no existe' }); if (family.level !== expectedLevel) throw new BadRequestException({ code: 'INVENTORY_FAMILY_LEVEL_INVALID', message: 'La clasificación técnica no corresponde al nivel seleccionado', }); if (kind === 'SUBINSTALACION') { if (!parent?.inventoryFamilyId) throw new BadRequestException({ code:'INVENTORY_PARENT_FAMILY_REQUIRED', message:'La Instalación padre debe tener una clasificación técnica válida', }); const [compatible]=(await manager.query(` SELECT 1 AS ok FROM inventory_family_parent_rules WHERE child_family_id=$1::uuid AND parent_family_id=$2::uuid LIMIT 1 `,[family.id,parent.inventoryFamilyId])) as Array<{ok:number}>; if (!compatible) throw new BadRequestException({ code: 'INVENTORY_SUBINSTALLATION_FAMILY_PARENT_INVALID', message: 'Ese tipo de Subinstalación no es compatible con la clasificación de la Instalación seleccionada', }); } 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' : kind === 'AREA' ? 'AREA' : kind === 'YACIMIENTO' ? 'YAC' : kind === 'INSTALACION' ? 'INST' : 'SUB'; const readable = name.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toUpperCase() .replace(/[^A-Z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'REGISTRO'; return `${prefix}-${readable}-${randomUUID().slice(0, 8).toUpperCase()}`.slice(0, 120); } private async loadView(manager: EntityManager, id: string) { const rows = await manager.query(` SELECT asset.id,asset.code,asset.name,asset.common_name AS "commonName",asset.description, asset.information_status AS "informationStatus",asset.operational_status AS "operationalStatus", 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 ) END AS "inventoryFamily", asset.created_at AS "createdAt",asset.updated_at AS "updatedAt" FROM assets asset 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]); return rows[0]; } }