feat(f6): aplicar compatibilidad múltiple al alta de Inventarios

This commit is contained in:
2026-09-09 09:23:32 -03:00
parent e181dea4cb
commit 0b074923be
@@ -20,6 +20,7 @@ import type { CreateInventoryStructureDto, InventoryStructureKind } from './dto/
import { AssetHistoryService } from './asset-history.service'; import { AssetHistoryService } from './asset-history.service';
type StructureTypeRow = { id: string; code: string; name: string }; type StructureTypeRow = { id: string; code: string; name: string };
type FamilyParent = { id: string; code: string; name: string };
type FamilyRow = { type FamilyRow = {
id: string; id: string;
code: string; code: string;
@@ -27,9 +28,8 @@ type FamilyRow = {
level: 'INSTALLATION' | 'SUBINSTALLATION'; level: 'INSTALLATION' | 'SUBINSTALLATION';
legacyTypeCode: string | null; legacyTypeCode: string | null;
informationLabels: string[]; informationLabels: string[];
parentFamilyId: string | null; parentFamilyIds: string[];
parentFamilyCode: string | null; parentFamilies: FamilyParent[];
parentFamilyName: string | null;
}; };
type ParentRow = { type ParentRow = {
id: string; id: string;
@@ -101,10 +101,15 @@ export class InventoryStructureService {
SELECT family.id,family.code,family.name,family.level, SELECT family.id,family.code,family.name,family.level,
family.legacy_type_code AS "legacyTypeCode", family.legacy_type_code AS "legacyTypeCode",
family.information_labels AS "informationLabels", 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 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 WHERE family.is_active=true
ORDER BY family.level,family.name,family.code ORDER BY family.level,family.name,family.code
`)) as FamilyRow[]; `)) as FamilyRow[];
@@ -201,7 +206,7 @@ export class InventoryStructureService {
AssetInformationStatus.DRAFT, AssetInformationStatus.DRAFT,
AssetOperationalStatus.UNKNOWN, AssetOperationalStatus.UNKNOWN,
AssetDataOrigin.MANUAL, 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()}`, 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}` : null,
principal.userId, principal.userId,
@@ -218,11 +223,7 @@ export class InventoryStructureService {
} }
const versionNumber = await this.history.capture( const versionNumber = await this.history.capture(
manager, manager,id,AssetVersionChangeType.CREATED,principal,request,
id,
AssetVersionChangeType.CREATED,
principal,
request,
); );
await manager.query(` await manager.query(`
INSERT INTO asset_context_history ( INSERT INTO asset_context_history (
@@ -230,38 +231,28 @@ export class InventoryStructureService {
change_reason,asset_version_number,source,request_id,created_by 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,NULL,CURRENT_TIMESTAMP,$4,$5,'WEB',$6,$7)
`, [ `, [
id, id,parent?.id ?? null,operationalAreaId,
parent?.id ?? null, dto.kind === 'EMPRESA' ? 'Alta manual de Empresa independiente F6' : 'Alta manual de estructura de Inventario F6',
operationalAreaId, versionNumber,request.requestId,principal.userId,
dto.kind === 'EMPRESA' ? 'Alta manual de Empresa independiente F5.1' : 'Alta manual de estructura de Inventario F5.1',
versionNumber,
request.requestId,
principal.userId,
]); ]);
const created = await this.loadView(manager, id); const created = await this.loadView(manager, id);
await this.audit.record({ await this.audit.record({
...administrationAuditContext(principal, request), ...administrationAuditContext(principal, request),
action: AuditAction.ASSET_CREATED, action: AuditAction.ASSET_CREATED,
entityType: 'asset', entityType: 'asset',entityId: id,
entityId: id,
afterData: created as unknown as Record<string, unknown>, afterData: created as unknown as Record<string, unknown>,
metadata: { metadata: {
versionNumber, versionNumber,inventoryStructureKind: dto.kind,
inventoryStructureKind: dto.kind, inventoryFamilyId: family?.id ?? null,inventoryFamilyCode: family?.code ?? null,
inventoryFamilyId: family?.id ?? null,
inventoryFamilyCode: family?.code ?? null,
operatorOwnership: false, operatorOwnership: false,
}, },
}, manager); }, manager);
return created; return created;
}); });
} catch (error) { } catch (error) {
if (isUniqueViolation(error)) { if (isUniqueViolation(error)) throw new ConflictException({
throw new ConflictException({ code: 'ASSET_CODE_ALREADY_EXISTS',message: 'Ya existe un registro con ese código',
code: 'ASSET_CODE_ALREADY_EXISTS', });
message: 'Ya existe un registro con ese código',
});
}
throw error; 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`; : `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<InventoryStructureKind,'EMPRESA'>]]; const params = kind === 'EMPRESA' ? [] : [TYPE_CODE_BY_KIND[kind as Exclude<InventoryStructureKind,'EMPRESA'>]];
const rows = (await manager.query(sql,params)) as StructureTypeRow[]; const rows = (await manager.query(sql,params)) as StructureTypeRow[];
if (!rows[0]) { if (!rows[0]) throw new ConflictException({
throw new ConflictException({ code: 'INVENTORY_STRUCTURE_TYPE_NOT_CONFIGURED',message: `El nivel ${kind} no está configurado`,
code: 'INVENTORY_STRUCTURE_TYPE_NOT_CONFIGURED', });
message: `El nivel ${kind} no está configurado`,
});
}
return rows[0]; return rows[0];
} }
@@ -288,22 +276,18 @@ export class InventoryStructureService {
): Promise<ParentRow | null> { ): Promise<ParentRow | null> {
const expectedType = PARENT_TYPE_BY_KIND[kind]; const expectedType = PARENT_TYPE_BY_KIND[kind];
if (!expectedType) { if (!expectedType) {
if (parentId) { if (parentId) throw new BadRequestException({
throw new BadRequestException({ code: 'INVENTORY_ROOT_MUST_NOT_HAVE_PARENT',
code: 'INVENTORY_ROOT_MUST_NOT_HAVE_PARENT', message: kind === 'EMPRESA'
message: kind === 'EMPRESA' ? 'Una Empresa es un maestro independiente y no puede tener padre'
? 'Una Empresa es un maestro independiente y no puede tener padre' : 'Un Departamento es un registro raíz y no puede tener padre',
: 'Un Departamento es un registro raíz y no puede tener padre', });
});
}
return null; return null;
} }
if (!parentId) { if (!parentId) throw new BadRequestException({
throw new BadRequestException({ code: 'INVENTORY_STRUCTURE_PARENT_REQUIRED',
code: 'INVENTORY_STRUCTURE_PARENT_REQUIRED', message: `Para crear ${kind.toLowerCase()} primero tenés que elegir su ${expectedType}`,
message: `Para crear ${kind.toLowerCase()} primero tenés que elegir su ${expectedType}`, });
});
}
const rows = (await manager.query(` const rows = (await manager.query(`
SELECT asset.id,asset.code,asset.name,type.code AS "typeCode", SELECT asset.id,asset.code,asset.name,type.code AS "typeCode",
asset.inventory_family_id AS "inventoryFamilyId" asset.inventory_family_id AS "inventoryFamilyId"
@@ -314,12 +298,10 @@ export class InventoryStructureService {
`, [parentId])) as ParentRow[]; `, [parentId])) as ParentRow[];
const parent = rows[0]; const parent = rows[0];
if (!parent) throw new NotFoundException({ code: 'INVENTORY_STRUCTURE_PARENT_NOT_FOUND', message: 'El registro padre no existe' }); if (!parent) throw new NotFoundException({ code: 'INVENTORY_STRUCTURE_PARENT_NOT_FOUND', message: 'El registro padre no existe' });
if (parent.typeCode.toLowerCase() !== expectedType) { if (parent.typeCode.toLowerCase() !== expectedType) throw new BadRequestException({
throw new BadRequestException({ code: 'INVENTORY_STRUCTURE_PARENT_INVALID',
code: 'INVENTORY_STRUCTURE_PARENT_INVALID', message: 'La jerarquía requerida es Departamento → Área → Yacimiento → Instalación → Subinstalación',
message: 'La jerarquía requerida es Departamento → Área → Yacimiento → Instalación → Subinstalación', });
});
}
return parent; return parent;
} }
@@ -338,12 +320,10 @@ export class InventoryStructureService {
LIMIT 1 LIMIT 1
`,[parent.id])) as IdRow[]; `,[parent.id])) as IdRow[];
const areaId=rows[0]?.id; const areaId=rows[0]?.id;
if (!areaId) { if (!areaId) throw new ConflictException({
throw new ConflictException({ code:'INVENTORY_STRUCTURE_AREA_ANCESTOR_MISSING',
code:'INVENTORY_STRUCTURE_AREA_ANCESTOR_MISSING', message:'La ubicación seleccionada no pertenece a un Área válida',
message:'La ubicación seleccionada no pertenece a un Área válida', });
});
}
return areaId; return areaId;
} }
@@ -367,12 +347,13 @@ export class InventoryStructureService {
}); });
const rows = (await manager.query(` const rows = (await manager.query(`
SELECT family.id,family.code,family.name,family.level, SELECT family.id,family.code,family.name,family.level,
family.legacy_type_code AS "legacyTypeCode", family.legacy_type_code AS "legacyTypeCode",family.information_labels AS "informationLabels",
family.information_labels AS "informationLabels", COALESCE((SELECT JSONB_AGG(rule.parent_family_id ORDER BY rule.parent_family_id)
parent.id AS "parentFamilyId",parent.code AS "parentFamilyCode",parent.name AS "parentFamilyName" 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 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 WHERE family.id=$1::uuid AND family.is_active=true
LIMIT 1 LIMIT 1
`, [familyId])) as FamilyRow[]; `, [familyId])) as FamilyRow[];
@@ -382,10 +363,19 @@ export class InventoryStructureService {
code: 'INVENTORY_FAMILY_LEVEL_INVALID', code: 'INVENTORY_FAMILY_LEVEL_INVALID',
message: 'La clasificación técnica no corresponde al nivel seleccionado', message: 'La clasificación técnica no corresponde al nivel seleccionado',
}); });
if (kind === 'SUBINSTALACION' && family.parentFamilyId !== parent?.inventoryFamilyId) { if (kind === 'SUBINSTALACION') {
throw new BadRequestException({ 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', 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; return family;
@@ -398,13 +388,8 @@ export class InventoryStructureService {
: kind === 'YACIMIENTO' ? 'YAC' : kind === 'YACIMIENTO' ? 'YAC'
: kind === 'INSTALACION' ? 'INST' : kind === 'INSTALACION' ? 'INST'
: 'SUB'; : 'SUB';
const readable = name const readable = name.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toUpperCase()
.normalize('NFD') .replace(/[^A-Z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'REGISTRO';
.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); return `${prefix}-${readable}-${randomUUID().slice(0, 8).toUpperCase()}`.slice(0, 120);
} }