diff --git a/api-v3/src/asset-master/inventory-structure.service.ts b/api-v3/src/asset-master/inventory-structure.service.ts index 9424d9f..c5d155b 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 FamilyParent = { id: string; code: string; name: string }; type FamilyRow = { id: string; code: string; @@ -27,9 +28,8 @@ type FamilyRow = { level: 'INSTALLATION' | 'SUBINSTALLATION'; legacyTypeCode: string | null; informationLabels: string[]; - parentFamilyId: string | null; - parentFamilyCode: string | null; - parentFamilyName: string | null; + parentFamilyIds: string[]; + parentFamilies: FamilyParent[]; }; type ParentRow = { id: string; @@ -101,10 +101,15 @@ export class InventoryStructureService { SELECT family.id,family.code,family.name,family.level, family.legacy_type_code AS "legacyTypeCode", family.information_labels AS "informationLabels", - parent.id AS "parentFamilyId",parent.code AS "parentFamilyCode",parent.name AS "parentFamilyName" + 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 - LEFT JOIN inventory_family_parent_rules rule ON rule.child_family_id=family.id - LEFT JOIN inventory_families parent ON parent.id=rule.parent_family_id WHERE family.is_active=true ORDER BY family.level,family.name,family.code `)) as FamilyRow[]; @@ -201,7 +206,7 @@ export class InventoryStructureService { AssetInformationStatus.DRAFT, AssetOperationalStatus.UNKNOWN, AssetDataOrigin.MANUAL, - dto.kind === 'EMPRESA' ? 'Maestro manual de Empresas F5.1' : 'Estructura manual de Inventario F5.1', + dto.kind === 'EMPRESA' ? 'Maestro manual de Empresas F6' : 'Estructura manual de Inventario F6', dto.kind === 'EMPRESA' ? 'inventory-master:empresa' : `inventory-structure:${dto.kind.toLowerCase()}`, family ? `Clasificación técnica: ${family.code} · ${family.name}` : null, principal.userId, @@ -218,11 +223,7 @@ export class InventoryStructureService { } const versionNumber = await this.history.capture( - manager, - id, - AssetVersionChangeType.CREATED, - principal, - request, + manager,id,AssetVersionChangeType.CREATED,principal,request, ); await manager.query(` INSERT INTO asset_context_history ( @@ -230,38 +231,28 @@ export class InventoryStructureService { change_reason,asset_version_number,source,request_id,created_by ) VALUES ($1,$2,$3,NULL,CURRENT_TIMESTAMP,$4,$5,'WEB',$6,$7) `, [ - id, - parent?.id ?? null, - operationalAreaId, - dto.kind === 'EMPRESA' ? 'Alta manual de Empresa independiente F5.1' : 'Alta manual de estructura de Inventario F5.1', - versionNumber, - request.requestId, - principal.userId, + id,parent?.id ?? null,operationalAreaId, + dto.kind === 'EMPRESA' ? 'Alta manual de Empresa independiente F6' : 'Alta manual de estructura de Inventario F6', + 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, + entityType: 'asset',entityId: id, afterData: created as unknown as Record, metadata: { - versionNumber, - inventoryStructureKind: dto.kind, - inventoryFamilyId: family?.id ?? null, - inventoryFamilyCode: family?.code ?? null, + versionNumber,inventoryStructureKind: dto.kind, + inventoryFamilyId: family?.id ?? null,inventoryFamilyCode: family?.code ?? null, operatorOwnership: false, }, }, 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', - }); - } + if (isUniqueViolation(error)) throw new ConflictException({ + code: 'ASSET_CODE_ALREADY_EXISTS',message: 'Ya existe un registro con ese código', + }); throw error; } } @@ -272,12 +263,9 @@ export class InventoryStructureService { : `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`, - }); - } + if (!rows[0]) throw new ConflictException({ + code: 'INVENTORY_STRUCTURE_TYPE_NOT_CONFIGURED',message: `El nivel ${kind} no está configurado`, + }); return rows[0]; } @@ -288,22 +276,18 @@ export class InventoryStructureService { ): 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', - }); - } + 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}`, - }); - } + 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" @@ -314,12 +298,10 @@ export class InventoryStructureService { `, [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', - }); - } + 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; } @@ -338,12 +320,10 @@ export class InventoryStructureService { 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', - }); - } + 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; } @@ -367,12 +347,13 @@ export class InventoryStructureService { }); 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", - parent.id AS "parentFamilyId",parent.code AS "parentFamilyCode",parent.name AS "parentFamilyName" + 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 - LEFT JOIN inventory_family_parent_rules rule ON rule.child_family_id=family.id - LEFT JOIN inventory_families parent ON parent.id=rule.parent_family_id WHERE family.id=$1::uuid AND family.is_active=true LIMIT 1 `, [familyId])) as FamilyRow[]; @@ -382,10 +363,19 @@ export class InventoryStructureService { code: 'INVENTORY_FAMILY_LEVEL_INVALID', message: 'La clasificación técnica no corresponde al nivel seleccionado', }); - if (kind === 'SUBINSTALACION' && family.parentFamilyId !== parent?.inventoryFamilyId) { - throw new BadRequestException({ + 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: 'La Subinstalación elegida no pertenece a la clasificación de la Instalación seleccionada', + message: 'Ese tipo de Subinstalación no es compatible con la clasificación de la Instalación seleccionada', }); } return family; @@ -398,13 +388,8 @@ export class InventoryStructureService { : 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'; + 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); }